1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-08 13:29:18 +02:00

Compare commits

...

2 Commits

Author SHA1 Message Date
Dr. Carsten Leue
475d09e987 fix: add skills
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-07 22:39:33 +01:00
Dr. Carsten Leue
fd21bdeabf fix: signature of local for context/readerresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-07 22:03:17 +01:00
8 changed files with 1293 additions and 30 deletions

View File

@@ -652,9 +652,9 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
// type key int
// const userKey key = 0
//
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// addUser := readerio.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// return pair.MakePair(func() {}, newCtx) // No-op cancel
// })
//
// getUser := readerio.FromReader(func(ctx context.Context) string {
@@ -673,8 +673,9 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// withTimeout := readerio.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
// return pair.MakePair(cancel, newCtx)
// })
//
// result := F.Pipe1(

View File

@@ -1028,9 +1028,9 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
// type key int
// const userKey key = 0
//
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// addUser := readerioresult.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// return pair.MakePair(func() {}, newCtx) // No-op cancel
// })
//
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
@@ -1049,8 +1049,9 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// withTimeout := readerioresult.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
// return pair.MakePair(cancel, newCtx)
// })
//
// result := F.Pipe1(

View File

@@ -19,6 +19,8 @@ import (
"context"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/pair"
RR "github.com/IBM/fp-go/v2/readerresult"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderResult.
@@ -34,21 +36,24 @@ import (
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - R: The input environment type that f transforms into context.Context
// - A: The original success type produced by the ReaderResult
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - f: Function to transform the input environment R into context.Context (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to B
//
// Note: When R is context.Context, this simplifies to an Operator[A, B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RR.Kleisli[R, ReaderResult[A], B] {
return function.Flow2(
Local[A](f),
Map(g),
RR.Map[R](g),
)
}
@@ -62,15 +67,18 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
//
// Type Parameters:
// - A: The success type (unchanged)
// - R: The input environment type that f transforms into context.Context
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to A
//
// Note: When R is context.Context, this simplifies to an Operator[A, A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RR.Kleisli[R, ReaderResult[A], A] {
return Local[A](f)
}
@@ -89,16 +97,19 @@ func Contramap[A any](f func(context.Context) (context.Context, context.CancelFu
//
// Type Parameters:
// - A: The result type (unchanged)
// - R: The input environment type that f transforms into context.Context
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) Result[A] {
otherCtx, otherCancel := f(ctx)
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to A
//
// Note: When R is context.Context, this simplifies to an Operator[A, A]
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RR.Kleisli[R, ReaderResult[A], A] {
return func(rr ReaderResult[A]) RR.ReaderResult[R, A] {
return func(r R) Result[A] {
otherCancel, otherCtx := pair.Unpack(f(r))
defer otherCancel()
return rr(otherCtx)
}

View File

@@ -20,6 +20,7 @@ import (
"strconv"
"testing"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -34,9 +35,9 @@ func TestPromapBasic(t *testing.T) {
return R.Of(0)
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
toString := strconv.Itoa
@@ -57,9 +58,9 @@ func TestContramapBasic(t *testing.T) {
return R.Of(0)
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
adapted := Contramap[int](addKey)(getValue)
@@ -79,9 +80,9 @@ func TestLocalBasic(t *testing.T) {
return R.Of("unknown")
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
addUser := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
adapted := Local[string](addUser)(getValue)

View File

@@ -229,9 +229,10 @@ func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
// Example:
//
// // Add a timeout to a specific operation
// withTimeout := statereaderioresult.Local[AppState, Data](
// func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 60*time.Second)
// withTimeout := statereaderioresult.Local[AppState, Data, context.Context](
// func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
// newCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
// return pair.MakePair(cancel, newCtx)
// },
// )
// result := withTimeout(computation)

View File

@@ -0,0 +1,318 @@
# fp-go HTTP Requests
## Overview
fp-go wraps `net/http` in the `ReaderIOResult` monad, giving you composable, context-aware HTTP operations with automatic error propagation. The core package is:
```
github.com/IBM/fp-go/v2/context/readerioresult/http
```
All HTTP operations are lazy — they describe what to do but do not execute until you call the resulting function with a `context.Context`.
## Core Types
```go
// Requester builds an *http.Request given a context.
type Requester = ReaderIOResult[*http.Request] // func(context.Context) func() result.Result[*http.Request]
// Client executes a Requester and returns the response wrapped in ReaderIOResult.
type Client interface {
Do(Requester) ReaderIOResult[*http.Response]
}
```
## Basic Usage
### 1. Create a Client
```go
import (
HTTP "net/http"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
)
client := H.MakeClient(HTTP.DefaultClient)
// Or with a custom client:
custom := &HTTP.Client{Timeout: 10 * time.Second}
client := H.MakeClient(custom)
```
### 2. Build a Request
```go
// GET request (most common)
req := H.MakeGetRequest("https://api.example.com/users/1")
// Arbitrary method + body
req := H.MakeRequest("POST", "https://api.example.com/users", bodyReader)
```
### 3. Execute and Parse
```go
import (
"context"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
client := H.MakeClient(HTTP.DefaultClient)
// ReadJSON validates status, Content-Type, then unmarshals JSON
result := H.ReadJSON[User](client)(H.MakeGetRequest("https://api.example.com/users/1"))
// Execute — provide context once
user, err := result(context.Background())()
```
## Response Readers
All accept a `Client` and return a function `Requester → ReaderIOResult[A]`:
| Function | Returns | Notes |
|----------|---------|-------|
| `ReadJSON[A](client)` | `ReaderIOResult[A]` | Validates status + Content-Type, unmarshals JSON |
| `ReadText(client)` | `ReaderIOResult[string]` | Validates status, reads body as UTF-8 string |
| `ReadAll(client)` | `ReaderIOResult[[]byte]` | Validates status, returns raw body bytes |
| `ReadFullResponse(client)` | `ReaderIOResult[FullResponse]` | Returns `Pair[*http.Response, []byte]` |
`FullResponse = Pair[*http.Response, []byte]` — use `pair.First` / `pair.Second` to access components.
## Composing Requests in Pipelines
```go
import (
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
)
client := H.MakeClient(HTTP.DefaultClient)
readPost := H.ReadJSON[Post](client)
pipeline := F.Pipe2(
H.MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1"),
readPost,
RIO.ChainFirstIOK(IO.Logf[Post]("Got post: %v")),
)
post, err := pipeline(context.Background())()
```
## Parallel Requests — Homogeneous Types
Use `RIO.TraverseArray` when all requests return the same type:
```go
import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
)
type PostItem struct {
UserID uint `json:"userId"`
ID uint `json:"id"`
Title string `json:"title"`
}
client := H.MakeClient(HTTP.DefaultClient)
readPost := H.ReadJSON[PostItem](client)
// Fetch 10 posts in parallel
data := F.Pipe3(
A.MakeBy(10, func(i int) string {
return fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", i+1)
}),
RIO.TraverseArray(F.Flow3(
H.MakeGetRequest,
readPost,
RIO.ChainFirstIOK(IO.Logf[PostItem]("Post: %v")),
)),
RIO.ChainFirstIOK(IO.Logf[[]PostItem]("All posts: %v")),
RIO.Map(A.Size[PostItem]),
)
count, err := data(context.Background())()
```
## Parallel Requests — Heterogeneous Types
Use `RIO.TraverseTuple2` (or `Tuple3`, etc.) when requests return different types:
```go
import (
T "github.com/IBM/fp-go/v2/tuple"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
F "github.com/IBM/fp-go/v2/function"
)
type CatFact struct {
Fact string `json:"fact"`
}
client := H.MakeClient(HTTP.DefaultClient)
readPost := H.ReadJSON[PostItem](client)
readCatFact := H.ReadJSON[CatFact](client)
// Execute both requests in parallel with different response types
data := F.Pipe3(
T.MakeTuple2(
"https://jsonplaceholder.typicode.com/posts/1",
"https://catfact.ninja/fact",
),
T.Map2(H.MakeGetRequest, H.MakeGetRequest), // build both requesters
RIO.TraverseTuple2(readPost, readCatFact), // run in parallel, typed
RIO.ChainFirstIOK(IO.Logf[T.Tuple2[PostItem, CatFact]]("Result: %v")),
)
both, err := data(context.Background())()
// both.F1 is PostItem, both.F2 is CatFact
```
## Building Requests with the Builder API
For complex requests (custom headers, query params, JSON body), use the builder:
```go
import (
B "github.com/IBM/fp-go/v2/http/builder"
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
F "github.com/IBM/fp-go/v2/function"
)
// GET with query parameters
req := F.Pipe2(
B.Default,
B.WithURL("https://api.example.com/items?page=1"),
B.WithQueryArg("limit")("50"),
)
requester := RB.Requester(req)
// POST with JSON body
req := F.Pipe3(
B.Default,
B.WithURL("https://api.example.com/users"),
B.WithMethod("POST"),
B.WithJSON(map[string]string{"name": "Alice"}),
// sets Content-Type: application/json automatically
)
requester := RB.Requester(req)
// With authentication and custom headers
req := F.Pipe3(
B.Default,
B.WithURL("https://api.example.com/protected"),
B.WithBearer("my-token"), // sets Authorization: Bearer my-token
B.WithHeader("X-Request-ID")("123"),
)
requester := RB.Requester(req)
// Execute
result := H.ReadJSON[Response](client)(requester)
data, err := result(ctx)()
```
### Builder Functions
| Function | Effect |
|----------|--------|
| `B.WithURL(url)` | Set the target URL |
| `B.WithMethod(method)` | Set HTTP method (GET, POST, PUT, DELETE, …) |
| `B.WithJSON(v)` | Marshal `v` as JSON body, set `Content-Type: application/json` |
| `B.WithBytes(data)` | Set raw bytes body, set `Content-Length` automatically |
| `B.WithHeader(key)(value)` | Add a request header |
| `B.WithBearer(token)` | Set `Authorization: Bearer <token>` |
| `B.WithQueryArg(key)(value)` | Append a query parameter |
## Error Handling
Errors from request creation, HTTP status codes, Content-Type validation, and JSON parsing all propagate automatically through the `Result` monad. You only handle errors at the call site:
```go
// Pattern 1: direct extraction
value, err := pipeline(ctx)()
if err != nil { /* handle */ }
// Pattern 2: Fold for clean HTTP handler
RIO.Fold(
func(err error) { http.Error(w, err.Error(), http.StatusInternalServerError) },
func(data MyType) { json.NewEncoder(w).Encode(data) },
)(pipeline)(ctx)()
```
## Full HTTP Handler Example
```go
package main
import (
"context"
"encoding/json"
"net/http"
HTTP "net/http"
"fmt"
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
)
type Post struct {
ID int `json:"id"`
Title string `json:"title"`
}
var client = H.MakeClient(HTTP.DefaultClient)
func fetchPost(id int) RIO.ReaderIOResult[Post] {
url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id)
return F.Pipe2(
H.MakeGetRequest(url),
H.ReadJSON[Post](client),
RIO.ChainFirstIOK(IO.Logf[Post]("fetched: %v")),
)
}
func handler(w http.ResponseWriter, r *http.Request) {
RIO.Fold(
func(err error) {
http.Error(w, err.Error(), http.StatusBadGateway)
},
func(post Post) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(post)
},
)(fetchPost(1))(r.Context())()
}
```
## Import Reference
```go
import (
HTTP "net/http"
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
B "github.com/IBM/fp-go/v2/http/builder"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/array"
T "github.com/IBM/fp-go/v2/tuple"
IO "github.com/IBM/fp-go/v2/io"
)
```
Requires Go 1.24+.

View File

@@ -0,0 +1,410 @@
# fp-go Logging
## Overview
fp-go provides logging utilities that integrate naturally with functional pipelines. Logging is always a **side effect** — it should not change the value being processed. The library achieves this through `ChainFirst`-style combinators that thread the original value through unchanged while performing the log.
## Packages
| Package | Purpose |
|---------|---------|
| `github.com/IBM/fp-go/v2/logging` | Global logger, context-embedded logger, `LoggingCallbacks` |
| `github.com/IBM/fp-go/v2/io` | `Logf`, `Logger`, `LogGo`, `Printf`, `PrintGo` — IO-level logging helpers |
| `github.com/IBM/fp-go/v2/readerio` | `SLog`, `SLogWithCallback` — structured logging for ReaderIO |
| `github.com/IBM/fp-go/v2/context/readerio` | `SLog`, `SLogWithCallback` — structured logging for context ReaderIO |
| `github.com/IBM/fp-go/v2/context/readerresult` | `SLog`, `TapSLog`, `SLogWithCallback` — structured logging for ReaderResult |
| `github.com/IBM/fp-go/v2/context/readerioresult` | `SLog`, `TapSLog`, `SLogWithCallback`, `LogEntryExit`, `LogEntryExitWithCallback` — full suite for ReaderIOResult |
## Logging Inside Pipelines
The idiomatic way to log inside a monadic pipeline is `ChainFirstIOK` (or `ChainFirst` where the monad is already IO). These combinators execute a side-effecting function and pass the **original value** downstream unchanged.
### With `IOResult` / `ReaderIOResult` — printf-style
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
fetchUser(42),
RIO.ChainEitherK(validateUser),
// Log after validation — value flows through unchanged
RIO.ChainFirstIOK(IO.Logf[User]("Validated user: %v")),
RIO.Map(enrichUser),
)
```
`IO.Logf[A](format string) func(A) IO[A]` logs using `log.Printf` and returns the value unchanged. It's a Kleisli arrow suitable for `ChainFirst` and `ChainFirstIOK`.
### With `IOEither` / plain `IO`
```go
import (
IOE "github.com/IBM/fp-go/v2/ioeither"
IO "github.com/IBM/fp-go/v2/io"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
file.ReadFile("config.json"),
IOE.ChainEitherK(J.Unmarshal[Config]),
IOE.ChainFirstIOK(IO.Logf[Config]("Loaded config: %v")),
IOE.Map[error](processConfig),
)
```
### Logging Arrays in TraverseArray
```go
import (
A "github.com/IBM/fp-go/v2/array"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
IO "github.com/IBM/fp-go/v2/io"
F "github.com/IBM/fp-go/v2/function"
)
// Log each item individually, then log the final slice
pipeline := F.Pipe2(
A.MakeBy(3, idxToFilename),
RIO.TraverseArray(F.Flow3(
file.ReadFile,
RIO.ChainEitherK(J.Unmarshal[Record]),
RIO.ChainFirstIOK(IO.Logf[Record]("Parsed record: %v")),
)),
RIO.ChainFirstIOK(IO.Logf[[]Record]("All records: %v")),
)
```
## IO Logging Functions
All live in `github.com/IBM/fp-go/v2/io`:
### `Logf` — printf-style
```go
IO.Logf[A any](format string) func(A) IO[A]
```
Uses `log.Printf`. The format string works like `fmt.Sprintf`.
```go
IO.Logf[User]("Processing user: %+v")
IO.Logf[int]("Count: %d")
```
### `Logger` — with custom `*log.Logger`
```go
IO.Logger[A any](loggers ...*log.Logger) func(prefix string) func(A) IO[A]
```
Uses `logger.Printf(prefix+": %v", value)`. Pass your own `*log.Logger` instance.
```go
customLog := log.New(os.Stderr, "APP ", log.LstdFlags)
logUser := IO.Logger[User](customLog)("user")
// logs: "APP user: {ID:42 Name:Alice}"
```
### `LogGo` — Go template syntax
```go
IO.LogGo[A any](tmpl string) func(A) IO[A]
```
Uses Go's `text/template`. The template receives the value as `.`.
```go
type User struct{ Name string; Age int }
IO.LogGo[User]("User {{.Name}} is {{.Age}} years old")
```
### `Printf` / `PrintGo` — stdout instead of log
Same signatures as `Logf` / `LogGo` but use `fmt.Printf`/`fmt.Println` (no log prefix, no timestamp).
```go
IO.Printf[Result]("Result: %v\n")
IO.PrintGo[User]("Name: {{.Name}}")
```
## Structured Logging in the `context` Package
The `context/readerioresult`, `context/readerresult`, and `context/readerio` packages provide structured `slog`-based logging functions that are context-aware: they retrieve the logger from the context (via `logging.GetLoggerFromContext`) rather than using a fixed logger instance.
### `TapSLog` — inline structured logging in a ReaderIOResult pipeline
`TapSLog` is an **Operator** (`func(ReaderIOResult[A]) ReaderIOResult[A]`). It sits directly in a `F.Pipe` call on a `ReaderIOResult`, logs the current value or error using `slog`, and passes the result through unchanged.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe4(
fetchOrder(orderID),
RIO.TapSLog[Order]("Order fetched"), // logs value=<Order> or error=<err>
RIO.Chain(validateOrder),
RIO.TapSLog[Order]("Order validated"),
RIO.Chain(processPayment),
)
result, err := pipeline(ctx)()
```
- Logs **both** success values (`value=<A>`) and errors (`error=<err>`) using `slog` structured attributes.
- Respects the logger level — if the logger is configured to discard Info-level logs, nothing is written.
- Available in both `context/readerioresult` and `context/readerresult`.
### `SLog` — Kleisli-style structured logging
`SLog` is a **Kleisli arrow** (`func(Result[A]) ReaderResult[A]` / `func(Result[A]) ReaderIOResult[A]`). It is used with `Chain` when you want to intercept the raw `Result` directly.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
fetchData(id),
RIO.Chain(RIO.SLog[Data]("Data fetched")), // log raw Result, pass it through
RIO.Chain(validateData),
RIO.Chain(RIO.SLog[Data]("Data validated")),
RIO.Chain(processData),
)
```
**Difference from `TapSLog`:**
- `TapSLog[A](msg)` is an `Operator[A, A]` — used directly in `F.Pipe` on a `ReaderIOResult[A]`.
- `SLog[A](msg)` is a `Kleisli[Result[A], A]` — used with `Chain`, giving access to the raw `Result[A]`.
Both log in the same format. `TapSLog` is more ergonomic in most pipelines.
### `SLogWithCallback` — custom log level and logger source
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
"log/slog"
)
// Log at DEBUG level with a custom logger extracted from context
debugLog := RIO.SLogWithCallback[User](
slog.LevelDebug,
logging.GetLoggerFromContext, // or any func(context.Context) *slog.Logger
"Fetched user",
)
pipeline := F.Pipe2(
fetchUser(123),
RIO.Chain(debugLog),
RIO.Map(func(u User) string { return u.Name }),
)
```
### `LogEntryExit` — automatic entry/exit timing with correlation IDs
`LogEntryExit` wraps a `ReaderIOResult` computation with structured entry and exit log messages. It assigns a unique **correlation ID** (`ID=<n>`) to each invocation so concurrent or nested operations can be correlated in logs.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
pipeline := F.Pipe3(
fetchUser(123),
RIO.LogEntryExit[User]("fetchUser"), // wraps the operation
RIO.Chain(func(user User) RIO.ReaderIOResult[[]Order] {
return F.Pipe1(
fetchOrders(user.ID),
RIO.LogEntryExit[[]Order]("fetchOrders"),
)
}),
)
result, err := pipeline(ctx)()
// Logs:
// level=INFO msg="[entering]" name=fetchUser ID=1
// level=INFO msg="[exiting ]" name=fetchUser ID=1 duration=42ms
// level=INFO msg="[entering]" name=fetchOrders ID=2
// level=INFO msg="[exiting ]" name=fetchOrders ID=2 duration=18ms
```
On error, the exit log changes to `[throwing]` and includes the error:
```
level=INFO msg="[throwing]" name=fetchUser ID=3 duration=5ms error="user not found"
```
Key properties:
- **Correlation ID** (`ID=`) is unique per operation, monotonically increasing, and stored in the context so nested operations can access the parent's ID.
- **Duration** (`duration=`) is measured from entry to exit.
- **Logger is taken from the context** — embed a request-scoped logger with `logging.WithLogger` before executing the pipeline and `LogEntryExit` picks it up automatically.
- **Level-aware** — if the logger does not have the log level enabled, the entire entry/exit instrumentation is skipped (zero overhead).
- The original `ReaderIOResult[A]` value flows through **unchanged**.
```go
// Use a context logger so all log messages carry request metadata
cancelFn, ctxWithLogger := pair.Unpack(
logging.WithLogger(
slog.Default().With("requestID", r.Header.Get("X-Request-ID")),
)(r.Context()),
)
defer cancelFn()
result, err := pipeline(ctxWithLogger)()
```
### `LogEntryExitWithCallback` — custom log level
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
"log/slog"
)
// Log at DEBUG level instead of INFO
debugPipeline := F.Pipe1(
expensiveComputation(),
RIO.LogEntryExitWithCallback[Result](
slog.LevelDebug,
logging.GetLoggerFromContext,
"expensiveComputation",
),
)
```
### `SLog` / `SLogWithCallback` in `context/readerresult`
The same `SLog` and `TapSLog` functions are also available in `context/readerresult` for use with the synchronous `ReaderResult[A] = func(context.Context) (A, error)` monad:
```go
import RR "github.com/IBM/fp-go/v2/context/readerresult"
pipeline := F.Pipe3(
queryDB(id),
RR.TapSLog[Row]("Row fetched"),
RR.Chain(parseRow),
RR.TapSLog[Record]("Record parsed"),
)
```
## Global Logger (`logging` package)
The `logging` package manages a global `*slog.Logger` (structured logging, Go 1.21+).
```go
import "github.com/IBM/fp-go/v2/logging"
// Get the current global logger (defaults to slog.Default())
logger := logging.GetLogger()
logger.Info("application started", "version", "1.0")
// Replace the global logger; returns the old one for deferred restore
old := logging.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
defer logging.SetLogger(old)
```
## Context-Embedded Logger
Embed a `*slog.Logger` in a `context.Context` to carry request-scoped loggers across the call stack. All context-package logging functions (`TapSLog`, `SLog`, `LogEntryExit`) pick up this logger automatically.
```go
import (
"github.com/IBM/fp-go/v2/logging"
"github.com/IBM/fp-go/v2/pair"
"log/slog"
)
// Create a request-scoped logger
reqLogger := slog.Default().With("requestID", "abc-123")
// Embed it into a context using the Kleisli arrow WithLogger
cancelFn, ctxWithLogger := pair.Unpack(logging.WithLogger(reqLogger)(ctx))
defer cancelFn()
// All downstream logging (TapSLog, LogEntryExit, etc.) uses reqLogger
result, err := pipeline(ctxWithLogger)()
```
`WithLogger` returns a `ContextCancel = Pair[context.CancelFunc, context.Context]`. The cancel function is a no-op — the context is only enriched, not made cancellable.
`GetLoggerFromContext` falls back to the global logger if no logger is found in the context.
## `LoggingCallbacks` — Dual-Logger Pattern
```go
import "github.com/IBM/fp-go/v2/logging"
// Returns (infoCallback, errorCallback) — both are func(string, ...any)
infoLog, errLog := logging.LoggingCallbacks() // use log.Default() for both
infoLog, errLog := logging.LoggingCallbacks(myLogger) // same logger for both
infoLog, errLog := logging.LoggingCallbacks(infoLog, errorLog) // separate loggers
```
Used internally by `io.Logger` and by packages that need separate info/error sinks.
## Choosing the Right Logging Function
| Situation | Use |
|-----------|-----|
| Quick printf logging mid-pipeline | `IO.Logf[A]("fmt")` with `ChainFirstIOK` |
| Go template formatting mid-pipeline | `IO.LogGo[A]("tmpl")` with `ChainFirstIOK` |
| Print to stdout (no log prefix) | `IO.Printf[A]("fmt")` with `ChainFirstIOK` |
| Structured slog — log value or error inline | `RIO.TapSLog[A]("msg")` (Operator, used in Pipe) |
| Structured slog — intercept raw Result | `RIO.Chain(RIO.SLog[A]("msg"))` (Kleisli) |
| Structured slog — custom log level | `RIO.SLogWithCallback[A](level, cb, "msg")` |
| Entry/exit timing + correlation IDs | `RIO.LogEntryExit[A]("name")` |
| Entry/exit at custom log level | `RIO.LogEntryExitWithCallback[A](level, cb, "name")` |
| Structured logging globally | `logging.GetLogger()` / `logging.SetLogger()` |
| Request-scoped logger in context | `logging.WithLogger(logger)` + `logging.GetLoggerFromContext(ctx)` |
| Custom `*log.Logger` in pipeline | `IO.Logger[A](logger)("prefix")` with `ChainFirstIOK` |
## Complete Example
```go
package main
import (
"context"
"log/slog"
"os"
F "github.com/IBM/fp-go/v2/function"
IO "github.com/IBM/fp-go/v2/io"
L "github.com/IBM/fp-go/v2/logging"
P "github.com/IBM/fp-go/v2/pair"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
)
func main() {
// Configure JSON structured logging globally
L.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
// Embed a request-scoped logger into the context
_, ctx := P.Unpack(L.WithLogger(
L.GetLogger().With("requestID", "req-001"),
)(context.Background()))
pipeline := F.Pipe5(
fetchData(42),
RIO.LogEntryExit[Data]("fetchData"), // entry/exit with timing + ID
RIO.TapSLog[Data]("raw data"), // inline structured value log
RIO.ChainEitherK(transformData),
RIO.LogEntryExit[Result]("transformData"),
RIO.ChainFirstIOK(IO.LogGo[Result]("result: {{.Value}}")), // template log
)
value, err := pipeline(ctx)()
if err != nil {
L.GetLogger().Error("pipeline failed", "error", err)
}
_ = value
}
```

View File

@@ -0,0 +1,520 @@
# fp-go Monadic Operations
## Overview
`fp-go` (import path `github.com/IBM/fp-go/v2`) brings type-safe functional programming to Go using generics. Every monad follows a **consistent interface**: once you know the pattern in one monad, it transfers to all others.
All functions use the **data-last** principle: the data being transformed is always the last argument, enabling partial application and pipeline composition.
## Core Types
| Type | Package | Represents |
|------|---------|------------|
| `Option[A]` | `option` | A value that may or may not be present (replaces nil) |
| `Either[E, A]` | `either` | A value that is either a left error `E` or a right success `A` |
| `Result[A]` | `result` | `Either[error, A]` — shorthand for the common case |
| `IO[A]` | `io` | A lazy computation that produces `A` (possibly with side effects) |
| `IOResult[A]` | `ioresult` | `IO[Result[A]]` — lazy computation that can fail |
| `ReaderIOResult[A]` | `context/readerioresult` | `func(context.Context) IOResult[A]` — context-aware IO with errors |
| `Effect[C, A]` | `effect` | `func(C) ReaderIOResult[A]` — typed dependency injection + IO + errors |
Idiomatic (high-performance, tuple-based) equivalents live in `idiomatic/`:
- `idiomatic/option``(A, bool)` tuples
- `idiomatic/result``(A, error)` tuples
- `idiomatic/ioresult``func() (A, error)`
- `idiomatic/context/readerresult``func(context.Context) (A, error)`
## Standard Operations
Every monad exports these operations (PascalCase for exported Go names):
| fp-go | fp-ts / Haskell | Description |
|-------|----------------|-------------|
| `Of` | `of` / `pure` | Lift a pure value into the monad |
| `Map` | `map` / `fmap` | Transform the value inside without changing the context |
| `Chain` | `chain` / `>>=` | Sequence a computation that itself returns a monadic value |
| `Ap` | `ap` / `<*>` | Apply a wrapped function to a wrapped value |
| `Fold` | `fold` / `either` | Eliminate the context — handle every case and extract a plain value |
| `GetOrElse` | `getOrElse` / `fromMaybe` | Extract the value or use a default (Option/Result) |
| `Filter` | `filter` / `mfilter` | Keep only values satisfying a predicate |
| `Flatten` | `flatten` / `join` | Remove one level of nesting (`M[M[A]]``M[A]`) |
| `ChainFirst` | `chainFirst` / `>>` | Sequence for side effects; keeps the original value |
| `Alt` | `alt` / `<\|>` | Provide an alternative when the first computation fails |
| `FromPredicate` | `fromPredicate` / `guard` | Build a monadic value from a predicate |
| `Sequence` | `sequence` | Turn `[]M[A]` into `M[[]A]` |
| `Traverse` | `traverse` | Map and sequence in one step |
Curried (composable) vs. monadic (direct) form:
```go
// Curried — data last, returns a transformer function
option.Map(strings.ToUpper) // func(Option[string]) Option[string]
// Monadic — data first, immediate execution
option.MonadMap(option.Some("hello"), strings.ToUpper)
```
Use curried form for pipelines; use `Monad*` form when you already have all arguments.
## Key Type Aliases (defined per monad)
```go
// A Kleisli arrow: a function from A to a monadic B
type Kleisli[A, B any] = func(A) M[B]
// An operator: transforms one monadic value into another
type Operator[A, B any] = func(M[A]) M[B]
```
`Chain` takes a `Kleisli`, `Map` returns an `Operator`. The naming is consistent across all monads.
## Examples
### Option — nullable values without nil
```go
import (
O "github.com/IBM/fp-go/v2/option"
F "github.com/IBM/fp-go/v2/function"
"strconv"
)
parseAndDouble := F.Flow2(
O.FromPredicate(func(s string) bool { return s != "" }),
O.Chain(func(s string) O.Option[int] {
n, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(n * 2)
}),
)
parseAndDouble("21") // Some(42)
parseAndDouble("") // None
parseAndDouble("abc") // None
```
### Result — error handling without if-err boilerplate
```go
import (
R "github.com/IBM/fp-go/v2/result"
F "github.com/IBM/fp-go/v2/function"
"strconv"
"errors"
)
parse := R.Eitherize1(strconv.Atoi) // lifts (int, error) → Result[int]
validate := func(n int) R.Result[int] {
if n < 0 {
return R.Error[int](errors.New("must be non-negative"))
}
return R.Of(n)
}
pipeline := F.Flow2(parse, R.Chain(validate))
pipeline("42") // Ok(42)
pipeline("-1") // Error("must be non-negative")
pipeline("abc") // Error(strconv parse error)
```
### IOResult — lazy IO with error handling
```go
import (
IOE "github.com/IBM/fp-go/v2/ioresult"
F "github.com/IBM/fp-go/v2/function"
J "github.com/IBM/fp-go/v2/json"
"os"
)
readConfig := F.Flow2(
IOE.Eitherize1(os.ReadFile), // func(string) IOResult[[]byte]
IOE.ChainEitherK(J.Unmarshal[Config]), // parse JSON, propagate errors
)
result := readConfig("config.json")() // execute lazily
```
### ReaderIOResult — context-aware pipelines (recommended for services)
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"context"
)
// type ReaderIOResult[A any] = func(context.Context) func() result.Result[A]
fetchUser := func(id int) RIO.ReaderIOResult[User] {
return func(ctx context.Context) func() result.Result[User] {
return func() result.Result[User] {
// perform IO here
}
}
}
pipeline := F.Pipe3(
fetchUser(42),
RIO.ChainEitherK(validateUser), // lift pure (User, error) function
RIO.Map(enrichUser), // lift pure User → User function
RIO.ChainFirstIOK(IO.Logf[User]("Fetched: %v")), // side-effect logging
)
user, err := pipeline(ctx)() // provide context once, execute
```
### Traversal — process slices monadically
```go
import (
A "github.com/IBM/fp-go/v2/array"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
// Fetch all users, stop on first error
fetchAll := F.Pipe1(
A.MakeBy(10, userID),
RIO.TraverseArray(fetchUser), // []ReaderIOResult[User] → ReaderIOResult[[]User]
)
```
## Function Composition with Flow and Pipe
```go
import F "github.com/IBM/fp-go/v2/function"
// Flow: compose functions left-to-right, returns a new function
transform := F.Flow3(
option.Map(strings.TrimSpace),
option.Filter(func(s string) bool { return s != "" }),
option.GetOrElse(func() string { return "default" }),
)
result := transform(option.Some(" hello ")) // "hello"
// Pipe: apply a value through a pipeline immediately
result := F.Pipe3(
option.Some(" hello "),
option.Map(strings.TrimSpace),
option.Filter(func(s string) bool { return s != "" }),
option.GetOrElse(func() string { return "default" }),
)
```
## Lifting Pure Functions into Monadic Context
fp-go provides helpers to promote non-monadic functions:
| Helper | Lifts |
|--------|-------|
| `ChainEitherK` | `func(A) (B, error)` → works inside the monad |
| `ChainOptionK` | `func(A) Option[B]` → works inside the monad |
| `ChainFirstIOK` | `func(A) IO[B]` for side effects, keeps original value |
| `Eitherize1..N` | `func(A) (B, error)``func(A) Result[B]` |
| `FromPredicate` | `func(A) bool` + error builder → `func(A) Result[A]` |
## Type Parameter Ordering Rule (V2)
Non-inferrable type parameters come **first**, so the compiler can infer the rest:
```go
// B cannot be inferred from the argument — it comes first
result := either.Ap[string](value)(funcInEither)
// All types inferrable — no explicit params needed
result := either.Map(transform)(value)
result := either.Chain(validator)(value)
```
## When to Use Which Monad
| Situation | Use |
|-----------|-----|
| Value that might be absent | `Option[A]` |
| Operation that can fail with custom error type | `Either[E, A]` |
| Operation that can fail with `error` | `Result[A]` |
| Lazy IO, side effects | `IO[A]` |
| IO that can fail | `IOResult[A]` |
| IO + context (cancellation, deadlines) | `ReaderIOResult[A]` from `context/readerioresult` |
| IO + context + typed dependencies | `Effect[C, A]` |
| High-performance services | Idiomatic packages in `idiomatic/` |
## Do-Notation: Accumulating State with `Bind` and `ApS`
When a pipeline needs to carry **multiple intermediate results** forward — not just a single value — the `Chain`/`Map` style becomes unwieldy because each step only threads one value and prior results are lost. Do-notation solves this by accumulating results into a growing struct (the "state") at each step.
Every monad that supports do-notation exports the same family of functions. The examples below use `context/readerioresult` (`RIO`), but the identical API is available in `result`, `option`, `ioresult`, `readerioresult`, and others.
### The Function Family
| Function | Kind | What it does |
|----------|------|-------------|
| `Do(empty S)` | — | Lift an empty struct into the monad; starting point |
| `BindTo(setter)` | monadic | Convert an existing `M[T]` into `M[S]`; alternative start |
| `Bind(setter, f)` | monadic | Add a result; `f` receives the **current state** and returns `M[T]` |
| `ApS(setter, fa)` | applicative | Add a result; `fa` is **independent** of the current state |
| `Let(setter, f)` | pure | Add a value computed by a **pure function** of the state |
| `LetTo(setter, value)` | pure | Add a **constant** value |
Lens variants (`BindL`, `ApSL`, `LetL`, `LetToL`) accept a `Lens[S, T]` instead of a manual setter, integrating naturally with the optics system.
### `Bind` — Sequential, Dependent Steps
`Bind` sequences two monadic computations. The function `f` receives the **full accumulated state** so it can read anything gathered so far. Errors short-circuit automatically.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
"context"
)
type Pipeline struct {
User User
Config Config
Posts []Post
}
// Lenses — focus on individual fields; .Set is already func(T) func(S) S
var (
userLens = L.MakeLens(func(s Pipeline) User { return s.User }, func(s Pipeline, u User) Pipeline { s.User = u; return s })
configLens = L.MakeLens(func(s Pipeline) Config { return s.Config }, func(s Pipeline, c Config) Pipeline { s.Config = c; return s })
postsLens = L.MakeLens(func(s Pipeline) []Post { return s.Posts }, func(s Pipeline, p []Post) Pipeline { s.Posts = p; return s })
)
result := F.Pipe3(
RIO.Do(Pipeline{}), // lift empty struct
RIO.Bind(userLens.Set, func(_ Pipeline) RIO.ReaderIOResult[User] { return fetchUser(42) }),
RIO.Bind(configLens.Set, F.Flow2(userLens.Get, fetchConfigForUser)), // read s.User, pass to fetcher
RIO.Bind(postsLens.Set, F.Flow2(userLens.Get, fetchPostsForUser)), // read s.User, pass to fetcher
)
pipeline, err := result(context.Background())()
// pipeline.User, pipeline.Config, pipeline.Posts are all populated
```
The setter signature is `func(T) func(S1) S2` — it takes the new value and returns a state transformer. `lens.Set` already has this shape, so no manual setter functions are needed. `F.Flow2(lens.Get, f)` composes the field getter with any Kleisli arrow `f` point-free.
### `ApS` — Independent, Applicative Steps
`ApS` uses **applicative** semantics: `fa` is evaluated without any access to the current state. Use it when steps have no dependency on each other — the library can choose to execute them concurrently.
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
)
type Summary struct {
User User
Weather Weather
}
var (
userLens = L.MakeLens(func(s Summary) User { return s.User }, func(s Summary, u User) Summary { s.User = u; return s })
weatherLens = L.MakeLens(func(s Summary) Weather { return s.Weather }, func(s Summary, w Weather) Summary { s.Weather = w; return s })
)
// Both are independent — neither needs the other's result
result := F.Pipe2(
RIO.Do(Summary{}),
RIO.ApS(userLens.Set, fetchUser(42)),
RIO.ApS(weatherLens.Set, fetchWeather("NYC")),
)
```
**Key difference from `Bind`:**
| | `Bind(setter, f)` | `ApS(setter, fa)` |
|-|---|---|
| Second argument | `func(S1) M[T]` — a **function** of state | `M[T]` — a **fixed** monadic value |
| Can read prior state? | Yes — receives `S1` | No — no access to state |
| Semantics | Monadic (sequential) | Applicative (independent) |
### `Let` and `LetTo` — Pure Additions
`Let` adds a value computed by a **pure function** of the current state (no monad, cannot fail):
```go
import (
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
)
type Enriched struct {
User User
FullName string
}
var (
userLens = L.MakeLens(func(s Enriched) User { return s.User }, func(s Enriched, u User) Enriched { s.User = u; return s })
fullNameLens = L.MakeLens(func(s Enriched) string { return s.FullName }, func(s Enriched, n string) Enriched { s.FullName = n; return s })
)
fullName := func(u User) string { return u.FirstName + " " + u.LastName }
result := F.Pipe2(
RIO.Do(Enriched{}),
RIO.Bind(userLens.Set, func(_ Enriched) RIO.ReaderIOResult[User] { return fetchUser(42) }),
RIO.Let(fullNameLens.Set, F.Flow2(userLens.Get, fullName)), // read s.User, compute pure string
)
```
`LetTo` adds a **constant** with no computation:
```go
RIO.LetTo(setVersion, "v1.2.3")
```
### `BindTo` — Starting from an Existing Value
When you have an existing `M[T]` and want to project it into a state struct rather than starting from `Do(empty)`:
```go
type State struct{ User User }
result := F.Pipe1(
fetchUser(42), // ReaderIOResult[User]
RIO.BindTo(func(u User) State { return State{User: u} }),// ReaderIOResult[State]
)
```
### Lens Variants (`ApSL`, `BindL`, `LetL`, `LetToL`)
If you have a `Lens[S, T]` (from the optics system or code generation), you can skip writing the setter function entirely:
```go
import (
RO "github.com/IBM/fp-go/v2/readeroption"
F "github.com/IBM/fp-go/v2/function"
)
// Lenses generated by go:generate (see optics/README.md)
// personLenses.Name : Lens[*Person, Name]
// personLenses.Age : Lens[*Person, Age]
makePerson := F.Pipe2(
RO.Do[*PartialPerson](emptyPerson),
RO.ApSL(personLenses.Name, maybeName), // replaces: ApS(personLenses.Name.Set, maybeName)
RO.ApSL(personLenses.Age, maybeAge),
)
```
This exact pattern is used in [`samples/builder`](samples/builder/builder.go) to validate and construct a `Person` from an unvalidated `PartialPerson`.
### Lifted Variants for Mixed Monads
`context/readerioresult` provides `Bind*K` helpers that lift simpler computations directly into the do-chain:
| Helper | Lifts |
|--------|-------|
| `BindResultK` / `BindEitherK` | `func(S1) (T, error)` — pure result |
| `BindIOResultK` / `BindIOEitherK` | `func(S1) func() (T, error)` — lazy IO result |
| `BindIOK` | `func(S1) func() T` — infallible IO |
| `BindReaderK` | `func(S1) func(ctx) T` — context reader |
```go
RIO.BindResultK(setUser, func(s Pipeline) (User, error) {
return validateAndBuild(s) // plain (value, error) function, no wrapping needed
})
```
### Decision Guide
```
Does the new step need to read prior accumulated state?
YES → Bind (monadic, sequential; f receives current S)
NO → ApS (applicative, independent; fa is a fixed M[T])
Is the new value derived purely from state, with no monad?
YES → Let (pure function of S)
Is the new value a compile-time or runtime constant?
YES → LetTo
Starting from an existing M[T] rather than an empty struct?
YES → BindTo
```
### Complete Example — `result` Monad
The same pattern works with simpler monads. Here with `result.Result[A]`:
`Eitherize1` converts any standard `func(A) (B, error)` into `func(A) Result[B]`. Define these lifted functions once as variables. Then use lenses to focus on individual struct fields and compose with `F.Flow2(lens.Get, f)` — no inline lambdas, no manual error handling.
```go
import (
R "github.com/IBM/fp-go/v2/result"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
N "github.com/IBM/fp-go/v2/number"
"strconv"
)
type Parsed struct {
Raw string
Number int
Double int
}
// Lenses — focus on individual fields of Parsed.
var (
rawLens = L.MakeLens(
func(s Parsed) string { return s.Raw },
func(s Parsed, v string) Parsed { s.Raw = v; return s },
)
numberLens = L.MakeLens(
func(s Parsed) int { return s.Number },
func(s Parsed, v int) Parsed { s.Number = v; return s },
)
doubleLens = L.MakeLens(
func(s Parsed) int { return s.Double },
func(s Parsed, v int) Parsed { s.Double = v; return s },
)
)
// Lifted functions — convert standard (value, error) functions into Result-returning ones.
var (
atoi = R.Eitherize1(strconv.Atoi) // func(string) Result[int]
)
parse := func(input string) R.Result[Parsed] {
return F.Pipe3(
R.Do(Parsed{}),
R.LetTo(rawLens.Set, input), // set Raw to constant input
R.Bind(numberLens.Set, F.Flow2(rawLens.Get, atoi)), // get Raw, parse → Result[int]
R.Let(doubleLens.Set, F.Flow2(numberLens.Get, N.Mul(2))), // get Number, multiply → int
)
}
parse("21") // Ok(Parsed{Raw:"21", Number:21, Double:42})
parse("abc") // Error(strconv parse error)
```
`rawLens.Set` is already `func(string) func(Parsed) Parsed`, matching the setter signature `Bind` and `LetTo` expect — no manual setter functions to write. `F.Flow2(rawLens.Get, atoi)` composes the field getter with the eitherized parse function into a `Kleisli[Parsed, int]` without any intermediate lambda.
## Import Paths
```go
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/array"
)
```
Requires Go 1.24+ (generic type aliases).