1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-10 13:31:01 +02:00

Compare commits

...

44 Commits

Author SHA1 Message Date
Dr. Carsten Leue
c6d30bb642 fix: increase test timeout
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:21:27 +01:00
Dr. Carsten Leue
1821f00fbe fix: introduce effect.LocalReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:52:20 +01:00
Dr. Carsten Leue
f0ec0b2541 fix: optimize record performance
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:20:19 +01:00
Dr. Carsten Leue
ce3c7d9359 fix: documentation of endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:02:11 +01:00
Dr. Carsten Leue
3ed354cc8c fix: implement endomorphism.Read
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 19:01:22 +01:00
Dr. Carsten Leue
0932c8c464 fix: add tests for totality and move skills
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 14:12:41 +01:00
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
Dr. Carsten Leue
6834f72856 fix: make signature of Local for context more generic, but backwards compatible
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-07 21:02:24 +01:00
Dr. Carsten Leue
8cfb7ef659 fix: logging implementation for context sensitive operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 23:54:42 +01:00
Dr. Carsten Leue
622c87d734 fix: logging implementation for context sensitive operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 23:50:54 +01:00
Dr. Carsten Leue
2ce406a410 fix: add context7 badge
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 15:03:05 +01:00
Dr. Carsten Leue
3743361b9f fix: context7 at correct location
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 14:41:47 +01:00
Dr. Carsten Leue
69d11f0a4b fix: claim context7
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-06 14:39:58 +01:00
Dr. Carsten Leue
e4dd1169c4 fix: recursion in Errors()
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-04 11:12:22 +01:00
Dr. Carsten Leue
1657569f1d Merge branch 'main' of github.com:IBM/fp-go 2026-03-04 10:31:04 +01:00
Dr. Carsten Leue
545876d013 fix: add bool codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-04 10:30:55 +01:00
renovate[bot]
9492c5d994 chore(deps): update actions/setup-node action to v6.3.0 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-04 09:20:04 +00:00
Dr. Carsten Leue
94b1ea30d1 fix: improved doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-02 13:23:59 +01:00
Dr. Carsten Leue
a77d61f632 fix: add Bind for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-02 13:01:21 +01:00
renovate[bot]
66b2f57d73 fix(deps): update module github.com/urfave/cli/v3 to v3.7.0 (#157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 08:40:21 +00:00
Dr. Carsten Leue
92eb2a50a2 fix: support MarshalJSON and MarshalText types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-01 22:38:20 +01:00
Dr. Carsten Leue
3df1dca146 fix: better result type for Pipe
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 17:03:12 +01:00
Dr. Carsten Leue
a0132e2e92 fix: parameter order
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 15:08:11 +01:00
Dr. Carsten Leue
c6b342908d doc: better explanation for logger
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:52:08 +01:00
Dr. Carsten Leue
962237492f doc: explain Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:27:20 +01:00
Dr. Carsten Leue
168a6e1072 fix: add Eitherize to Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 12:55:03 +01:00
Dr. Carsten Leue
4d67b1d254 fix: expose Empty for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 10:52:23 +01:00
Dr. Carsten Leue
77a8cc6b09 fix: implement ApSO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:44:27 +01:00
Dr. Carsten Leue
bc8743fdfc fix: build error
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:21:37 +01:00
Dr. Carsten Leue
1837d3f86d fix: add semigroup helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 16:39:05 +01:00
Dr. Carsten Leue
b2d111e8ec fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 14:04:44 +01:00
Dr. Carsten Leue
ae141c85c6 fix: add tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 13:40:12 +01:00
Dr. Carsten Leue
1230b4581b doc: add doc links
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 10:12:20 +01:00
Dr. Carsten Leue
70c831c8f9 fix: simplify type arguments
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 16:27:21 +01:00
Dr. Carsten Leue
cc0c14c7cf fix: do not fail if coverage fails
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 11:35:16 +01:00
Dr. Carsten Leue
19159ad49e fix: WriteFile
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 10:42:46 +01:00
Dr. Carsten Leue
b9c8fb4ff1 fix: parameter order for Local and TapIOK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 17:55:32 +01:00
Dr. Carsten Leue
4529e720bc fix: add ChainThunkK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:59:51 +01:00
Dr. Carsten Leue
ef8b2ea65d fix: add ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:31:51 +01:00
Dr. Carsten Leue
5bd7caafdd fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 13:42:29 +01:00
Dr. Carsten Leue
47ebcd79b1 fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:15:00 +01:00
Dr. Carsten Leue
dbad94806e fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:14:21 +01:00
Dr. Carsten Leue
c4cac1cb3e fix: add FromReaderResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-21 16:44:51 +01:00
144 changed files with 21470 additions and 2138 deletions

View File

@@ -42,6 +42,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -81,6 +82,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -106,6 +108,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -131,7 +134,7 @@ jobs:
fetch-depth: 0
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: ${{ env.NODE_VERSION }}

4
context7.json Normal file
View File

@@ -0,0 +1,4 @@
{
"url": "https://context7.com/ibm/fp-go",
"public_key": "pk_7wJdJRn8zGHxvIYu7eh9h"
}

318
skills/fp-go-http/SKILL.md Normal file
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).

231
v2/AGENTS.md Normal file
View File

@@ -0,0 +1,231 @@
# Agent Guidelines for fp-go/v2
This document provides guidelines for AI agents working on the fp-go/v2 project.
## Documentation Standards
### Go Doc Comments
1. **Use Standard Go Doc Format**
- Do NOT use markdown-style links like `[text](url)`
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
- Go's documentation system will automatically create links
2. **Structure**
```go
// FunctionName does something useful.
//
// Longer description explaining the purpose and behavior.
//
// # Type Parameters
//
// - T: Description of type parameter
//
// # Parameters
//
// - param: Description of parameter
//
// # Returns
//
// - ReturnType: Description of return value
//
// # Example Usage
//
// code example here
//
// # See Also
//
// - RelatedFunction: Brief description
func FunctionName[T any](param T) ReturnType {
```
3. **Code Examples**
- Use idiomatic Go patterns
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
- Show realistic, runnable examples
### File Headers
Always include the Apache 2.0 license header:
```go
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
```
## Testing Standards
### Test Structure
1. **Organize Tests by Category**
```go
func TestFunctionName_Success(t *testing.T) {
t.Run("specific success case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_Failure(t *testing.T) {
t.Run("specific failure case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_EdgeCases(t *testing.T) {
// edge case tests
}
func TestFunctionName_Integration(t *testing.T) {
// integration tests
}
```
2. **Use Direct Assertions**
- Prefer: `assert.Equal(t, validation.Success(expected), actual)`
- Avoid: Verbose `either.MonadFold` patterns unless necessary
- Exception: When you need to verify pointer is not nil or extract specific fields
3. **Use Idiomatic Patterns**
- Use `result.Eitherize1` for converting `(T, error)` functions
- Use `result.Of` for success values
- Use `result.Left` for error values
### Test Coverage
Include tests for:
- **Success cases**: Normal operation with various input types
- **Failure cases**: Error handling and error preservation
- **Edge cases**: Nil, empty, zero values, boundary conditions
- **Integration**: Composition with other functions
- **Type safety**: Verify type parameters work correctly
- **Benchmarks**: Performance-critical paths
### Example Test Pattern
```go
func TestFromReaderResult_Success(t *testing.T) {
t.Run("converts successful ReaderResult", func(t *testing.T) {
// Arrange
parseIntRR := result.Eitherize1(strconv.Atoi)
validator := FromReaderResult[string, int](parseIntRR)
// Act
result := validator("42")(nil)
// Assert
assert.Equal(t, validation.Success(42), result)
})
}
```
## Code Style
### Functional Patterns
1. **Prefer Composition**
```go
validator := F.Pipe1(
FromReaderResult[string, int](parseIntRR),
Chain(validatePositive),
)
```
2. **Use Type-Safe Helpers**
- `result.Eitherize1` for `func(T) (R, error)`
- `result.Of` for wrapping success values
- `result.Left` for wrapping errors
3. **Avoid Verbose Patterns**
- Don't manually handle `(value, error)` tuples when helpers exist
- Don't use `either.MonadFold` in tests unless necessary
4. **Use Void Type for Unit Values**
- Use `function.Void` (or `F.Void`) instead of `struct{}`
- Use `function.VOID` (or `F.VOID`) instead of `struct{}{}`
- Example: `Empty[F.Void, F.Void, any](lazy.Of(pair.MakePair(F.VOID, F.VOID)))`
### Error Handling
1. **In Production Code**
- Use `validation.Success` for successful validations
- Use `validation.FailureWithMessage` for simple failures
- Use `validation.FailureWithError` to preserve error causes
2. **In Tests**
- Verify error messages and causes
- Check error context is preserved
- Test error accumulation when applicable
## Common Patterns
### Converting Error-Based Functions
```go
// Good: Use Eitherize1
parseIntRR := result.Eitherize1(strconv.Atoi)
// Avoid: Manual error handling
parseIntRR := func(input string) result.Result[int] {
val, err := strconv.Atoi(input)
if err != nil {
return result.Left[int](err)
}
return result.Of(val)
}
```
### Testing Validation Results
```go
// Good: Direct comparison
assert.Equal(t, validation.Success(42), result)
// Avoid: Verbose extraction (unless you need to verify specific fields)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
```
### Documentation Examples
```go
// Good: Concise and idiomatic
// parseIntRR := result.Eitherize1(strconv.Atoi)
// validator := FromReaderResult[string, int](parseIntRR)
// Avoid: Verbose manual patterns
// parseIntRR := func(input string) result.Result[int] {
// val, err := strconv.Atoi(input)
// if err != nil {
// return result.Left[int](err)
// }
// return result.Of(val)
// }
```
## Checklist for New Code
- [ ] Apache 2.0 license header included
- [ ] Go doc comments use standard format (no markdown links)
- [ ] Code examples are idiomatic and concise
- [ ] Tests cover success, failure, edge cases, and integration
- [ ] Tests use direct assertions where possible
- [ ] Benchmarks included for performance-critical code
- [ ] All tests pass
- [ ] Code uses functional composition patterns
- [ ] Error handling preserves context and causes

View File

@@ -1,6 +1,6 @@
# Functional I/O in Go: Context, Errors, and the Reader Pattern
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult` and `idiomatic/context/readerresult` packages.
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult`, `idiomatic/context/readerresult`, and `effect` packages.
## Table of Contents
@@ -10,6 +10,7 @@ This document explores how functional programming principles apply to I/O operat
- [Benefits of the Functional Approach](#benefits-of-the-functional-approach)
- [Side-by-Side Comparison](#side-by-side-comparison)
- [Advanced Patterns](#advanced-patterns)
- [The Effect Package: Higher-Level Abstraction](#the-effect-package-higher-level-abstraction)
- [When to Use Each Approach](#when-to-use-each-approach)
## Why Context in I/O Operations
@@ -775,6 +776,191 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
}
```
## The Effect Package: Higher-Level Abstraction
### What is Effect?
The `effect` package provides a higher-level abstraction over `ReaderReaderIOResult`, offering a complete effect system for managing dependencies, errors, and side effects in a composable way. It's inspired by [effect-ts](https://effect.website/) and provides a cleaner API for complex workflows.
### Core Type
```go
// Effect represents an effectful computation that:
// - Requires a context of type C (dependency injection)
// - Can perform I/O operations
// - Can fail with an error
// - Produces a value of type A on success
type Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
```
**Key difference from ReaderIOResult**: Effect adds an additional layer of dependency injection (the `C` type parameter) on top of the runtime `context.Context`, enabling type-safe dependency management.
### When to Use Effect
Use the Effect package when you need:
1. **Type-Safe Dependency Injection**: Your application has typed dependencies (config, services, repositories) that need to be threaded through operations
2. **Complex Workflows**: Multiple services and dependencies need to be composed
3. **Testability**: You want to easily mock dependencies by providing different contexts
4. **Separation of Concerns**: Clear separation between business logic, dependencies, and I/O
### Effect vs ReaderIOResult
```go
// ReaderIOResult - depends only on runtime context
type ReaderIOResult[A any] = func(context.Context) (A, error)
// Effect - depends on typed context C AND runtime context
type Effect[C, A any] = func(C) func(context.Context) (A, error)
```
**ReaderIOResult** is simpler and suitable when you only need runtime context (cancellation, deadlines, request-scoped values).
**Effect** adds typed dependency injection, making it ideal for applications with complex service dependencies.
### Basic Usage
#### Creating Effects
```go
type AppConfig struct {
DatabaseURL string
APIKey string
}
// Create a successful effect
successEffect := effect.Succeed[AppConfig, string]("hello")
// Create a failed effect
failEffect := effect.Fail[AppConfig, string](errors.New("failed"))
// Lift a pure value
pureEffect := effect.Of[AppConfig, int](42)
```
#### Integrating Standard Go Functions
The `Eitherize` function makes it easy to integrate standard Go functions that return `(value, error)`:
```go
type Database struct {
conn *sql.DB
}
// Convert a standard Go function to an Effect using Eitherize
func fetchUser(id int) effect.Effect[Database, User] {
return effect.Eitherize(func(db Database, ctx context.Context) (User, error) {
var user User
err := db.conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).Scan(&user)
return user, err
})
}
// Use Eitherize1 for Kleisli arrows (functions with an additional parameter)
fetchUserKleisli := effect.Eitherize1(func(db Database, ctx context.Context, id int) (User, error) {
var user User
err := db.conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).Scan(&user)
return user, err
})
// fetchUserKleisli has type: func(int) Effect[Database, User]
```
#### Composing Effects
```go
type Services struct {
UserRepo UserRepository
EmailSvc EmailService
}
// Compose multiple effects with typed dependencies
func processUser(id int, newEmail string) effect.Effect[Services, User] {
return F.Pipe3(
// Fetch user from repository
effect.Eitherize(func(svc Services, ctx context.Context) (User, error) {
return svc.UserRepo.GetUser(ctx, id)
}),
// Validate user (pure function lifted into Effect)
effect.ChainEitherK[Services](validateUser),
// Update email
effect.Chain[Services](func(user User) effect.Effect[Services, User] {
return effect.Eitherize(func(svc Services, ctx context.Context) (User, error) {
if err := svc.EmailSvc.SendVerification(ctx, newEmail); err != nil {
return User{}, err
}
return svc.UserRepo.UpdateEmail(ctx, user.ID, newEmail)
})
}),
)
}
```
#### Running Effects
```go
func main() {
// Set up typed dependencies once
services := Services{
UserRepo: &PostgresUserRepo{db: db},
EmailSvc: &SMTPEmailService{host: "smtp.example.com"},
}
// Build the effect pipeline (no execution yet)
userEffect := processUser(42, "new@email.com")
// Provide dependencies - returns a Thunk (ReaderIOResult)
thunk := effect.Provide(services)(userEffect)
// Run synchronously - returns a func(context.Context) (User, error)
readerResult := effect.RunSync(thunk)
// Execute with runtime context
user, err := readerResult(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Updated user: %+v\n", user)
}
```
### Comparison: Traditional vs ReaderIOResult vs Effect
| Aspect | Traditional | ReaderIOResult | Effect |
|---|---|---|---|
| Error propagation | Manual | Automatic | Automatic |
| Dependency injection | Function parameters | Closure / `context.Context` | Typed `C` parameter |
| Testability | Requires mocking | Mock `ReaderIOResult` | Provide mock `C` |
| Composability | Low | High | High |
| Type-safe dependencies | No | No | Yes |
| Complexity | Low | Medium | Medium-High |
### Testing with Effect
One of the key benefits of Effect is easy testing through dependency substitution:
```go
func TestProcessUser(t *testing.T) {
// Create mock services
mockServices := Services{
UserRepo: &MockUserRepo{
users: map[int]User{42: {ID: 42, Age: 25, Email: "old@email.com"}},
},
EmailSvc: &MockEmailService{},
}
// Run the effect with mock dependencies
user, err := effect.RunSync(
effect.Provide(mockServices)(processUser(42, "new@email.com")),
)(context.Background())
assert.NoError(t, err)
assert.Equal(t, "new@email.com", user.Email)
}
```
No database or SMTP server needed — just provide mock implementations of the `Services` struct.
## When to Use Each Approach
### Use Traditional Go Style When:
@@ -794,6 +980,17 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
5. **Resource management**: Need guaranteed cleanup (Bracket)
6. **Parallel execution**: Need to parallelize operations easily
7. **Type safety**: Want the type system to track I/O effects
8. **Simple dependencies**: Only need runtime `context.Context`, no typed dependencies
### Use Effect When:
1. **Type-safe dependency injection**: Application has typed dependencies (config, services, repositories)
2. **Complex service architectures**: Multiple services need to be composed with clear dependency management
3. **Testability with mocks**: Want to easily substitute dependencies for testing
4. **Separation of concerns**: Need clear separation between business logic, dependencies, and I/O
5. **Large applications**: Building applications where dependency management is critical
6. **Team experience**: Team is comfortable with functional programming and effect systems
7. **Integration with standard Go**: Need to integrate many standard `(value, error)` functions using `Eitherize`
### Use Idiomatic Functional Style (idiomatic/context/readerresult) When:
@@ -803,6 +1000,7 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
4. **Go integration**: Want seamless integration with existing Go code
5. **Production services**: Building high-throughput services
6. **Best of both worlds**: Want functional composition with Go's native patterns
7. **Simple dependencies**: Only need runtime `context.Context`, no typed dependencies
## Summary
@@ -814,11 +1012,18 @@ The functional approach to I/O in Go offers significant advantages:
4. **Type Safety**: I/O effects visible in the type system
5. **Lazy Evaluation**: Build pipelines, execute when ready
6. **Context Propagation**: Automatic threading of context
7. **Performance**: Idiomatic version offers 2-10x speedup
7. **Dependency Injection**: Type-safe dependency management with Effect
8. **Performance**: Idiomatic version offers 2-10x speedup
The key insight is that **I/O operations return descriptions of effects** (ReaderIOResult) rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
The key insight is that **I/O operations return descriptions of effects** rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
For production Go services, the **idiomatic/context/readerresult** package provides the best balance: full functional programming capabilities with native Go performance and familiar error handling patterns.
### Choosing the Right Abstraction
- **ReaderIOResult**: Best for simple I/O pipelines that only need runtime `context.Context`
- **Effect**: Best for complex applications with typed dependencies and service architectures
- **idiomatic/context/readerresult**: Best for production services needing high performance with functional patterns
For production Go services, the **idiomatic/context/readerresult** package provides the best balance of performance and functional capabilities. For applications with complex dependency management, the **effect** package provides type-safe dependency injection with a clean, composable API.
## Further Reading
@@ -826,4 +1031,6 @@ For production Go services, the **idiomatic/context/readerresult** package provi
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison
- [idiomatic/doc.go](./idiomatic/doc.go) - Idiomatic package overview
- [context/readerioresult](./context/readerioresult/) - ReaderIOResult package
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
- [effect](./effect/) - Effect package for type-safe dependency injection
- [effect-ts](https://effect.website/) - TypeScript effect system that inspired this package

View File

@@ -3,6 +3,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go/v2.svg)](https://pkg.go.dev/github.com/IBM/fp-go/v2)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main&flag=v2)](https://coveralls.io/github/IBM/fp-go?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/IBM/fp-go/v2)](https://goreportcard.com/report/github.com/IBM/fp-go/v2)
[![Context7](https://img.shields.io/badge/context7-docs-blue)](https://context7.com/ibm/fp-go)
**fp-go** is a comprehensive functional programming library for Go, bringing type-safe functional patterns inspired by [fp-ts](https://gcanti.github.io/fp-ts/) to the Go ecosystem. Version 2 leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
@@ -461,7 +462,8 @@ func process() IOResult[string] {
- **Result** - Simplified Either with error as left type (recommended for error handling)
- **IO** - Lazy evaluation and side effect management
- **IOOption** - Combine IO with Option for optional values with side effects
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither when using standard `error` type)
- **Effect** - Composable effects with dependency injection and error handling
- **Reader** - Dependency injection pattern
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects

522
v2/array/array_nil_test.go Normal file
View File

@@ -0,0 +1,522 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
"fmt"
"testing"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestNilSlice_IsEmpty verifies that IsEmpty handles nil slices correctly
func TestNilSlice_IsEmpty(t *testing.T) {
var nilSlice []int
assert.True(t, IsEmpty(nilSlice), "nil slice should be empty")
}
// TestNilSlice_IsNonEmpty verifies that IsNonEmpty handles nil slices correctly
func TestNilSlice_IsNonEmpty(t *testing.T) {
var nilSlice []int
assert.False(t, IsNonEmpty(nilSlice), "nil slice should not be non-empty")
}
// TestNilSlice_MonadMap verifies that MonadMap handles nil slices correctly
func TestNilSlice_MonadMap(t *testing.T) {
var nilSlice []int
result := MonadMap(nilSlice, func(v int) string {
return fmt.Sprintf("%d", v)
})
assert.NotNil(t, result, "MonadMap should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadMap should return empty slice for nil input")
}
// TestNilSlice_MonadMapRef verifies that MonadMapRef handles nil slices correctly
func TestNilSlice_MonadMapRef(t *testing.T) {
var nilSlice []int
result := MonadMapRef(nilSlice, func(v *int) string {
return fmt.Sprintf("%d", *v)
})
assert.NotNil(t, result, "MonadMapRef should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadMapRef should return empty slice for nil input")
}
// TestNilSlice_Map verifies that Map handles nil slices correctly
func TestNilSlice_Map(t *testing.T) {
var nilSlice []int
mapper := Map(func(v int) string {
return fmt.Sprintf("%d", v)
})
result := mapper(nilSlice)
assert.NotNil(t, result, "Map should return non-nil slice")
assert.Equal(t, 0, len(result), "Map should return empty slice for nil input")
}
// TestNilSlice_MapRef verifies that MapRef handles nil slices correctly
func TestNilSlice_MapRef(t *testing.T) {
var nilSlice []int
mapper := MapRef(func(v *int) string {
return fmt.Sprintf("%d", *v)
})
result := mapper(nilSlice)
assert.NotNil(t, result, "MapRef should return non-nil slice")
assert.Equal(t, 0, len(result), "MapRef should return empty slice for nil input")
}
// TestNilSlice_MapWithIndex verifies that MapWithIndex handles nil slices correctly
func TestNilSlice_MapWithIndex(t *testing.T) {
var nilSlice []int
mapper := MapWithIndex(func(i int, v int) string {
return fmt.Sprintf("%d:%d", i, v)
})
result := mapper(nilSlice)
assert.NotNil(t, result, "MapWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "MapWithIndex should return empty slice for nil input")
}
// TestNilSlice_Filter verifies that Filter handles nil slices correctly
func TestNilSlice_Filter(t *testing.T) {
var nilSlice []int
filter := Filter(func(v int) bool {
return v > 0
})
result := filter(nilSlice)
assert.NotNil(t, result, "Filter should return non-nil slice")
assert.Equal(t, 0, len(result), "Filter should return empty slice for nil input")
}
// TestNilSlice_FilterWithIndex verifies that FilterWithIndex handles nil slices correctly
func TestNilSlice_FilterWithIndex(t *testing.T) {
var nilSlice []int
filter := FilterWithIndex(func(i int, v int) bool {
return v > 0
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterWithIndex should return empty slice for nil input")
}
// TestNilSlice_FilterRef verifies that FilterRef handles nil slices correctly
func TestNilSlice_FilterRef(t *testing.T) {
var nilSlice []int
filter := FilterRef(func(v *int) bool {
return *v > 0
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterRef should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterRef should return empty slice for nil input")
}
// TestNilSlice_MonadFilterMap verifies that MonadFilterMap handles nil slices correctly
func TestNilSlice_MonadFilterMap(t *testing.T) {
var nilSlice []int
result := MonadFilterMap(nilSlice, func(v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d", v))
})
assert.NotNil(t, result, "MonadFilterMap should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadFilterMap should return empty slice for nil input")
}
// TestNilSlice_MonadFilterMapWithIndex verifies that MonadFilterMapWithIndex handles nil slices correctly
func TestNilSlice_MonadFilterMapWithIndex(t *testing.T) {
var nilSlice []int
result := MonadFilterMapWithIndex(nilSlice, func(i int, v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d:%d", i, v))
})
assert.NotNil(t, result, "MonadFilterMapWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadFilterMapWithIndex should return empty slice for nil input")
}
// TestNilSlice_FilterMap verifies that FilterMap handles nil slices correctly
func TestNilSlice_FilterMap(t *testing.T) {
var nilSlice []int
filter := FilterMap(func(v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d", v))
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterMap should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterMap should return empty slice for nil input")
}
// TestNilSlice_FilterMapWithIndex verifies that FilterMapWithIndex handles nil slices correctly
func TestNilSlice_FilterMapWithIndex(t *testing.T) {
var nilSlice []int
filter := FilterMapWithIndex(func(i int, v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d:%d", i, v))
})
result := filter(nilSlice)
assert.NotNil(t, result, "FilterMapWithIndex should return non-nil slice")
assert.Equal(t, 0, len(result), "FilterMapWithIndex should return empty slice for nil input")
}
// TestNilSlice_MonadReduce verifies that MonadReduce handles nil slices correctly
func TestNilSlice_MonadReduce(t *testing.T) {
var nilSlice []int
result := MonadReduce(nilSlice, func(acc int, v int) int {
return acc + v
}, 10)
assert.Equal(t, 10, result, "MonadReduce should return initial value for nil slice")
}
// TestNilSlice_MonadReduceWithIndex verifies that MonadReduceWithIndex handles nil slices correctly
func TestNilSlice_MonadReduceWithIndex(t *testing.T) {
var nilSlice []int
result := MonadReduceWithIndex(nilSlice, func(i int, acc int, v int) int {
return acc + v
}, 10)
assert.Equal(t, 10, result, "MonadReduceWithIndex should return initial value for nil slice")
}
// TestNilSlice_Reduce verifies that Reduce handles nil slices correctly
func TestNilSlice_Reduce(t *testing.T) {
var nilSlice []int
reducer := Reduce(func(acc int, v int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "Reduce should return initial value for nil slice")
}
// TestNilSlice_ReduceWithIndex verifies that ReduceWithIndex handles nil slices correctly
func TestNilSlice_ReduceWithIndex(t *testing.T) {
var nilSlice []int
reducer := ReduceWithIndex(func(i int, acc int, v int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceWithIndex should return initial value for nil slice")
}
// TestNilSlice_ReduceRight verifies that ReduceRight handles nil slices correctly
func TestNilSlice_ReduceRight(t *testing.T) {
var nilSlice []int
reducer := ReduceRight(func(v int, acc int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceRight should return initial value for nil slice")
}
// TestNilSlice_ReduceRightWithIndex verifies that ReduceRightWithIndex handles nil slices correctly
func TestNilSlice_ReduceRightWithIndex(t *testing.T) {
var nilSlice []int
reducer := ReduceRightWithIndex(func(i int, v int, acc int) int {
return acc + v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceRightWithIndex should return initial value for nil slice")
}
// TestNilSlice_ReduceRef verifies that ReduceRef handles nil slices correctly
func TestNilSlice_ReduceRef(t *testing.T) {
var nilSlice []int
reducer := ReduceRef(func(acc int, v *int) int {
return acc + *v
}, 10)
result := reducer(nilSlice)
assert.Equal(t, 10, result, "ReduceRef should return initial value for nil slice")
}
// TestNilSlice_Append verifies that Append handles nil slices correctly
func TestNilSlice_Append(t *testing.T) {
var nilSlice []int
result := Append(nilSlice, 42)
assert.NotNil(t, result, "Append should return non-nil slice")
assert.Equal(t, 1, len(result), "Append should create slice with one element")
assert.Equal(t, 42, result[0], "Append should add element correctly")
}
// TestNilSlice_MonadChain verifies that MonadChain handles nil slices correctly
func TestNilSlice_MonadChain(t *testing.T) {
var nilSlice []int
result := MonadChain(nilSlice, func(v int) []string {
return []string{fmt.Sprintf("%d", v)}
})
assert.NotNil(t, result, "MonadChain should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadChain should return empty slice for nil input")
}
// TestNilSlice_Chain verifies that Chain handles nil slices correctly
func TestNilSlice_Chain(t *testing.T) {
var nilSlice []int
chain := Chain(func(v int) []string {
return []string{fmt.Sprintf("%d", v)}
})
result := chain(nilSlice)
assert.NotNil(t, result, "Chain should return non-nil slice")
assert.Equal(t, 0, len(result), "Chain should return empty slice for nil input")
}
// TestNilSlice_MonadAp verifies that MonadAp handles nil slices correctly
func TestNilSlice_MonadAp(t *testing.T) {
var nilFuncs []func(int) string
var nilValues []int
// nil functions, nil values
result1 := MonadAp(nilFuncs, nilValues)
assert.NotNil(t, result1, "MonadAp should return non-nil slice")
assert.Equal(t, 0, len(result1), "MonadAp should return empty slice for nil inputs")
// nil functions, non-nil values
nonNilValues := []int{1, 2, 3}
result2 := MonadAp(nilFuncs, nonNilValues)
assert.NotNil(t, result2, "MonadAp should return non-nil slice")
assert.Equal(t, 0, len(result2), "MonadAp should return empty slice when functions are nil")
// non-nil functions, nil values
nonNilFuncs := []func(int) string{func(v int) string { return fmt.Sprintf("%d", v) }}
result3 := MonadAp(nonNilFuncs, nilValues)
assert.NotNil(t, result3, "MonadAp should return non-nil slice")
assert.Equal(t, 0, len(result3), "MonadAp should return empty slice when values are nil")
}
// TestNilSlice_Ap verifies that Ap handles nil slices correctly
func TestNilSlice_Ap(t *testing.T) {
var nilValues []int
ap := Ap[string](nilValues)
var nilFuncs []func(int) string
result := ap(nilFuncs)
assert.NotNil(t, result, "Ap should return non-nil slice")
assert.Equal(t, 0, len(result), "Ap should return empty slice for nil inputs")
}
// TestNilSlice_Head verifies that Head handles nil slices correctly
func TestNilSlice_Head(t *testing.T) {
var nilSlice []int
result := Head(nilSlice)
assert.True(t, O.IsNone(result), "Head should return None for nil slice")
}
// TestNilSlice_First verifies that First handles nil slices correctly
func TestNilSlice_First(t *testing.T) {
var nilSlice []int
result := First(nilSlice)
assert.True(t, O.IsNone(result), "First should return None for nil slice")
}
// TestNilSlice_Last verifies that Last handles nil slices correctly
func TestNilSlice_Last(t *testing.T) {
var nilSlice []int
result := Last(nilSlice)
assert.True(t, O.IsNone(result), "Last should return None for nil slice")
}
// TestNilSlice_Tail verifies that Tail handles nil slices correctly
func TestNilSlice_Tail(t *testing.T) {
var nilSlice []int
result := Tail(nilSlice)
assert.True(t, O.IsNone(result), "Tail should return None for nil slice")
}
// TestNilSlice_Flatten verifies that Flatten handles nil slices correctly
func TestNilSlice_Flatten(t *testing.T) {
var nilSlice [][]int
result := Flatten(nilSlice)
assert.NotNil(t, result, "Flatten should return non-nil slice")
assert.Equal(t, 0, len(result), "Flatten should return empty slice for nil input")
}
// TestNilSlice_Lookup verifies that Lookup handles nil slices correctly
func TestNilSlice_Lookup(t *testing.T) {
var nilSlice []int
lookup := Lookup[int](0)
result := lookup(nilSlice)
assert.True(t, O.IsNone(result), "Lookup should return None for nil slice")
}
// TestNilSlice_Size verifies that Size handles nil slices correctly
func TestNilSlice_Size(t *testing.T) {
var nilSlice []int
result := Size(nilSlice)
assert.Equal(t, 0, result, "Size should return 0 for nil slice")
}
// TestNilSlice_MonadPartition verifies that MonadPartition handles nil slices correctly
func TestNilSlice_MonadPartition(t *testing.T) {
var nilSlice []int
result := MonadPartition(nilSlice, func(v int) bool {
return v > 0
})
left := P.Head(result)
right := P.Tail(result)
assert.NotNil(t, left, "MonadPartition left should return non-nil slice")
assert.NotNil(t, right, "MonadPartition right should return non-nil slice")
assert.Equal(t, 0, len(left), "MonadPartition left should be empty for nil input")
assert.Equal(t, 0, len(right), "MonadPartition right should be empty for nil input")
}
// TestNilSlice_Partition verifies that Partition handles nil slices correctly
func TestNilSlice_Partition(t *testing.T) {
var nilSlice []int
partition := Partition(func(v int) bool {
return v > 0
})
result := partition(nilSlice)
left := P.Head(result)
right := P.Tail(result)
assert.NotNil(t, left, "Partition left should return non-nil slice")
assert.NotNil(t, right, "Partition right should return non-nil slice")
assert.Equal(t, 0, len(left), "Partition left should be empty for nil input")
assert.Equal(t, 0, len(right), "Partition right should be empty for nil input")
}
// TestNilSlice_IsNil verifies that IsNil handles nil slices correctly
func TestNilSlice_IsNil(t *testing.T) {
var nilSlice []int
assert.True(t, IsNil(nilSlice), "IsNil should return true for nil slice")
nonNilSlice := []int{}
assert.False(t, IsNil(nonNilSlice), "IsNil should return false for non-nil empty slice")
}
// TestNilSlice_IsNonNil verifies that IsNonNil handles nil slices correctly
func TestNilSlice_IsNonNil(t *testing.T) {
var nilSlice []int
assert.False(t, IsNonNil(nilSlice), "IsNonNil should return false for nil slice")
nonNilSlice := []int{}
assert.True(t, IsNonNil(nonNilSlice), "IsNonNil should return true for non-nil empty slice")
}
// TestNilSlice_Copy verifies that Copy handles nil slices correctly
func TestNilSlice_Copy(t *testing.T) {
var nilSlice []int
result := Copy(nilSlice)
assert.NotNil(t, result, "Copy should return non-nil slice")
assert.Equal(t, 0, len(result), "Copy should return empty slice for nil input")
}
// TestNilSlice_FoldMap verifies that FoldMap handles nil slices correctly
func TestNilSlice_FoldMap(t *testing.T) {
var nilSlice []int
monoid := S.Monoid
foldMap := FoldMap[int](monoid)(func(v int) string {
return fmt.Sprintf("%d", v)
})
result := foldMap(nilSlice)
assert.Equal(t, "", result, "FoldMap should return empty value for nil slice")
}
// TestNilSlice_FoldMapWithIndex verifies that FoldMapWithIndex handles nil slices correctly
func TestNilSlice_FoldMapWithIndex(t *testing.T) {
var nilSlice []int
monoid := S.Monoid
foldMap := FoldMapWithIndex[int](monoid)(func(i int, v int) string {
return fmt.Sprintf("%d:%d", i, v)
})
result := foldMap(nilSlice)
assert.Equal(t, "", result, "FoldMapWithIndex should return empty value for nil slice")
}
// TestNilSlice_Fold verifies that Fold handles nil slices correctly
func TestNilSlice_Fold(t *testing.T) {
var nilSlice []string
monoid := S.Monoid
fold := Fold[string](monoid)
result := fold(nilSlice)
assert.Equal(t, "", result, "Fold should return empty value for nil slice")
}
// TestNilSlice_Concat verifies that Concat handles nil slices correctly
func TestNilSlice_Concat(t *testing.T) {
var nilSlice []int
nonNilSlice := []int{1, 2, 3}
// nil concat non-nil
concat1 := Concat(nonNilSlice)
result1 := concat1(nilSlice)
assert.Equal(t, nonNilSlice, result1, "nil concat non-nil should return non-nil slice")
// non-nil concat nil
concat2 := Concat(nilSlice)
result2 := concat2(nonNilSlice)
assert.Equal(t, nonNilSlice, result2, "non-nil concat nil should return non-nil slice")
// nil concat nil
concat3 := Concat(nilSlice)
result3 := concat3(nilSlice)
assert.Nil(t, result3, "nil concat nil should return nil")
}
// TestNilSlice_MonadFlap verifies that MonadFlap handles nil slices correctly
func TestNilSlice_MonadFlap(t *testing.T) {
var nilSlice []func(int) string
result := MonadFlap(nilSlice, 42)
assert.NotNil(t, result, "MonadFlap should return non-nil slice")
assert.Equal(t, 0, len(result), "MonadFlap should return empty slice for nil input")
}
// TestNilSlice_Flap verifies that Flap handles nil slices correctly
func TestNilSlice_Flap(t *testing.T) {
var nilSlice []func(int) string
flap := Flap[string, int](42)
result := flap(nilSlice)
assert.NotNil(t, result, "Flap should return non-nil slice")
assert.Equal(t, 0, len(result), "Flap should return empty slice for nil input")
}
// TestNilSlice_Reverse verifies that Reverse handles nil slices correctly
func TestNilSlice_Reverse(t *testing.T) {
var nilSlice []int
result := Reverse(nilSlice)
assert.Nil(t, result, "Reverse should return nil for nil slice")
}
// TestNilSlice_Extend verifies that Extend handles nil slices correctly
func TestNilSlice_Extend(t *testing.T) {
var nilSlice []int
extend := Extend(func(as []int) string {
return fmt.Sprintf("%v", as)
})
result := extend(nilSlice)
assert.NotNil(t, result, "Extend should return non-nil slice")
assert.Equal(t, 0, len(result), "Extend should return empty slice for nil input")
}
// TestNilSlice_Empty verifies that Empty creates an empty non-nil slice
func TestNilSlice_Empty(t *testing.T) {
result := Empty[int]()
assert.NotNil(t, result, "Empty should return non-nil slice")
assert.Equal(t, 0, len(result), "Empty should return empty slice")
assert.False(t, IsNil(result), "Empty should not return nil slice")
}
// TestNilSlice_Zero verifies that Zero creates an empty non-nil slice
func TestNilSlice_Zero(t *testing.T) {
result := Zero[int]()
assert.NotNil(t, result, "Zero should return non-nil slice")
assert.Equal(t, 0, len(result), "Zero should return empty slice")
assert.False(t, IsNil(result), "Zero should not return nil slice")
}
// TestNilSlice_ConstNil verifies that ConstNil returns a nil slice
func TestNilSlice_ConstNil(t *testing.T) {
result := ConstNil[int]()
assert.Nil(t, result, "ConstNil should return nil slice")
assert.True(t, IsNil(result), "ConstNil should return nil slice")
}
// TestNilSlice_Of verifies that Of creates a proper singleton slice
func TestNilSlice_Of(t *testing.T) {
result := Of(42)
assert.NotNil(t, result, "Of should return non-nil slice")
assert.Equal(t, 1, len(result), "Of should create slice with one element")
assert.Equal(t, 42, result[0], "Of should set value correctly")
}

View File

@@ -29,7 +29,7 @@ func TestFromReaderIOResult(t *testing.T) {
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Return a Reader that always passes
return result.Of[Reader](func(t *testing.T) bool {
return result.Of(func(t *testing.T) bool {
return true
})
}
@@ -46,7 +46,7 @@ func TestFromReaderIOResult(t *testing.T) {
// Create a ReaderIOResult that returns a successful Equal assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(42))
return result.Of(Equal(42)(42))
}
}
@@ -80,7 +80,7 @@ func TestFromReaderIOResult(t *testing.T) {
// Create a ReaderIOResult that returns a failing assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(43))
return result.Of(Equal(42)(43))
}
}
@@ -100,7 +100,7 @@ func TestFromReaderIOResult(t *testing.T) {
contextUsed = true
}
return func() result.Result[Reader] {
return result.Of[Reader](func(t *testing.T) bool {
return result.Of(func(t *testing.T) bool {
return true
})
}
@@ -118,7 +118,7 @@ func TestFromReaderIOResult(t *testing.T) {
// Create a ReaderIOResult that returns NoError assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](NoError(nil))
return result.Of(NoError(nil))
}
}
@@ -139,7 +139,7 @@ func TestFromReaderIOResult(t *testing.T) {
ArrayLength[int](3)(arr),
ArrayContains(2)(arr),
})
return result.Of[Reader](assertions)
return result.Of(assertions)
}
}
@@ -297,7 +297,7 @@ func TestFromReaderIO(t *testing.T) {
// Create a ReaderIO with Result assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
successResult := result.Of[int](42)
successResult := result.Of(42)
return Success(successResult)
}
}
@@ -338,7 +338,7 @@ func TestFromReaderIOResultIntegration(t *testing.T) {
}
// Return a successful assertion
return result.Of[Reader](Equal("test")("test"))
return result.Of(Equal("test")("test"))
}
}

273
v2/cli/README.md Normal file
View File

@@ -0,0 +1,273 @@
# CLI Package - Functional Wrappers for urfave/cli/v3
This package provides functional programming wrappers for the `github.com/urfave/cli/v3` library, enabling Effect-based command actions and type-safe flag handling through Prisms.
## Features
### 1. Effect-Based Command Actions
Transform CLI command actions into composable Effects that follow functional programming principles.
#### Key Functions
- **`ToAction(effect CommandEffect) func(context.Context, *C.Command) error`**
- Converts a CommandEffect into a standard urfave/cli Action function
- Enables Effect-based command handlers to work with cli/v3 framework
- **`FromAction(action func(context.Context, *C.Command) error) CommandEffect`**
- Lifts existing cli/v3 action handlers into the Effect type
- Allows gradual migration to functional style
- **`MakeCommand(name, usage string, flags []C.Flag, effect CommandEffect) *C.Command`**
- Creates a new Command with an Effect-based action
- Convenience function combining command creation with Effect conversion
- **`MakeCommandWithSubcommands(...) *C.Command`**
- Creates a Command with subcommands and an Effect-based action
#### Example Usage
```go
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Define an Effect-based command action
processEffect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process input...
return R.Of(F.Void{})
}
}
}
// Create command with Effect
command := cli.MakeCommand(
"process",
"Process input files",
[]C.Flag{
&C.StringFlag{Name: "input", Usage: "Input file path"},
},
processEffect,
)
// Or convert existing action to Effect
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Existing logic...
return nil
}
effect := cli.FromAction(existingAction)
```
### 2. Flag Type Prisms
Type-safe extraction and manipulation of CLI flags using Prisms from the optics package.
#### Available Prisms
- `StringFlagPrism()` - Extract `*C.StringFlag` from `C.Flag`
- `IntFlagPrism()` - Extract `*C.IntFlag` from `C.Flag`
- `BoolFlagPrism()` - Extract `*C.BoolFlag` from `C.Flag`
- `Float64FlagPrism()` - Extract `*C.Float64Flag` from `C.Flag`
- `DurationFlagPrism()` - Extract `*C.DurationFlag` from `C.Flag`
- `TimestampFlagPrism()` - Extract `*C.TimestampFlag` from `C.Flag`
- `StringSliceFlagPrism()` - Extract `*C.StringSliceFlag` from `C.Flag`
- `IntSliceFlagPrism()` - Extract `*C.IntSliceFlag` from `C.Flag`
- `Float64SliceFlagPrism()` - Extract `*C.Float64SliceFlag` from `C.Flag`
- `UintFlagPrism()` - Extract `*C.UintFlag` from `C.Flag`
- `Uint64FlagPrism()` - Extract `*C.Uint64Flag` from `C.Flag`
- `Int64FlagPrism()` - Extract `*C.Int64Flag` from `C.Flag`
#### Example Usage
```go
import (
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Extract a StringFlag from a Flag interface
var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
prism := cli.StringFlagPrism()
// Safe extraction returns Option
result := prism.GetOption(flag)
if O.IsSome(result) {
strFlag := O.MonadFold(result,
func() *C.StringFlag { return nil },
func(f *C.StringFlag) *C.StringFlag { return f },
)
// Use strFlag...
}
// Type mismatch returns None
var intFlag C.Flag = &C.IntFlag{Name: "count"}
result = prism.GetOption(intFlag) // Returns None
// Convert back to Flag
strFlag := &C.StringFlag{Name: "output"}
flag = prism.ReverseGet(strFlag)
```
## Type Definitions
### CommandEffect
```go
type CommandEffect = E.Effect[*C.Command, F.Void]
```
A CommandEffect represents a CLI command action as an Effect. It takes a `*C.Command` as context and produces a result wrapped in the Effect monad.
The Effect structure is:
```
func(*C.Command) -> func(context.Context) -> func() -> Result[Void]
```
This allows for:
- **Composability**: Effects can be composed using standard functional combinators
- **Testability**: Pure functions are easier to test
- **Error Handling**: Errors are explicitly represented in the Result type
- **Context Management**: Context flows naturally through the Effect
## Benefits
### 1. Functional Composition
Effects can be composed using standard functional programming patterns:
```go
import (
F "github.com/IBM/fp-go/v2/function"
RRIOE "github.com/IBM/fp-go/v2/context/readerreaderioresult"
)
// Compose multiple effects
validateInput := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
processData := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
saveResults := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
// Chain effects together
pipeline := F.Pipe3(
validateInput,
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return processData }),
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return saveResults }),
)
```
### 2. Type Safety
Prisms provide compile-time type safety when working with flags:
```go
// Type-safe flag extraction
flags := []C.Flag{
&C.StringFlag{Name: "input"},
&C.IntFlag{Name: "count"},
}
for _, flag := range flags {
// Safe extraction with pattern matching
O.MonadFold(
cli.StringFlagPrism().GetOption(flag),
func() { /* Not a string flag */ },
func(sf *C.StringFlag) { /* Handle string flag */ },
)
}
```
### 3. Error Handling
Errors are explicitly represented in the Result type:
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
if err := validateInput(cmd); err != nil {
return R.Left[F.Void](err) // Explicit error
}
return R.Of(F.Void{}) // Success
}
}
}
```
### 4. Testability
Pure functions are easier to test:
```go
func TestCommandEffect(t *testing.T) {
cmd := &C.Command{Name: "test"}
effect := myCommandEffect(cmd)
// Execute effect
result := effect(context.Background())()
// Assert on result
assert.True(t, R.IsRight(result))
}
```
## Migration Guide
### From Standard Actions to Effects
**Before:**
```go
command := &C.Command{
Name: "process",
Action: func(ctx context.Context, cmd *C.Command) error {
input := cmd.String("input")
// Process...
return nil
},
}
```
**After:**
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process...
return R.Of(F.Void{})
}
}
}
command := cli.MakeCommand("process", "Process files", flags, effect)
```
### Gradual Migration
You can mix both styles during migration:
```go
// Wrap existing action
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Legacy code...
return nil
}
// Use as Effect
effect := cli.FromAction(existingAction)
command := cli.MakeCommand("legacy", "Legacy command", flags, effect)
```
## See Also
- [Effect Package](../effect/) - Core Effect type definitions
- [Optics Package](../optics/) - Prism and other optics
- [urfave/cli/v3](https://github.com/urfave/cli) - Underlying CLI framework

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -388,8 +387,8 @@ func generateApplyHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -266,8 +265,8 @@ func generateBindHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -189,8 +188,8 @@ func generateDIHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

199
v2/cli/effect.go Normal file
View File

@@ -0,0 +1,199 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
R "github.com/IBM/fp-go/v2/result"
C "github.com/urfave/cli/v3"
)
// CommandEffect represents a CLI command action as an Effect.
// The Effect takes a *C.Command as context and produces a result.
type CommandEffect = E.Effect[*C.Command, F.Void]
// ToAction converts a CommandEffect into a standard urfave/cli Action function.
// This allows Effect-based command handlers to be used with the cli/v3 framework.
//
// The conversion process:
// 1. Takes the Effect which expects a *C.Command context
// 2. Executes it with the provided command
// 3. Runs the resulting IO operation
// 4. Converts the Result to either nil (success) or error (failure)
//
// # Parameters
//
// - effect: The CommandEffect to convert
//
// # Returns
//
// - A function compatible with C.Command.Action signature
//
// # Example Usage
//
// effect := func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// // Command logic here
// return R.Of(F.Void{})
// }
// }
// }
// action := ToAction(effect)
// command := &C.Command{
// Name: "example",
// Action: action,
// }
func ToAction(effect CommandEffect) func(context.Context, *C.Command) error {
return func(ctx context.Context, cmd *C.Command) error {
// Execute the effect: cmd -> ctx -> IO -> Result
return F.Pipe3(
ctx,
effect(cmd),
io.Run,
// Convert Result[Void] to error
ET.Fold(F.Identity[error], F.Constant1[F.Void, error](nil)),
)
}
}
// FromAction converts a standard urfave/cli Action function into a CommandEffect.
// This allows existing cli/v3 action handlers to be lifted into the Effect type.
//
// The conversion process:
// 1. Takes a standard action function (context.Context, *C.Command) -> error
// 2. Wraps it in the Effect structure
// 3. Converts the error result to a Result type
//
// # Parameters
//
// - action: The standard cli/v3 action function to convert
//
// # Returns
//
// - A CommandEffect that wraps the original action
//
// # Example Usage
//
// standardAction := func(ctx context.Context, cmd *C.Command) error {
// // Existing command logic
// return nil
// }
// effect := FromAction(standardAction)
// // Now can be composed with other Effects
func FromAction(action func(context.Context, *C.Command) error) CommandEffect {
return func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
err := action(ctx, cmd)
if err != nil {
return R.Left[F.Void](err)
}
return R.Of(F.Void{})
}
}
}
}
// MakeCommand creates a new Command with an Effect-based action.
// This is a convenience function that combines command creation with Effect conversion.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommand(
// "process",
// "Process data files",
// []C.Flag{
// &C.StringFlag{Name: "input", Usage: "Input file"},
// },
// func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// input := cmd.String("input")
// // Process input...
// return R.Of(F.Void{})
// }
// }
// },
// )
func MakeCommand(
name string,
usage string,
flags []C.Flag,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Action: ToAction(effect),
}
}
// MakeCommandWithSubcommands creates a new Command with subcommands and an Effect-based action.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - commands: The subcommands
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with subcommands and the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommandWithSubcommands(
// "app",
// "Application commands",
// []C.Flag{},
// []*C.Command{subCmd1, subCmd2},
// defaultEffect,
// )
func MakeCommandWithSubcommands(
name string,
usage string,
flags []C.Flag,
commands []*C.Command,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Commands: commands,
Action: ToAction(effect),
}
}

204
v2/cli/effect_test.go Normal file
View File

@@ -0,0 +1,204 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"errors"
"testing"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestToAction_Success(t *testing.T) {
t.Run("converts successful Effect to action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
})
}
func TestToAction_Failure(t *testing.T) {
t.Run("converts failed Effect to error", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Left[F.Void](expectedErr)
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestFromAction_Success(t *testing.T) {
t.Run("converts successful action to Effect", func(t *testing.T) {
// Arrange
action := func(ctx context.Context, cmd *C.Command) error {
return nil
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsRight(result))
})
}
func TestFromAction_Failure(t *testing.T) {
t.Run("converts failed action to Effect", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
action := func(ctx context.Context, cmd *C.Command) error {
return expectedErr
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsLeft(result))
err := R.MonadFold(result, F.Identity[error], func(F.Void) error { return nil })
assert.Equal(t, expectedErr, err)
})
}
func TestMakeCommand(t *testing.T) {
t.Run("creates command with Effect-based action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommand(
"test",
"Test command",
[]C.Flag{},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "test", cmd.Name)
assert.Equal(t, "Test command", cmd.Usage)
assert.NotNil(t, cmd.Action)
// Test the action
err := cmd.Action(context.Background(), cmd)
assert.NoError(t, err)
})
}
func TestMakeCommandWithSubcommands(t *testing.T) {
t.Run("creates command with subcommands and Effect-based action", func(t *testing.T) {
// Arrange
subCmd := &C.Command{Name: "sub"}
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommandWithSubcommands(
"parent",
"Parent command",
[]C.Flag{},
[]*C.Command{subCmd},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "parent", cmd.Name)
assert.Equal(t, "Parent command", cmd.Usage)
assert.Len(t, cmd.Commands, 1)
assert.Equal(t, "sub", cmd.Commands[0].Name)
assert.NotNil(t, cmd.Action)
})
}
func TestToAction_Integration(t *testing.T) {
t.Run("Effect can access command flags", func(t *testing.T) {
// Arrange
var capturedValue string
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
capturedValue = cmd.String("input")
return R.Of(F.Void{})
}
}
}
cmd := &C.Command{
Name: "test",
Flags: []C.Flag{
&C.StringFlag{
Name: "input",
Value: "default-value",
},
},
Action: ToAction(effect),
}
// Act
err := cmd.Action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
assert.Equal(t, "default-value", capturedValue)
})
}

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

359
v2/cli/flags.go Normal file
View File

@@ -0,0 +1,359 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
C "github.com/urfave/cli/v3"
)
// StringFlagPrism creates a Prism for extracting a StringFlag from a Flag.
// This provides a type-safe way to work with string flags, handling type
// mismatches gracefully through the Option type.
//
// The prism's GetOption attempts to cast a Flag to *C.StringFlag.
// If the cast succeeds, it returns Some(*C.StringFlag); if it fails, it returns None.
//
// The prism's ReverseGet converts a *C.StringFlag back to a Flag.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringFlag] for safe StringFlag extraction
//
// # Example Usage
//
// prism := StringFlagPrism()
//
// // Extract StringFlag from Flag
// var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
// result := prism.GetOption(flag) // Some(*C.StringFlag{...})
//
// // Type mismatch returns None
// var intFlag C.Flag = &C.IntFlag{Name: "count"}
// result = prism.GetOption(intFlag) // None[*C.StringFlag]()
//
// // Convert back to Flag
// strFlag := &C.StringFlag{Name: "output"}
// flag = prism.ReverseGet(strFlag)
func StringFlagPrism() P.Prism[C.Flag, *C.StringFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringFlag] {
if sf, ok := flag.(*C.StringFlag); ok {
return O.Some(sf)
}
return O.None[*C.StringFlag]()
},
func(f *C.StringFlag) C.Flag { return f },
)
}
// IntFlagPrism creates a Prism for extracting an IntFlag from a Flag.
// This provides a type-safe way to work with integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntFlag] for safe IntFlag extraction
//
// # Example Usage
//
// prism := IntFlagPrism()
//
// // Extract IntFlag from Flag
// var flag C.Flag = &C.IntFlag{Name: "count", Value: 10}
// result := prism.GetOption(flag) // Some(*C.IntFlag{...})
func IntFlagPrism() P.Prism[C.Flag, *C.IntFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntFlag] {
if f, ok := flag.(*C.IntFlag); ok {
return O.Some(f)
}
return O.None[*C.IntFlag]()
},
func(f *C.IntFlag) C.Flag { return f },
)
}
// BoolFlagPrism creates a Prism for extracting a BoolFlag from a Flag.
// This provides a type-safe way to work with boolean flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.BoolFlag] for safe BoolFlag extraction
//
// # Example Usage
//
// prism := BoolFlagPrism()
//
// // Extract BoolFlag from Flag
// var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// result := prism.GetOption(flag) // Some(*C.BoolFlag{...})
func BoolFlagPrism() P.Prism[C.Flag, *C.BoolFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.BoolFlag] {
if f, ok := flag.(*C.BoolFlag); ok {
return O.Some(f)
}
return O.None[*C.BoolFlag]()
},
func(f *C.BoolFlag) C.Flag { return f },
)
}
// Float64FlagPrism creates a Prism for extracting a Float64Flag from a Flag.
// This provides a type-safe way to work with float64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64Flag] for safe Float64Flag extraction
//
// # Example Usage
//
// prism := Float64FlagPrism()
//
// // Extract Float64Flag from Flag
// var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// result := prism.GetOption(flag) // Some(*C.Float64Flag{...})
func Float64FlagPrism() P.Prism[C.Flag, *C.Float64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64Flag] {
if f, ok := flag.(*C.Float64Flag); ok {
return O.Some(f)
}
return O.None[*C.Float64Flag]()
},
func(f *C.Float64Flag) C.Flag { return f },
)
}
// DurationFlagPrism creates a Prism for extracting a DurationFlag from a Flag.
// This provides a type-safe way to work with duration flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.DurationFlag] for safe DurationFlag extraction
//
// # Example Usage
//
// prism := DurationFlagPrism()
//
// // Extract DurationFlag from Flag
// var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: 30 * time.Second}
// result := prism.GetOption(flag) // Some(*C.DurationFlag{...})
func DurationFlagPrism() P.Prism[C.Flag, *C.DurationFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.DurationFlag] {
if f, ok := flag.(*C.DurationFlag); ok {
return O.Some(f)
}
return O.None[*C.DurationFlag]()
},
func(f *C.DurationFlag) C.Flag { return f },
)
}
// TimestampFlagPrism creates a Prism for extracting a TimestampFlag from a Flag.
// This provides a type-safe way to work with timestamp flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.TimestampFlag] for safe TimestampFlag extraction
//
// # Example Usage
//
// prism := TimestampFlagPrism()
//
// // Extract TimestampFlag from Flag
// var flag C.Flag = &C.TimestampFlag{Name: "created"}
// result := prism.GetOption(flag) // Some(*C.TimestampFlag{...})
func TimestampFlagPrism() P.Prism[C.Flag, *C.TimestampFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.TimestampFlag] {
if f, ok := flag.(*C.TimestampFlag); ok {
return O.Some(f)
}
return O.None[*C.TimestampFlag]()
},
func(f *C.TimestampFlag) C.Flag { return f },
)
}
// StringSliceFlagPrism creates a Prism for extracting a StringSliceFlag from a Flag.
// This provides a type-safe way to work with string slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringSliceFlag] for safe StringSliceFlag extraction
//
// # Example Usage
//
// prism := StringSliceFlagPrism()
//
// // Extract StringSliceFlag from Flag
// var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// result := prism.GetOption(flag) // Some(*C.StringSliceFlag{...})
func StringSliceFlagPrism() P.Prism[C.Flag, *C.StringSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringSliceFlag] {
if f, ok := flag.(*C.StringSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.StringSliceFlag]()
},
func(f *C.StringSliceFlag) C.Flag { return f },
)
}
// IntSliceFlagPrism creates a Prism for extracting an IntSliceFlag from a Flag.
// This provides a type-safe way to work with int slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntSliceFlag] for safe IntSliceFlag extraction
//
// # Example Usage
//
// prism := IntSliceFlagPrism()
//
// // Extract IntSliceFlag from Flag
// var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// result := prism.GetOption(flag) // Some(*C.IntSliceFlag{...})
func IntSliceFlagPrism() P.Prism[C.Flag, *C.IntSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntSliceFlag] {
if f, ok := flag.(*C.IntSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.IntSliceFlag]()
},
func(f *C.IntSliceFlag) C.Flag { return f },
)
}
// Float64SliceFlagPrism creates a Prism for extracting a Float64SliceFlag from a Flag.
// This provides a type-safe way to work with float64 slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64SliceFlag] for safe Float64SliceFlag extraction
//
// # Example Usage
//
// prism := Float64SliceFlagPrism()
//
// // Extract Float64SliceFlag from Flag
// var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// result := prism.GetOption(flag) // Some(*C.Float64SliceFlag{...})
func Float64SliceFlagPrism() P.Prism[C.Flag, *C.Float64SliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64SliceFlag] {
if f, ok := flag.(*C.Float64SliceFlag); ok {
return O.Some(f)
}
return O.None[*C.Float64SliceFlag]()
},
func(f *C.Float64SliceFlag) C.Flag { return f },
)
}
// UintFlagPrism creates a Prism for extracting a UintFlag from a Flag.
// This provides a type-safe way to work with unsigned integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.UintFlag] for safe UintFlag extraction
//
// # Example Usage
//
// prism := UintFlagPrism()
//
// // Extract UintFlag from Flag
// var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// result := prism.GetOption(flag) // Some(*C.UintFlag{...})
func UintFlagPrism() P.Prism[C.Flag, *C.UintFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.UintFlag] {
if f, ok := flag.(*C.UintFlag); ok {
return O.Some(f)
}
return O.None[*C.UintFlag]()
},
func(f *C.UintFlag) C.Flag { return f },
)
}
// Uint64FlagPrism creates a Prism for extracting a Uint64Flag from a Flag.
// This provides a type-safe way to work with uint64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Uint64Flag] for safe Uint64Flag extraction
//
// # Example Usage
//
// prism := Uint64FlagPrism()
//
// // Extract Uint64Flag from Flag
// var flag C.Flag = &C.Uint64Flag{Name: "size"}
// result := prism.GetOption(flag) // Some(*C.Uint64Flag{...})
func Uint64FlagPrism() P.Prism[C.Flag, *C.Uint64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Uint64Flag] {
if f, ok := flag.(*C.Uint64Flag); ok {
return O.Some(f)
}
return O.None[*C.Uint64Flag]()
},
func(f *C.Uint64Flag) C.Flag { return f },
)
}
// Int64FlagPrism creates a Prism for extracting an Int64Flag from a Flag.
// This provides a type-safe way to work with int64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Int64Flag] for safe Int64Flag extraction
//
// # Example Usage
//
// prism := Int64FlagPrism()
//
// // Extract Int64Flag from Flag
// var flag C.Flag = &C.Int64Flag{Name: "offset"}
// result := prism.GetOption(flag) // Some(*C.Int64Flag{...})
func Int64FlagPrism() P.Prism[C.Flag, *C.Int64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Int64Flag] {
if f, ok := flag.(*C.Int64Flag); ok {
return O.Some(f)
}
return O.None[*C.Int64Flag]()
},
func(f *C.Int64Flag) C.Flag { return f },
)
}

287
v2/cli/flags_test.go Normal file
View File

@@ -0,0 +1,287 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"testing"
"time"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestStringFlagPrism_Success(t *testing.T) {
t.Run("extracts StringFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.StringFlag{Name: "input", Value: "test"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringFlag { return nil }, func(f *C.StringFlag) *C.StringFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "input", extracted.Name)
assert.Equal(t, "test", extracted.Value)
})
}
func TestStringFlagPrism_Failure(t *testing.T) {
t.Run("returns None for non-StringFlag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsNone(result))
})
}
func TestStringFlagPrism_ReverseGet(t *testing.T) {
t.Run("converts StringFlag back to Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
strFlag := &C.StringFlag{Name: "output", Value: "result"}
// Act
flag := prism.ReverseGet(strFlag)
// Assert
assert.NotNil(t, flag)
assert.IsType(t, &C.StringFlag{}, flag)
})
}
func TestIntFlagPrism_Success(t *testing.T) {
t.Run("extracts IntFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count", Value: 42}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntFlag { return nil }, func(f *C.IntFlag) *C.IntFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "count", extracted.Name)
assert.Equal(t, 42, extracted.Value)
})
}
func TestBoolFlagPrism_Success(t *testing.T) {
t.Run("extracts BoolFlag from Flag", func(t *testing.T) {
// Arrange
prism := BoolFlagPrism()
var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.BoolFlag { return nil }, func(f *C.BoolFlag) *C.BoolFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "verbose", extracted.Name)
assert.Equal(t, true, extracted.Value)
})
}
func TestFloat64FlagPrism_Success(t *testing.T) {
t.Run("extracts Float64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Float64FlagPrism()
var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64Flag { return nil }, func(f *C.Float64Flag) *C.Float64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratio", extracted.Name)
assert.Equal(t, 0.5, extracted.Value)
})
}
func TestDurationFlagPrism_Success(t *testing.T) {
t.Run("extracts DurationFlag from Flag", func(t *testing.T) {
// Arrange
prism := DurationFlagPrism()
duration := 30 * time.Second
var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: duration}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.DurationFlag { return nil }, func(f *C.DurationFlag) *C.DurationFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "timeout", extracted.Name)
assert.Equal(t, duration, extracted.Value)
})
}
func TestTimestampFlagPrism_Success(t *testing.T) {
t.Run("extracts TimestampFlag from Flag", func(t *testing.T) {
// Arrange
prism := TimestampFlagPrism()
var flag C.Flag = &C.TimestampFlag{Name: "created"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.TimestampFlag { return nil }, func(f *C.TimestampFlag) *C.TimestampFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "created", extracted.Name)
})
}
func TestStringSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts StringSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringSliceFlagPrism()
var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringSliceFlag { return nil }, func(f *C.StringSliceFlag) *C.StringSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "tags", extracted.Name)
})
}
func TestIntSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts IntSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntSliceFlagPrism()
var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntSliceFlag { return nil }, func(f *C.IntSliceFlag) *C.IntSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ports", extracted.Name)
})
}
func TestFloat64SliceFlagPrism_Success(t *testing.T) {
t.Run("extracts Float64SliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := Float64SliceFlagPrism()
var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64SliceFlag { return nil }, func(f *C.Float64SliceFlag) *C.Float64SliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratios", extracted.Name)
})
}
func TestUintFlagPrism_Success(t *testing.T) {
t.Run("extracts UintFlag from Flag", func(t *testing.T) {
// Arrange
prism := UintFlagPrism()
var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.UintFlag { return nil }, func(f *C.UintFlag) *C.UintFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "workers", extracted.Name)
assert.Equal(t, uint(4), extracted.Value)
})
}
func TestUint64FlagPrism_Success(t *testing.T) {
t.Run("extracts Uint64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Uint64FlagPrism()
var flag C.Flag = &C.Uint64Flag{Name: "size", Value: 1024}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Uint64Flag { return nil }, func(f *C.Uint64Flag) *C.Uint64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "size", extracted.Name)
assert.Equal(t, uint64(1024), extracted.Value)
})
}
func TestInt64FlagPrism_Success(t *testing.T) {
t.Run("extracts Int64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Int64FlagPrism()
var flag C.Flag = &C.Int64Flag{Name: "offset", Value: -100}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Int64Flag { return nil }, func(f *C.Int64Flag) *C.Int64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "offset", extracted.Name)
assert.Equal(t, int64(-100), extracted.Value)
})
}
func TestPrisms_EdgeCases(t *testing.T) {
t.Run("all prisms return None for wrong type", func(t *testing.T) {
// Arrange
var flag C.Flag = &C.StringFlag{Name: "test"}
// Act & Assert
assert.True(t, O.IsNone(IntFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(BoolFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(DurationFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(TimestampFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(StringSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(IntSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64SliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(UintFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Uint64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Int64FlagPrism().GetOption(flag)))
})
}

View File

@@ -18,7 +18,6 @@ package cli
import (
"fmt"
"os"
"time"
)
func writePackage(f *os.File, pkg string) {
@@ -26,6 +25,6 @@ func writePackage(f *os.File, pkg string) {
fmt.Fprintf(f, "package %s\n\n", pkg)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
}

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -62,8 +61,8 @@ func generateIdentityHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -71,8 +70,8 @@ func generateIOHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -219,8 +218,8 @@ func generateIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -234,8 +233,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -76,8 +75,8 @@ func generateIOOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -378,8 +377,8 @@ func generatePipeHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -118,8 +117,8 @@ func generateReaderHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -131,8 +130,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -233,8 +232,8 @@ func generateReaderIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -246,8 +245,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -22,7 +22,6 @@ import (
"os"
"path/filepath"
"strings"
"time"
C "github.com/urfave/cli/v3"
)
@@ -399,8 +398,8 @@ func generateTupleHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -13,6 +13,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package constant provides the Const functor, a phantom type that ignores its second type parameter.
//
// The Const functor is a fundamental building block in functional programming that wraps a value
// of type E while having a phantom type parameter A. This makes it useful for:
// - Accumulating values during traversals (e.g., collecting metadata)
// - Implementing optics (lenses, prisms) where you need to track information
// - Building applicative functors that combine values using a semigroup
//
// # The Const Functor
//
// Const[E, A] wraps a value of type E and has a phantom type parameter A that doesn't affect
// the runtime value. This allows it to participate in functor and applicative operations while
// maintaining the wrapped value unchanged.
//
// # Key Properties
//
// - Map operations ignore the function and preserve the wrapped value
// - Ap operations combine wrapped values using a semigroup
// - The phantom type A allows type-safe composition with other functors
//
// # Example Usage
//
// // Accumulate string values
// c1 := Make[string, int]("hello")
// c2 := Make[string, int]("world")
//
// // Map doesn't change the wrapped value
// mapped := Map[string, int, string](strconv.Itoa)(c1) // Still contains "hello"
//
// // Ap combines values using a semigroup
// combined := Ap[string, int, int](S.Monoid)(c1)(c2) // Contains "helloworld"
package constant
import (
@@ -21,36 +52,209 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Const is a functor that wraps a value of type E with a phantom type parameter A.
//
// The Const functor is useful for accumulating values during traversals or implementing
// optics. The type parameter A is phantom - it doesn't affect the runtime value but allows
// the type to participate in functor and applicative operations.
//
// Type Parameters:
// - E: The type of the wrapped value (the actual data)
// - A: The phantom type parameter (not stored, only used for type-level operations)
//
// Example:
//
// // Create a Const that wraps a string
// c := Make[string, int]("metadata")
//
// // The int type parameter is phantom - no int value is stored
// value := Unwrap(c) // "metadata"
type Const[E, A any] struct {
value E
}
// Make creates a Const value wrapping the given value.
//
// This is the primary constructor for Const values. The second type parameter A
// is phantom and must be specified explicitly when needed for type inference.
//
// Type Parameters:
// - E: The type of the value to wrap
// - A: The phantom type parameter
//
// Parameters:
// - e: The value to wrap
//
// Returns:
// - A Const[E, A] wrapping the value
//
// Example:
//
// c := Make[string, int]("hello")
// value := Unwrap(c) // "hello"
func Make[E, A any](e E) Const[E, A] {
return Const[E, A]{value: e}
}
// Unwrap extracts the wrapped value from a Const.
//
// This is the inverse of Make, retrieving the actual value stored in the Const.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The phantom type parameter
//
// Parameters:
// - c: The Const to unwrap
//
// Returns:
// - The wrapped value of type E
//
// Example:
//
// c := Make[string, int]("world")
// value := Unwrap(c) // "world"
func Unwrap[E, A any](c Const[E, A]) E {
return c.value
}
// Of creates a Const containing the monoid's empty value, ignoring the input.
//
// This implements the Applicative's "pure" operation for Const. It creates a Const
// wrapping the monoid's identity element, regardless of the input value.
//
// Type Parameters:
// - E: The type of the wrapped value (must have a monoid)
// - A: The input type (ignored)
//
// Parameters:
// - m: The monoid providing the empty value
//
// Returns:
// - A function that ignores its input and returns Const[E, A] with the empty value
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// of := Of[string, int](S.Monoid)
// c := of(42) // Const[string, int] containing ""
// value := Unwrap(c) // ""
func Of[E, A any](m M.Monoid[E]) func(A) Const[E, A] {
return F.Constant1[A](Make[E, A](m.Empty()))
}
// MonadMap applies a function to the phantom type parameter without changing the wrapped value.
//
// This implements the Functor's map operation for Const. Since the type parameter A is phantom,
// the function is never actually called - the wrapped value E remains unchanged.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - fa: The Const to map over
// - _: The function to apply (ignored)
//
// Returns:
// - A Const[E, B] with the same wrapped value
//
// Example:
//
// c := Make[string, int]("hello")
// mapped := MonadMap(c, func(i int) string { return strconv.Itoa(i) })
// // mapped still contains "hello", function was never called
func MonadMap[E, A, B any](fa Const[E, A], _ func(A) B) Const[E, B] {
return Make[E, B](fa.value)
}
// MonadAp combines two Const values using a semigroup.
//
// This implements the Applicative's ap operation for Const. It combines the wrapped
// values from both Const instances using the provided semigroup, ignoring the function
// type in the first argument.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A function that takes two Const values and combines their wrapped values
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// ap := MonadAp[string, int, int](S.Monoid)
// c1 := Make[string, func(int) int]("hello")
// c2 := Make[string, int]("world")
// result := ap(c1, c2) // Const containing "helloworld"
func MonadAp[E, A, B any](s S.Semigroup[E]) func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return Make[E, B](s.Concat(fab.value, fa.value))
}
}
// Map applies a function to the phantom type parameter without changing the wrapped value.
//
// This is the curried version of MonadMap, providing a more functional programming style.
// The function is never actually called since A is a phantom type.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - f: The function to apply (ignored)
//
// Returns:
// - A function that transforms Const[E, A] to Const[E, B]
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// c := Make[string, int]("data")
// mapped := F.Pipe1(c, Map[string, int, string](strconv.Itoa))
// // mapped still contains "data"
func Map[E, A, B any](f func(A) B) func(fa Const[E, A]) Const[E, B] {
return F.Bind2nd(MonadMap[E, A, B], f)
}
// Ap combines Const values using a semigroup in a curried style.
//
// This is the curried version of MonadAp, providing data-last style for better composition.
// It combines the wrapped values from both Const instances using the provided semigroup.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A curried function for combining Const values
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// c1 := Make[string, int]("hello")
// c2 := Make[string, func(int) int]("world")
// result := F.Pipe1(c1, Ap[string, int, int](S.Monoid)(c2))
// // result contains "helloworld"
func Ap[E, A, B any](s S.Semigroup[E]) func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {
monadap := MonadAp[E, A, B](s)
return func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {

View File

@@ -16,25 +16,340 @@
package constant
import (
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestMap(t *testing.T) {
fa := Make[string, int]("foo")
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
// TestMake tests the Make constructor
func TestMake(t *testing.T) {
t.Run("creates Const with string value", func(t *testing.T) {
c := Make[string, int]("hello")
assert.Equal(t, "hello", Unwrap(c))
})
t.Run("creates Const with int value", func(t *testing.T) {
c := Make[int, string](42)
assert.Equal(t, 42, Unwrap(c))
})
t.Run("creates Const with struct value", func(t *testing.T) {
type Config struct {
Name string
Port int
}
cfg := Config{Name: "server", Port: 8080}
c := Make[Config, bool](cfg)
assert.Equal(t, cfg, Unwrap(c))
})
}
// TestUnwrap tests extracting values from Const
func TestUnwrap(t *testing.T) {
t.Run("unwraps string value", func(t *testing.T) {
c := Make[string, int]("world")
value := Unwrap(c)
assert.Equal(t, "world", value)
})
t.Run("unwraps empty string", func(t *testing.T) {
c := Make[string, int]("")
value := Unwrap(c)
assert.Equal(t, "", value)
})
t.Run("unwraps zero value", func(t *testing.T) {
c := Make[int, string](0)
value := Unwrap(c)
assert.Equal(t, 0, value)
})
}
// TestOf tests the Of function
func TestOf(t *testing.T) {
assert.Equal(t, Make[string, int](""), Of[string, int](S.Monoid)(1))
t.Run("creates Const with monoid empty value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c := of(42)
assert.Equal(t, "", Unwrap(c))
})
t.Run("ignores input value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c1 := of(1)
c2 := of(100)
assert.Equal(t, Unwrap(c1), Unwrap(c2))
})
t.Run("works with int monoid", func(t *testing.T) {
of := Of[int, string](N.MonoidSum[int]())
c := of("ignored")
assert.Equal(t, 0, Unwrap(c))
})
}
func TestAp(t *testing.T) {
fab := Make[string, int]("bar")
assert.Equal(t, Make[string, int]("foobar"), Ap[string, int, int](S.Monoid)(fab)(Make[string, func(int) int]("foo")))
// TestMap tests the Map function
func TestMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("foo")
result := F.Pipe1(fa, Map[string](utils.Double))
assert.Equal(t, "foo", Unwrap(result))
})
t.Run("changes phantom type", func(t *testing.T) {
fa := Make[string, int]("data")
fb := Map[string, int, string](strconv.Itoa)(fa)
// Value unchanged, but type changed from Const[string, int] to Const[string, string]
assert.Equal(t, "data", Unwrap(fb))
})
t.Run("function is never called", func(t *testing.T) {
called := false
fa := Make[string, int]("test")
fb := Map[string, int, string](func(i int) string {
called = true
return strconv.Itoa(i)
})(fa)
assert.False(t, called, "Map function should not be called")
assert.Equal(t, "test", Unwrap(fb))
})
}
// TestMonadMap tests the MonadMap function
func TestMonadMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("original")
fb := MonadMap(fa, func(i int) string { return strconv.Itoa(i) })
assert.Equal(t, "original", Unwrap(fb))
})
t.Run("works with different types", func(t *testing.T) {
fa := Make[int, string](42)
fb := MonadMap(fa, func(s string) bool { return len(s) > 0 })
assert.Equal(t, 42, Unwrap(fb))
})
}
// TestAp tests the Ap function
func TestAp(t *testing.T) {
t.Run("combines string values", func(t *testing.T) {
fab := Make[string, int]("bar")
fa := Make[string, func(int) int]("foo")
result := Ap[string, int, int](S.Monoid)(fab)(fa)
assert.Equal(t, "foobar", Unwrap(result))
})
t.Run("combines int values with sum", func(t *testing.T) {
fab := Make[int, string](10)
fa := Make[int, func(string) string](5)
result := Ap[int, string, string](N.SemigroupSum[int]())(fab)(fa)
assert.Equal(t, 15, Unwrap(result))
})
t.Run("combines int values with product", func(t *testing.T) {
fab := Make[int, bool](3)
fa := Make[int, func(bool) bool](4)
result := Ap[int, bool, bool](N.SemigroupProduct[int]())(fab)(fa)
assert.Equal(t, 12, Unwrap(result))
})
}
// TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) {
t.Run("combines values using semigroup", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("hello")
fa := Make[string, int]("world")
result := ap(fab, fa)
assert.Equal(t, "helloworld", Unwrap(result))
})
t.Run("works with empty strings", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("")
fa := Make[string, int]("test")
result := ap(fab, fa)
assert.Equal(t, "test", Unwrap(result))
})
}
// TestMonoid tests the Monoid function
func TestMonoid(t *testing.T) {
t.Run("always returns constant value", func(t *testing.T) {
m := Monoid(42)
assert.Equal(t, 42, m.Concat(1, 2))
assert.Equal(t, 42, m.Concat(100, 200))
assert.Equal(t, 42, m.Empty())
})
t.Run("works with strings", func(t *testing.T) {
m := Monoid("constant")
assert.Equal(t, "constant", m.Concat("a", "b"))
assert.Equal(t, "constant", m.Empty())
})
t.Run("works with structs", func(t *testing.T) {
type Point struct{ X, Y int }
p := Point{X: 1, Y: 2}
m := Monoid(p)
assert.Equal(t, p, m.Concat(Point{X: 3, Y: 4}, Point{X: 5, Y: 6}))
assert.Equal(t, p, m.Empty())
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := Monoid(10)
// Left identity: Concat(Empty(), x) = x (both return constant)
assert.Equal(t, 10, m.Concat(m.Empty(), 5))
// Right identity: Concat(x, Empty()) = x (both return constant)
assert.Equal(t, 10, m.Concat(5, m.Empty()))
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
left := m.Concat(m.Concat(1, 2), 3)
right := m.Concat(1, m.Concat(2, 3))
assert.Equal(t, left, right)
assert.Equal(t, 10, left)
})
}
// TestConstFunctorLaws tests functor laws for Const
func TestConstFunctorLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// map id = id
fa := Make[string, int]("test")
mapped := Map[string, int, int](F.Identity[int])(fa)
assert.Equal(t, Unwrap(fa), Unwrap(mapped))
})
t.Run("composition law", func(t *testing.T) {
// map (g . f) = map g . map f
fa := Make[string, int]("data")
f := func(i int) string { return strconv.Itoa(i) }
g := func(s string) bool { return len(s) > 0 }
// map (g . f)
composed := Map[string, int, bool](func(i int) bool { return g(f(i)) })(fa)
// map g . map f
intermediate := F.Pipe1(fa, Map[string, int, string](f))
chained := Map[string, string, bool](g)(intermediate)
assert.Equal(t, Unwrap(composed), Unwrap(chained))
})
}
// TestConstApplicativeLaws tests applicative laws for Const
func TestConstApplicativeLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// For Const, ap combines the wrapped values using the semigroup
// ap (of id) v combines empty (from of) with v's value
v := Make[string, int]("value")
ofId := Of[string, func(int) int](S.Monoid)(F.Identity[int])
result := Ap[string, int, int](S.Monoid)(v)(ofId)
// Result combines "" (from Of) with "value" using string monoid
assert.Equal(t, "value", Unwrap(result))
})
t.Run("homomorphism law", func(t *testing.T) {
// ap (of f) (of x) = of (f x)
f := func(i int) string { return strconv.Itoa(i) }
x := 42
ofF := Of[string, func(int) string](S.Monoid)(f)
ofX := Of[string, int](S.Monoid)(x)
left := Ap[string, int, string](S.Monoid)(ofX)(ofF)
right := Of[string, string](S.Monoid)(f(x))
assert.Equal(t, Unwrap(left), Unwrap(right))
})
}
// TestConstEdgeCases tests edge cases
func TestConstEdgeCases(t *testing.T) {
t.Run("empty string values", func(t *testing.T) {
c := Make[string, int]("")
assert.Equal(t, "", Unwrap(c))
mapped := Map[string, int, string](strconv.Itoa)(c)
assert.Equal(t, "", Unwrap(mapped))
})
t.Run("zero values", func(t *testing.T) {
c := Make[int, string](0)
assert.Equal(t, 0, Unwrap(c))
})
t.Run("nil pointer", func(t *testing.T) {
var ptr *int
c := Make[*int, string](ptr)
assert.Nil(t, Unwrap(c))
})
t.Run("multiple map operations", func(t *testing.T) {
c := Make[string, int]("original")
// Chain multiple map operations
step1 := Map[string, int, string](strconv.Itoa)(c)
step2 := Map[string, string, bool](func(s string) bool { return len(s) > 0 })(step1)
result := Map[string, bool, int](func(b bool) int {
if b {
return 1
}
return 0
})(step2)
assert.Equal(t, "original", Unwrap(result))
})
}
// BenchmarkMake benchmarks the Make constructor
func BenchmarkMake(b *testing.B) {
b.ResetTimer()
for b.Loop() {
_ = Make[string, int]("test")
}
}
// BenchmarkUnwrap benchmarks the Unwrap function
func BenchmarkUnwrap(b *testing.B) {
c := Make[string, int]("test")
b.ResetTimer()
for b.Loop() {
_ = Unwrap(c)
}
}
// BenchmarkMap benchmarks the Map function
func BenchmarkMap(b *testing.B) {
c := Make[string, int]("test")
mapFn := Map[string, int, string](strconv.Itoa)
b.ResetTimer()
for b.Loop() {
_ = mapFn(c)
}
}
// BenchmarkAp benchmarks the Ap function
func BenchmarkAp(b *testing.B) {
fab := Make[string, int]("hello")
fa := Make[string, func(int) int]("world")
apFn := Ap[string, int, int](S.Monoid)
b.ResetTimer()
for b.Loop() {
_ = apFn(fab)(fa)
}
}
// BenchmarkMonoid benchmarks the Monoid function
func BenchmarkMonoid(b *testing.B) {
m := Monoid(42)
b.ResetTimer()
for b.Loop() {
_ = m.Concat(1, 2)
}
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package constant
import (
@@ -5,7 +20,47 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a [M.Monoid] that returns a constant value in all operations
// Monoid creates a monoid that always returns a constant value.
//
// This creates a trivial monoid where both the Concat operation and Empty
// always return the same constant value, regardless of inputs. This is useful
// for testing, placeholder implementations, or when you need a monoid instance
// but the actual combining behavior doesn't matter.
//
// # Monoid Laws
//
// The constant monoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always returns 'a'
// - Left Identity: Concat(Empty(), x) = x - both return 'a'
// - Right Identity: Concat(x, Empty()) = x - both return 'a'
//
// Type Parameters:
// - A: The type of the constant value
//
// Parameters:
// - a: The constant value to return in all operations
//
// Returns:
// - A Monoid[A] that always returns the constant value
//
// Example:
//
// // Create a monoid that always returns 42
// m := Monoid(42)
// result := m.Concat(1, 2) // 42
// empty := m.Empty() // 42
//
// // Useful for testing or placeholder implementations
// type Config struct {
// Timeout int
// }
// defaultConfig := Monoid(Config{Timeout: 30})
// config := defaultConfig.Concat(Config{Timeout: 10}, Config{Timeout: 20})
// // config is Config{Timeout: 30}
//
// See also:
// - function.Constant2: The underlying constant function
// - M.MakeMonoid: The monoid constructor
func Monoid[A any](a A) M.Monoid[A] {
return M.MakeMonoid(function.Constant2[A, A](a), a)
}

130
v2/context/reader/reader.go Normal file
View File

@@ -0,0 +1,130 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package reader provides a specialization of the Reader monad for [context.Context].
//
// This package offers a context-aware Reader monad that simplifies working with
// Go's [context.Context] in a functional programming style. It eliminates the need
// to explicitly thread context through function calls while maintaining type safety
// and composability.
//
// # Core Concept
//
// The Reader monad represents computations that depend on a shared environment.
// In this package, that environment is fixed to [context.Context], making it
// particularly useful for:
//
// - Request-scoped data propagation
// - Cancellation and timeout handling
// - Dependency injection via context values
// - Avoiding explicit context parameter threading
//
// # Type Definitions
//
// - Reader[A]: A computation that depends on context.Context and produces A
// - Kleisli[A, B]: A function from A to Reader[B] for composing computations
// - Operator[A, B]: A transformation from Reader[A] to Reader[B]
//
// # Usage Pattern
//
// Instead of passing context explicitly through every function:
//
// func processUser(ctx context.Context, userID string) (User, error) {
// user := fetchUser(ctx, userID)
// profile := fetchProfile(ctx, user.ProfileID)
// return enrichUser(ctx, user, profile), nil
// }
//
// You can use Reader to compose context-dependent operations:
//
// fetchUser := func(userID string) Reader[User] {
// return func(ctx context.Context) User {
// // Use ctx for database access, cancellation, etc.
// return queryDatabase(ctx, userID)
// }
// }
//
// processUser := func(userID string) Reader[User] {
// return F.Pipe2(
// fetchUser(userID),
// reader.Chain(func(user User) Reader[Profile] {
// return fetchProfile(user.ProfileID)
// }),
// reader.Map(func(profile Profile) User {
// return enrichUser(user, profile)
// }),
// )
// }
//
// // Execute with context
// ctx := context.Background()
// user := processUser("user123")(ctx)
//
// # Integration with Standard Library
//
// This package works seamlessly with Go's standard [context] package:
//
// - Context cancellation and deadlines are preserved
// - Context values can be accessed within Reader computations
// - Readers can be composed with context-aware libraries
//
// # Relationship to Other Packages
//
// This package is a specialization of [github.com/IBM/fp-go/v2/reader] where
// the environment type R is fixed to [context.Context]. For more general
// Reader operations, see the base reader package.
//
// For combining Reader with other monads:
// - [github.com/IBM/fp-go/v2/context/readerio]: Reader + IO effects
// - [github.com/IBM/fp-go/v2/readeroption]: Reader + Option
// - [github.com/IBM/fp-go/v2/readerresult]: Reader + Result (Either)
//
// # Example: HTTP Request Handler
//
// type RequestContext struct {
// UserID string
// RequestID string
// }
//
// // Extract request context from context.Context
// getRequestContext := func(ctx context.Context) RequestContext {
// return RequestContext{
// UserID: ctx.Value("userID").(string),
// RequestID: ctx.Value("requestID").(string),
// }
// }
//
// // A Reader that logs with request context
// logInfo := func(message string) Reader[function.Void] {
// return func(ctx context.Context) function.Void {
// reqCtx := getRequestContext(ctx)
// log.Printf("[%s] User %s: %s", reqCtx.RequestID, reqCtx.UserID, message)
// return function.VOID
// }
// }
//
// // Compose operations
// handleRequest := func(data string) Reader[Response] {
// return F.Pipe2(
// logInfo("Processing request"),
// reader.Chain(func(_ function.Void) Reader[Result] {
// return processData(data)
// }),
// reader.Map(func(result Result) Response {
// return Response{Data: result}
// }),
// )
// }
package reader

142
v2/context/reader/types.go Normal file
View File

@@ -0,0 +1,142 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reader
import (
"context"
R "github.com/IBM/fp-go/v2/reader"
)
type (
// Reader represents a computation that depends on a [context.Context] and produces a value of type A.
//
// This is a specialization of the generic Reader monad where the environment type is fixed
// to [context.Context]. This is particularly useful for Go applications that need to thread
// context through computations for cancellation, deadlines, and request-scoped values.
//
// Type Parameters:
// - A: The result type produced by the computation
//
// Reader[A] is equivalent to func(context.Context) A
//
// The Reader monad enables:
// - Dependency injection using context values
// - Cancellation and timeout handling
// - Request-scoped data propagation
// - Avoiding explicit context parameter threading
//
// Example:
//
// // A Reader that extracts a user ID from context
// getUserID := func(ctx context.Context) string {
// if userID, ok := ctx.Value("userID").(string); ok {
// return userID
// }
// return "anonymous"
// }
//
// // A Reader that checks if context is cancelled
// isCancelled := func(ctx context.Context) bool {
// select {
// case <-ctx.Done():
// return true
// default:
// return false
// }
// }
//
// // Use the readers with a context
// ctx := context.WithValue(context.Background(), "userID", "user123")
// userID := getUserID(ctx) // "user123"
// cancelled := isCancelled(ctx) // false
Reader[A any] = R.Reader[context.Context, A]
// Kleisli represents a Kleisli arrow for the context-based Reader monad.
//
// It's a function from A to Reader[B], used for composing Reader computations
// that all depend on the same [context.Context].
//
// Type Parameters:
// - A: The input type
// - B: The output type wrapped in Reader
//
// Kleisli[A, B] is equivalent to func(A) func(context.Context) B
//
// Kleisli arrows are fundamental for monadic composition, allowing you to chain
// operations that depend on context without explicitly passing the context through
// each function call.
//
// Example:
//
// // A Kleisli arrow that creates a greeting Reader from a name
// greet := func(name string) Reader[string] {
// return func(ctx context.Context) string {
// if deadline, ok := ctx.Deadline(); ok {
// return fmt.Sprintf("Hello %s (deadline: %v)", name, deadline)
// }
// return fmt.Sprintf("Hello %s", name)
// }
// }
//
// // Use the Kleisli arrow
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// greeting := greet("Alice")(ctx) // "Hello Alice (deadline: ...)"
Kleisli[A, B any] = R.Reader[A, Reader[B]]
// Operator represents a transformation from one Reader to another.
//
// It takes a Reader[A] and produces a Reader[B], where both readers depend on
// the same [context.Context]. This type is commonly used for operations like
// Map, Chain, and other transformations that convert readers while preserving
// the context dependency.
//
// Type Parameters:
// - A: The input Reader's result type
// - B: The output Reader's result type
//
// Operator[A, B] is equivalent to func(Reader[A]) func(context.Context) B
//
// Operators enable building pipelines of context-dependent computations where
// each step can transform the result of the previous computation while maintaining
// access to the shared context.
//
// Example:
//
// // An operator that transforms int readers to string readers
// intToString := func(r Reader[int]) Reader[string] {
// return func(ctx context.Context) string {
// value := r(ctx)
// return strconv.Itoa(value)
// }
// }
//
// // A Reader that extracts a timeout value from context
// getTimeout := func(ctx context.Context) int {
// if deadline, ok := ctx.Deadline(); ok {
// return int(time.Until(deadline).Seconds())
// }
// return 0
// }
//
// // Transform the Reader
// getTimeoutStr := intToString(getTimeout)
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
// defer cancel()
// result := getTimeoutStr(ctx) // "30" (approximately)
Operator[A, B any] = Kleisli[Reader[A], B]
)

View File

@@ -1,6 +1,7 @@
package readerio
import (
"github.com/IBM/fp-go/v2/function"
RIO "github.com/IBM/fp-go/v2/readerio"
)
@@ -73,3 +74,117 @@ func Bracket[
) ReaderIO[B] {
return RIO.Bracket(acquire, use, release)
}
// WithResource creates a higher-order function that manages a resource lifecycle for any operation.
// It returns a Kleisli arrow that takes a use function and automatically handles resource
// acquisition and cleanup using the bracket pattern.
//
// This is a more composable alternative to Bracket, allowing you to define resource management
// once and reuse it with different use functions. The resource is acquired when the returned
// Kleisli arrow is invoked, used by the provided function, and then released regardless of
// success or failure.
//
// Type Parameters:
// - A: The type of the resource to be managed
// - B: The type of the result produced by the use function
// - ANY: The type returned by the release function (typically ignored)
//
// Parameters:
// - onCreate: A ReaderIO that acquires/creates the resource
// - onRelease: A Kleisli arrow that releases/cleans up the resource
//
// Returns:
// - A Kleisli arrow that takes a use function and returns a ReaderIO managing the full lifecycle
//
// Example with database connection:
//
// // Define resource management once
// withDB := WithResource(
// // Acquire connection
// func(ctx context.Context) IO[*sql.DB] {
// return func() *sql.DB {
// db, _ := sql.Open("postgres", "connection-string")
// return db
// }
// },
// // Release connection
// func(db *sql.DB) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// db.Close()
// return nil
// }
// }
// },
// )
//
// // Reuse with different operations
// queryUsers := withDB(func(db *sql.DB) ReaderIO[[]User] {
// return func(ctx context.Context) IO[[]User] {
// return func() []User {
// // Query users from db
// return users
// }
// }
// })
//
// insertUser := withDB(func(db *sql.DB) ReaderIO[int64] {
// return func(ctx context.Context) IO[int64] {
// return func() int64 {
// // Insert user into db
// return userID
// }
// }
// })
//
// Example with file handling:
//
// withFile := WithResource(
// func(ctx context.Context) IO[*os.File] {
// return func() *os.File {
// f, _ := os.Open("data.txt")
// return f
// }
// },
// func(f *os.File) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// f.Close()
// return nil
// }
// }
// },
// )
//
// // Use for reading
// readContent := withFile(func(f *os.File) ReaderIO[string] {
// return func(ctx context.Context) IO[string] {
// return func() string {
// data, _ := io.ReadAll(f)
// return string(data)
// }
// }
// })
//
// // Use for getting file info
// getSize := withFile(func(f *os.File) ReaderIO[int64] {
// return func(ctx context.Context) IO[int64] {
// return func() int64 {
// info, _ := f.Stat()
// return info.Size()
// }
// }
// })
//
// Use Cases:
// - Database connections: Acquire connection, execute queries, close connection
// - File handles: Open file, read/write, close file
// - Network connections: Establish connection, transfer data, close connection
// - Locks: Acquire lock, perform critical section, release lock
// - Temporary resources: Create temp file/directory, use it, clean up
//
//go:inline
func WithResource[A, B, ANY any](
onCreate ReaderIO[A], onRelease Kleisli[A, ANY]) Kleisli[Kleisli[A, B], B] {
return function.Bind13of3(Bracket[A, B, ANY])(onCreate, function.Ignore2of2[B](onRelease))
}

View File

@@ -0,0 +1,454 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerio
import (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/io"
"github.com/stretchr/testify/assert"
)
// mockResource simulates a resource that tracks its lifecycle
type mockResource struct {
id int
acquired bool
released bool
used bool
}
// TestBracket_Success tests that Bracket properly manages resource lifecycle on success
func TestBracket_Success(t *testing.T) {
resource := &mockResource{id: 1}
// Acquire resource
acquire := func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
}
// Use resource
use := func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r.used = true
return "success"
}
}
}
// Release resource
release := func(r *mockResource, result string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
}
// Execute bracket
operation := Bracket(acquire, use, release)
result := operation(context.Background())()
// Verify lifecycle
assert.True(t, resource.acquired, "Resource should be acquired")
assert.True(t, resource.used, "Resource should be used")
assert.True(t, resource.released, "Resource should be released")
assert.Equal(t, "success", result)
}
// TestBracket_MultipleResources tests managing multiple resources
func TestBracket_MultipleResources(t *testing.T) {
resource1 := &mockResource{id: 1}
resource2 := &mockResource{id: 2}
acquire1 := func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource1.acquired = true
return resource1
}
}
use1 := func(r1 *mockResource) ReaderIO[*mockResource] {
return func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
r1.used = true
resource2.acquired = true
return resource2
}
}
}
release1 := func(r1 *mockResource, result string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r1.released = true
return nil
}
}
}
// Nested bracket for second resource
use2 := func(r2 *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r2.used = true
return "both used"
}
}
}
release2 := func(r2 *mockResource, result string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r2.released = true
return nil
}
}
}
// Compose brackets
operation := Bracket(acquire1, func(r1 *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
r2 := use1(r1)(ctx)()
return Bracket(
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource { return r2 }
},
use2,
release2,
)(ctx)
}
}, release1)
result := operation(context.Background())()
assert.True(t, resource1.acquired)
assert.True(t, resource1.used)
assert.True(t, resource1.released)
assert.True(t, resource2.acquired)
assert.True(t, resource2.used)
assert.True(t, resource2.released)
assert.Equal(t, "both used", result)
}
// TestWithResource_Success tests WithResource with successful operation
func TestWithResource_Success(t *testing.T) {
resource := &mockResource{id: 1}
// Define resource management
withResource := WithResource[*mockResource, string, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// Use resource
operation := withResource(func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r.used = true
return "result"
}
}
})
result := operation(context.Background())()
assert.True(t, resource.acquired)
assert.True(t, resource.used)
assert.True(t, resource.released)
assert.Equal(t, "result", result)
}
// TestWithResource_Reusability tests that WithResource can be reused with different operations
func TestWithResource_Reusability(t *testing.T) {
callCount := 0
withResource := WithResource[*mockResource, int, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
callCount++
return &mockResource{id: callCount, acquired: true}
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// First operation
op1 := withResource(func(r *mockResource) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
r.used = true
return r.id * 2
}
}
})
result1 := op1(context.Background())()
assert.Equal(t, 2, result1)
assert.Equal(t, 1, callCount)
// Second operation (should create new resource)
op2 := withResource(func(r *mockResource) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
r.used = true
return r.id * 3
}
}
})
result2 := op2(context.Background())()
assert.Equal(t, 6, result2)
assert.Equal(t, 2, callCount)
}
// TestWithResource_DifferentResultTypes tests WithResource with different result types
func TestWithResource_DifferentResultTypes(t *testing.T) {
resource := &mockResource{id: 42}
withResourceInt := WithResource[*mockResource, int, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// Operation returning int
opInt := withResourceInt(func(r *mockResource) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
return r.id
}
}
})
resultInt := opInt(context.Background())()
assert.Equal(t, 42, resultInt)
// Reset resource state
resource.acquired = false
resource.released = false
// Create new WithResource for string type
withResourceString := WithResource[*mockResource, string, any](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
r.released = true
return nil
}
}
},
)
// Operation returning string
opString := withResourceString(func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
return "value"
}
}
})
resultString := opString(context.Background())()
assert.Equal(t, "value", resultString)
assert.True(t, resource.released)
}
// TestWithResource_ContextPropagation tests that context is properly propagated
func TestWithResource_ContextPropagation(t *testing.T) {
type contextKey string
const key contextKey = "test-key"
withResource := WithResource[string, string, any](
func(ctx context.Context) io.IO[string] {
return func() string {
value := ctx.Value(key)
if value != nil {
return value.(string)
}
return "no-value"
}
},
func(r string) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
return nil
}
}
},
)
operation := withResource(func(r string) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
return r + "-processed"
}
}
})
ctx := context.WithValue(context.Background(), key, "test-value")
result := operation(ctx)()
assert.Equal(t, "test-value-processed", result)
}
// TestWithResource_ErrorInRelease tests behavior when release function encounters an error
func TestWithResource_ErrorInRelease(t *testing.T) {
resource := &mockResource{id: 1}
releaseError := errors.New("release failed")
withResource := WithResource[*mockResource, string, error](
func(ctx context.Context) io.IO[*mockResource] {
return func() *mockResource {
resource.acquired = true
return resource
}
},
func(r *mockResource) ReaderIO[error] {
return func(ctx context.Context) io.IO[error] {
return func() error {
r.released = true
return releaseError
}
}
},
)
operation := withResource(func(r *mockResource) ReaderIO[string] {
return func(ctx context.Context) io.IO[string] {
return func() string {
r.used = true
return "success"
}
}
})
result := operation(context.Background())()
// Operation should succeed even if release returns error
assert.Equal(t, "success", result)
assert.True(t, resource.acquired)
assert.True(t, resource.used)
assert.True(t, resource.released)
}
// BenchmarkBracket benchmarks the Bracket function
func BenchmarkBracket(b *testing.B) {
acquire := func(ctx context.Context) io.IO[int] {
return func() int {
return 42
}
}
use := func(n int) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
return n * 2
}
}
}
release := func(n int, result int) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
return nil
}
}
}
operation := Bracket(acquire, use, release)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
operation(ctx)()
}
}
// BenchmarkWithResource benchmarks the WithResource function
func BenchmarkWithResource(b *testing.B) {
withResource := WithResource[int, int, any](
func(ctx context.Context) io.IO[int] {
return func() int {
return 42
}
},
func(n int) ReaderIO[any] {
return func(ctx context.Context) io.IO[any] {
return func() any {
return nil
}
}
},
)
operation := withResource(func(n int) ReaderIO[int] {
return func(ctx context.Context) io.IO[int] {
return func() int {
return n * 2
}
}
})
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
operation(ctx)()
}
}

View File

@@ -19,6 +19,9 @@ import (
"context"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
RIO "github.com/IBM/fp-go/v2/readerio"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
@@ -33,21 +36,24 @@ import (
// The function f returns both a new context and a CancelFunc that should be called to release resources.
//
// Type Parameters:
// - R: The input environment type that f transforms into context.Context
// - A: The original result type produced by the ReaderIO
// - B: The new output result 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 value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
// - A Kleisli arrow that takes a ReaderIO[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) RIO.Kleisli[R, ReaderIO[A], B] {
return function.Flow2(
Local[A](f),
Map(g),
RIO.Map[R](g),
)
}
@@ -61,14 +67,87 @@ func Promap[A, B 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 ReaderIO[A] and returns a ReaderIO[A]
// - A Kleisli arrow that takes a ReaderIO[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]) RIO.Kleisli[R, ReaderIO[A], A] {
return Local[A](f)
}
// LocalIOK transforms the context using an IO effect before passing it to a ReaderIO computation.
//
// This is similar to Local, but the context transformation itself is wrapped in an IO effect,
// allowing for side-effectful context transformations. The transformation function receives
// the current context and returns an IO effect that produces a new context along with a
// cancel function. The cancel function is automatically called when the computation completes
// (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Context transformations that require side effects (e.g., loading configuration)
// - Lazy initialization of context values
// - Context transformations that may fail or need to perform I/O
// - Composing effectful context setup with computations
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - f: An IO Kleisli arrow that transforms the context with side effects
//
// Returns:
// - An Operator that runs the computation with the effectfully transformed context
//
// Example:
//
// import (
// "context"
// G "github.com/IBM/fp-go/v2/io"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Context transformation with side effects (e.g., loading config)
// loadConfig := func(ctx context.Context) G.IO[ContextCancel] {
// return func() ContextCancel {
// // Simulate loading configuration
// config := loadConfigFromFile()
// newCtx := context.WithValue(ctx, "config", config)
// return pair.MakePair[context.CancelFunc](func() {}, newCtx)
// }
// }
//
// getValue := readerio.FromReader(func(ctx context.Context) string {
// if cfg := ctx.Value("config"); cfg != nil {
// return cfg.(string)
// }
// return "default"
// })
//
// result := F.Pipe1(
// getValue,
// readerio.LocalIOK[string](loadConfig),
// )
// value := result(t.Context())() // Loads config and uses it
//
// Comparison with Local:
// - Local: Takes a pure function that transforms the context
// - LocalIOK: Takes an IO effect that transforms the context, allowing side effects
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return func(r ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
p := f(ctx)
return func() A {
otherCancel, otherCtx := pair.Unpack(p())
defer otherCancel()
return r(otherCtx)()
}
}
}
}

View File

@@ -21,6 +21,7 @@ import (
"testing"
"time"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
@@ -38,9 +39,9 @@ func TestPromapBasic(t *testing.T) {
}
// Transform context and result
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) ContextCancel {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
toString := strconv.Itoa
@@ -63,9 +64,9 @@ func TestContramapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) ContextCancel {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
adapted := Contramap[int](addKey)(getValue)
@@ -85,8 +86,9 @@ func TestLocalBasic(t *testing.T) {
}
}
addTimeout := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, time.Second)
addTimeout := func(ctx context.Context) ContextCancel {
newCtx, cancelFct := context.WithTimeout(ctx, time.Second)
return pair.MakePair(cancelFct, newCtx)
}
adapted := Local[bool](addTimeout)(getValue)
@@ -95,3 +97,81 @@ func TestLocalBasic(t *testing.T) {
assert.True(t, result)
})
}
// TestLocalIOKBasic tests basic LocalIOK functionality
func TestLocalIOKBasic(t *testing.T) {
t.Run("context transformation with IO effect", func(t *testing.T) {
getValue := func(ctx context.Context) IO[string] {
return func() string {
if v := ctx.Value("key"); v != nil {
return v.(string)
}
return "default"
}
}
// Context transformation wrapped in IO effect
addKeyIO := func(ctx context.Context) IO[ContextCancel] {
return func() ContextCancel {
// Simulate side effect (e.g., loading config)
newCtx := context.WithValue(ctx, "key", "loaded-value")
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
}
}
adapted := LocalIOK[string](addKeyIO)(getValue)
result := adapted(t.Context())()
assert.Equal(t, "loaded-value", result)
})
t.Run("cleanup function is called", func(t *testing.T) {
cleanupCalled := false
getValue := func(ctx context.Context) IO[int] {
return func() int {
if v := ctx.Value("value"); v != nil {
return v.(int)
}
return 0
}
}
addValueIO := func(ctx context.Context) IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "value", 42)
cleanup := context.CancelFunc(func() {
cleanupCalled = true
})
return pair.MakePair(cleanup, newCtx)
}
}
adapted := LocalIOK[int](addValueIO)(getValue)
result := adapted(t.Context())()
assert.Equal(t, 42, result)
assert.True(t, cleanupCalled, "cleanup function should be called")
})
t.Run("works with timeout context", func(t *testing.T) {
getValue := func(ctx context.Context) IO[bool] {
return func() bool {
_, hasDeadline := ctx.Deadline()
return hasDeadline
}
}
addTimeoutIO := func(ctx context.Context) IO[ContextCancel] {
return func() ContextCancel {
newCtx, cancelFct := context.WithTimeout(ctx, time.Second)
return pair.MakePair(cancelFct, newCtx)
}
}
adapted := LocalIOK[bool](addTimeoutIO)(getValue)
result := adapted(t.Context())()
assert.True(t, result, "context should have deadline")
})
}

View File

@@ -20,6 +20,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
RIO "github.com/IBM/fp-go/v2/readerio"
)
@@ -633,12 +634,15 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
//
// Type Parameters:
// - A: The value type of the ReaderIO
// - R: The input environment type that f transforms into context.Context
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
// - f: A function that transforms the input environment R into context.Context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
// - A Kleisli arrow that runs the computation with the transformed context
//
// Note: When R is context.Context, this simplifies to an Operator[A, A]
//
// Example:
//
@@ -648,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 {
@@ -669,19 +673,20 @@ 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(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIO.Kleisli[R, ReaderIO[A], A] {
return func(rr ReaderIO[A]) RIO.ReaderIO[R, A] {
return func(r R) IO[A] {
return func() A {
otherCtx, otherCancel := f(ctx)
otherCancel, otherCtx := pair.Unpack(f(r))
defer otherCancel()
return rr(otherCtx)()
}
@@ -742,8 +747,9 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// )
// data := result(t.Context())() // Returns Data{Value: "quick"}
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
return Local[A](func(ctx context.Context) ContextCancel {
newCtx, cancelFct := context.WithTimeout(ctx, timeout)
return pair.MakePair(cancelFct, newCtx)
})
}
@@ -806,8 +812,9 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// )
// data := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
return Local[A](func(ctx context.Context) ContextCancel {
newCtx, cancelFct := context.WithDeadline(ctx, deadline)
return pair.MakePair(cancelFct, newCtx)
})
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
@@ -81,4 +82,15 @@ type (
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
// Pair represents a tuple of two values of types A and B.
// It is used to group two related values together.
Pair[A, B any] = pair.Pair[A, B]
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

View File

@@ -21,6 +21,7 @@ import (
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/pair"
)
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
@@ -85,3 +86,7 @@ func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
WithContext,
)
}
func pairFromContextCancel(newCtx context.Context, cancelFct context.CancelFunc) ContextCancel {
return pair.MakePair(cancelFct, newCtx)
}

View File

@@ -13,6 +13,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides context-aware file operations that integrate with the ReaderIOResult monad.
// It offers safe, composable file I/O operations that respect context cancellation and properly
// manage resources using the RAII pattern.
//
// All operations in this package:
// - Respect context.Context for cancellation and timeouts
// - Return ReaderIOResult for composable error handling
// - Automatically manage resource cleanup
// - Are safe to use in concurrent environments
//
// # Example Usage
//
// // Read a file with automatic resource management
// readOp := ReadFile("data.txt")
// result := readOp(ctx)()
//
// // Open and manually manage a file
// fileOp := Open("config.json")
// fileResult := fileOp(ctx)()
package file
import (
@@ -29,32 +48,181 @@ import (
)
var (
// Open opens a file for reading within the given context
// Open opens a file for reading within the given context.
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or ReadFile.
//
// Parameters:
// - path: The path to the file to open
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that opens the file
//
// Example:
//
// openFile := Open("data.txt")
// result := openFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// // Use file...
// },
// )
//
// See Also:
// - ReadFile: For reading entire file contents with automatic resource management
// - Close: For closing file handles
Open = F.Flow3(
IOEF.Open,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name
// Create creates or truncates a file for writing within the given context.
// If the file already exists, it is truncated. If it doesn't exist, it is created
// with mode 0666 (before umask).
//
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or WriteFile.
//
// Parameters:
// - path: The path to the file to create or truncate
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that creates the file
//
// Example:
//
// createFile := Create("output.txt")
// result := createFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// f.WriteString("Hello, World!")
// },
// )
//
// See Also:
// - WriteFile: For writing data to a file with automatic resource management
// - Open: For opening files for reading
// - Close: For closing file handles
Create = F.Flow3(
IOEF.Create,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name.
// The operation returns the filename on success, allowing for easy composition
// with other file operations.
//
// Parameters:
// - name: The path to the file to remove
//
// Returns:
// - ReaderIOResult[string]: A computation that removes the file and returns its name
//
// Example:
//
// removeOp := Remove("temp.txt")
// result := removeOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Failed to remove: %v", err) },
// func(name string) { log.Printf("Removed: %s", name) },
// )
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading file contents
Remove = F.Flow2(
IOEF.Remove,
RIOE.FromIOEither[string],
)
)
// Close closes an object
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
// Close closes an io.Closer resource and returns a ReaderIOResult.
// This function is generic and works with any type that implements io.Closer,
// including os.File, network connections, and other closeable resources.
//
// The function captures any error that occurs during closing and returns it
// as part of the ReaderIOResult. On success, it returns Void (empty struct).
//
// Type Parameters:
// - C: Any type that implements io.Closer
//
// Parameters:
// - c: The resource to close
//
// Returns:
// - ReaderIOResult[Void]: A computation that closes the resource
//
// Example:
//
// file, _ := os.Open("data.txt")
// closeOp := Close(file)
// result := closeOp(ctx)()
//
// Note: This function is typically used with WithResource for automatic resource management
// rather than being called directly.
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading files with automatic closing
func Close[C io.Closer](c C) ReaderIOResult[Void] {
return F.Pipe2(
c,
IOEF.Close[C],
RIOE.FromIOEither[struct{}],
RIOE.FromIOEither[Void],
)
}
// ReadFile reads a file in the scope of a context
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
// ReadFile reads the entire contents of a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// The operation:
// - Opens the file for reading
// - Reads all contents into a byte slice
// - Automatically closes the file when done
// - Respects context cancellation during the read operation
//
// Parameters:
// - path: The path to the file to read
//
// Returns:
// - ReaderIOResult[[]byte]: A computation that reads the file contents
//
// Example:
//
// readOp := ReadFile("config.json")
// result := readOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Read error: %v", err) },
// func(data []byte) { log.Printf("Read %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// ReadFile(path) = WithResource(Open(path), Close)(readAllBytes)
//
// See Also:
// - Open: For opening files without automatic reading
// - Close: For closing file handles
// - WithResource: For custom resource management patterns
func ReadFile(path string) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) ReaderIOResult[[]byte] {
return func(ctx context.Context) IOE.IOEither[error, []byte] {
return func() ET.Either[error, []byte] {
return file.ReadAll(ctx, r)
@@ -62,3 +230,48 @@ func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
}
})
}
// WriteFile writes data to a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// If the file doesn't exist, it is created with mode 0666 (before umask).
// If the file already exists, it is truncated before writing.
//
// The operation:
// - Creates or truncates the file for writing
// - Writes all data to the file
// - Automatically closes the file when done
// - Respects context cancellation during the write operation
//
// Parameters:
// - data: The byte slice to write to the file
//
// Returns:
// - Kleisli[string, []byte]: A function that takes a file path and returns a computation
// that writes the data and returns the written bytes on success
//
// Example:
//
// writeOp := WriteFile([]byte("Hello, World!"))
// result := writeOp("output.txt")(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Write error: %v", err) },
// func(data []byte) { log.Printf("Wrote %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// WriteFile(data) = Create >> WriteAll(data) >> Close
//
// See Also:
// - ReadFile: For reading file contents with automatic resource management
// - Create: For creating files without automatic writing
// - WriteAll: For writing to an already-open file handle
func WriteFile(data []byte) Kleisli[string, []byte] {
return F.Flow2(
Create,
WriteAll[*os.File](data),
)
}

View File

@@ -18,11 +18,16 @@ package file
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
R "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
J "github.com/IBM/fp-go/v2/json"
"github.com/stretchr/testify/assert"
)
type RecordType struct {
@@ -49,3 +54,267 @@ func ExampleReadFile() {
// Output:
// Right[string](Carsten)
}
func TestCreate(t *testing.T) {
ctx := context.Background()
t.Run("Success - creates new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_create.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was created
_, err := os.Stat(tempFile)
assert.NoError(t, err)
// Clean up file handle
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
})
t.Run("Success - truncates existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_truncate.txt")
// Create file with initial content
err := os.WriteFile(tempFile, []byte("initial content"), 0644)
assert.NoError(t, err)
// Create should truncate
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Close the file
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
// Verify file was truncated
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
// Try to create file in non-existent directory
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
createOp := Create(invalidPath)
result := createOp(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - file can be written to", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Write to the file
E.MonadFold(result,
func(err error) *os.File { t.Fatalf("Unexpected error: %v", err); return nil },
func(f *os.File) *os.File {
defer f.Close()
_, err := f.WriteString("test content")
assert.NoError(t, err)
return f
},
)
// Verify content was written
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, "test content", string(content))
})
t.Run("Context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
createOp := Create(tempFile)
result := createOp(cancelCtx)()
// Note: File creation itself doesn't check context, but this tests the pattern
// In practice, context cancellation would affect subsequent operations
_ = result
})
}
func TestWriteFile(t *testing.T) {
ctx := context.Background()
t.Run("Success - writes data to new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
testData := []byte("Hello, World!")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify returned data
E.MonadFold(result,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("Success - overwrites existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_overwrite.txt")
// Write initial content
err := os.WriteFile(tempFile, []byte("old content"), 0644)
assert.NoError(t, err)
// Overwrite with new content
newData := []byte("new content")
writeOp := WriteFile(newData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was overwritten
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, newData, content)
})
t.Run("Success - writes empty data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_empty.txt")
emptyData := []byte{}
writeOp := WriteFile(emptyData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file is empty
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Success - writes large data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_large.txt")
largeData := make([]byte, 1024*1024) // 1MB
for i := range largeData {
largeData[i] = byte(i % 256)
}
writeOp := WriteFile(largeData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, largeData, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(invalidPath)(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - writes binary data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_binary.bin")
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
writeOp := WriteFile(binaryData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify binary content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, binaryData, content)
})
t.Run("Integration - write then read", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_roundtrip.txt")
testData := []byte("Round trip test data")
// Write data
writeOp := WriteFile(testData)
writeResult := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(writeResult))
// Read data back
readOp := ReadFile(tempFile)
readResult := readOp(ctx)()
assert.True(t, E.IsRight(readResult))
// Verify data matches
E.MonadFold(readResult,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
})
t.Run("Composition with Map", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_compose.txt")
testData := []byte("test data")
// Write and transform result
pipeline := F.Pipe1(
WriteFile(testData)(tempFile),
R.Map(func(data []byte) int { return len(data) }),
)
result := pipeline(ctx)()
assert.True(t, E.IsRight(result))
E.MonadFold(result,
func(err error) int { t.Fatalf("Unexpected error: %v", err); return 0 },
func(length int) int {
assert.Equal(t, len(testData), length)
return length
},
)
})
t.Run("Context cancellation during write", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(cancelCtx)()
// Note: The actual write may complete before cancellation is checked
// This test verifies the pattern works with cancelled contexts
_ = result
})
}

View File

@@ -38,7 +38,7 @@ var (
)
// CreateTemp created a temp file with proper parametrization
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
func CreateTemp(dir, pattern string) ReaderIOResult[*os.File] {
return F.Pipe2(
IOEF.CreateTemp(dir, pattern),
RIOE.FromIOEither[*os.File],
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
}
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
func WithTempFile[A any](f Kleisli[*os.File, A]) ReaderIOResult[A] {
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
}

View File

@@ -34,7 +34,7 @@ func TestWithTempFile(t *testing.T) {
func TestWithTempFileOnClosedFile(t *testing.T) {
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
res := WithTempFile(func(f *os.File) ReaderIOResult[[]byte] {
return F.Pipe2(
f,
onWriteAll[*os.File]([]byte("Carsten")),

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
)
type (
// ReaderIOResult represents a context-aware computation that performs side effects
// and can fail with an error. This is the main type used throughout the file package
// for all file operations.
//
// ReaderIOResult[A] is equivalent to:
// func(context.Context) func() Either[error, A]
//
// The computation:
// - Takes a context.Context for cancellation and timeouts
// - Performs side effects (IO operations)
// - Returns Either an error or a value of type A
//
// See Also:
// - readerioresult.ReaderIOResult: The underlying type definition
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Void represents the absence of a meaningful value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful result.
//
// Void is typically used as the success type in operations like Close that perform
// an action but don't produce a useful value.
//
// Example:
// Close[*os.File](file) // Returns ReaderIOResult[Void]
//
// See Also:
// - function.Void: The underlying type definition
Void = function.Void
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain and Bind operations.
//
// Kleisli[A, B] is equivalent to:
// func(A) ReaderIOResult[B]
//
// Example:
// // A Kleisli arrow that reads a file given its path
// var readFileK Kleisli[string, []byte] = ReadFile
//
// See Also:
// - readerioresult.Kleisli: The underlying type definition
// - Operator: For transforming ReaderIOResults
Kleisli[A, B any] = readerioresult.Kleisli[A, B]
// Operator represents a transformation from one ReaderIOResult to another.
// This is useful for point-free style composition and building reusable transformations.
//
// Operator[A, B] is equivalent to:
// func(ReaderIOResult[A]) ReaderIOResult[B]
//
// Operators are used to transform computations without executing them, enabling
// powerful composition patterns.
//
// Example:
// // An operator that maps over file contents
// var toUpper Operator[[]byte, string] = Map(func(data []byte) string {
// return strings.ToUpper(string(data))
// })
//
// See Also:
// - readerioresult.Operator: The underlying type definition
// - Kleisli: For functions that produce ReaderIOResults
Operator[A, B any] = readerioresult.Operator[A, B]
)

View File

@@ -23,8 +23,8 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
return func(w W) RIOE.ReaderIOResult[[]byte] {
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
return func(w W) ReaderIOResult[[]byte] {
return F.Pipe1(
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
return func() ([]byte, error) {
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte]
}
// WriteAll uses a generator function to create a stream, writes data to it and closes it
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
onWrite := onWriteAll[W](data)
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
return func(onCreate ReaderIOResult[W]) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](
onCreate,
Close[W])(
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]
}
// Write uses a generator function to create a stream, writes data to it and closes it
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
func Write[R any, W io.WriteCloser](acquire ReaderIOResult[W]) Kleisli[Kleisli[W, R], R] {
return RIOE.WithResource[R](
acquire,
Close[W])

View File

@@ -28,6 +28,7 @@ import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/logging"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
@@ -90,132 +91,7 @@ func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
return F.Bind2nd(withLoggingContextValue, any(lctx))
}
// LogEntryExitF creates a customizable operator that wraps a ReaderIOResult computation with entry/exit callbacks.
//
// This is a more flexible version of LogEntryExit that allows you to provide custom callbacks for
// entry and exit events. The onEntry callback receives the current context and can return a modified
// context (e.g., with additional logging information). The onExit callback receives the computation
// result and can perform custom logging, metrics collection, or cleanup.
//
// The function uses the bracket pattern to ensure that:
// - The onEntry callback is executed before the computation starts
// - The computation runs with the context returned by onEntry
// - The onExit callback is executed after the computation completes (success or failure)
// - The original result is preserved and returned unchanged
// - Cleanup happens even if the computation fails
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
// - ANY: The return type of the onExit callback (typically any)
//
// Parameters:
// - onEntry: A ReaderIO that receives the current context and returns a (possibly modified) context.
// This is executed before the computation starts. Use this for logging entry, adding context values,
// starting timers, or initialization logic.
// - onExit: A Kleisli function that receives the Result[A] and returns a ReaderIO[ANY].
// This is executed after the computation completes, regardless of success or failure.
// Use this for logging exit, recording metrics, cleanup, or finalization logic.
//
// Returns:
// - An Operator that wraps the ReaderIOResult computation with the custom entry/exit callbacks
//
// Example with custom context modification:
//
// type RequestID string
//
// logOp := LogEntryExitF[User, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// reqID := RequestID(uuid.New().String())
// log.Printf("[%s] Starting operation", reqID)
// return context.WithValue(ctx, "requestID", reqID)
// }
// },
// func(res Result[User]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// reqID := ctx.Value("requestID").(RequestID)
// return F.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// log.Printf("[%s] Operation failed: %v", reqID, err)
// return nil
// },
// func(_ User) any {
// log.Printf("[%s] Operation succeeded", reqID)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// wrapped := logOp(fetchUser(123))
//
// Example with metrics collection:
//
// import "github.com/prometheus/client_golang/prometheus"
//
// metricsOp := LogEntryExitF[Response, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// requestCount.WithLabelValues("api_call", "started").Inc()
// return context.WithValue(ctx, "startTime", time.Now())
// }
// },
// func(res Result[Response]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// startTime := ctx.Value("startTime").(time.Time)
// duration := time.Since(startTime).Seconds()
//
// return F.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// requestCount.WithLabelValues("api_call", "error").Inc()
// requestDuration.WithLabelValues("api_call", "error").Observe(duration)
// return nil
// },
// func(_ Response) any {
// requestCount.WithLabelValues("api_call", "success").Inc()
// requestDuration.WithLabelValues("api_call", "success").Observe(duration)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// Use Cases:
// - Custom context modification: Adding request IDs, trace IDs, or other context values
// - Structured logging: Integration with zap, logrus, or other structured loggers
// - Metrics collection: Recording operation durations, success/failure rates
// - Distributed tracing: OpenTelemetry, Jaeger integration
// - Custom monitoring: Application-specific monitoring and alerting
//
// Note: LogEntryExit is implemented using LogEntryExitF with standard logging and context management.
// Use LogEntryExitF when you need more control over the entry/exit behavior or context modification.
func LogEntryExitF[A, ANY any](
onEntry ReaderIO[context.Context],
onExit readerio.Kleisli[Result[A], ANY],
) Operator[A, A] {
bracket := F.Bind13of3(readerio.Bracket[context.Context, Result[A], ANY])(onEntry, func(newCtx context.Context, res Result[A]) ReaderIO[ANY] {
return readerio.FromIO(onExit(res)(newCtx)) // Get the exit callback for this result
})
return func(src ReaderIOResult[A]) ReaderIOResult[A] {
return bracket(F.Flow2(
src,
FromIOResult,
))
}
}
func noop() {}
// onEntry creates a ReaderIO that handles the entry logging for an operation.
// It generates a unique logging ID, captures the start time, and logs the entry message.
@@ -230,15 +106,15 @@ func LogEntryExitF[A, ANY any](
// - A ReaderIO that prepares the context with logging information and logs the entry
func onEntry(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
cb Reader[context.Context, *slog.Logger],
nameAttr slog.Attr,
) ReaderIO[context.Context] {
) ReaderIO[ContextCancel] {
return func(ctx context.Context) IO[context.Context] {
return func(ctx context.Context) IO[ContextCancel] {
// logger
logger := cb(ctx)
return func() context.Context {
return func() ContextCancel {
// check if the logger is enabled
if logger.Enabled(ctx, logLevel) {
// Generate unique logging ID and capture start time
@@ -258,19 +134,23 @@ func onEntry(
})
withLogger := logging.WithLogger(newLogger)
return withCtx(withLogger(ctx))
return F.Pipe2(
ctx,
withLogger,
pair.Map[context.CancelFunc](withCtx),
)
}
// logging disabled
withCtx := withLoggingContext(loggingContext{
logger: logger,
isEnabled: false,
})
return withCtx(ctx)
return pair.MakePair[context.CancelFunc](noop, withCtx(ctx))
}
}
}
// onExitAny creates a Kleisli function that handles exit logging for an operation.
// onExitVoid creates a Kleisli function that handles exit logging for an operation.
// It logs either success or error based on the Result, including the operation duration.
// Only logs if logging was enabled during entry (checked via loggingContext.isEnabled).
//
@@ -280,33 +160,33 @@ func onEntry(
//
// Returns:
// - A Kleisli function that logs the exit/error and returns nil
func onExitAny(
func onExitVoid(
logLevel slog.Level,
nameAttr slog.Attr,
) readerio.Kleisli[Result[any], any] {
return func(res Result[any]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
) readerio.Kleisli[Result[Void], Void] {
return func(res Result[Void]) ReaderIO[Void] {
return func(ctx context.Context) IO[Void] {
value := getLoggingContext(ctx)
if value.isEnabled {
return func() any {
return func() Void {
// Retrieve logging information from context
durationAttr := slog.Duration("duration", time.Since(value.startTime))
// Log error with ID and duration
onError := func(err error) any {
onError := func(err error) Void {
value.logger.LogAttrs(ctx, logLevel, "[throwing]",
nameAttr,
durationAttr,
slog.Any("error", err))
return nil
return F.VOID
}
// Log success with ID and duration
onSuccess := func(_ any) any {
onSuccess := func(v Void) Void {
value.logger.LogAttrs(ctx, logLevel, "[exiting ]", nameAttr, durationAttr)
return nil
return v
}
return F.Pipe1(
@@ -316,7 +196,7 @@ func onExitAny(
}
}
// nothing to do
return io.Of[any](nil)
return io.Of(F.VOID)
}
}
}
@@ -369,18 +249,26 @@ func onExitAny(
// )
func LogEntryExitWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
cb Reader[context.Context, *slog.Logger],
name string) Operator[A, A] {
nameAttr := slog.String("name", name)
return LogEntryExitF(
entry := F.Pipe1(
onEntry(logLevel, cb, nameAttr),
F.Flow2(
result.MapTo[A, any](nil),
onExitAny(logLevel, nameAttr),
),
readerio.LocalIOK[Result[A]],
)
exit := readerio.Tap(F.Flow2(
result.MapTo[A](F.VOID),
onExitVoid(logLevel, nameAttr),
))
return F.Flow2(
exit,
entry,
)
}
// LogEntryExit creates an operator that logs the entry and exit of a ReaderIOResult computation with timing and correlation IDs.
@@ -499,12 +387,12 @@ func LogEntryExit[A any](name string) Operator[A, A] {
func curriedLog(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) func(slog.Attr) func(context.Context) func() struct{} {
return F.Curry2(func(a slog.Attr, ctx context.Context) func() struct{} {
message string) func(slog.Attr) ReaderIO[Void] {
return F.Curry2(func(a slog.Attr, ctx context.Context) IO[Void] {
logger := cb(ctx)
return func() struct{} {
return func() Void {
logger.LogAttrs(ctx, logLevel, message, a)
return struct{}{}
return F.VOID
}
})
}
@@ -571,7 +459,7 @@ func curriedLog(
// - Conditional logging: Enable/disable logging based on logger configuration
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
cb Reader[context.Context, *slog.Logger],
message string) Kleisli[Result[A], A] {
return F.Pipe1(
@@ -582,18 +470,23 @@ func SLogWithCallback[A any](
curriedLog(logLevel, cb, message),
),
// preserve the original context
reader.Chain(reader.Sequence(readerio.MapTo[struct{}, Result[A]])),
reader.Chain(reader.Sequence(readerio.MapTo[Void, Result[A]])),
)
}
// SLog creates a Kleisli arrow that logs a Result value (success or error) with a message.
//
// This function logs both successful values and errors at Info level using the logger from the context.
// This function logs both successful values and errors at Info level. It retrieves the logger
// using logging.GetLoggerFromContext, which returns either:
// - The logger stored in the context via logging.WithLogger, or
// - The global logger (set via logging.SetLogger or slog.Default())
//
// It's a convenience wrapper around SLogWithCallback with standard settings.
//
// The logged output includes:
// - For success: The message with the value as a structured "value" attribute
// - For error: The message with the error as a structured "error" attribute
// The message parameter becomes the main log message text, and the Result value or error
// is attached as a structured logging attribute:
// - For success: Logs message with attribute value=<the actual value>
// - For error: Logs message with attribute error=<the error>
//
// The Result is passed through unchanged after logging, making this function transparent in the
// computation pipeline.
@@ -647,25 +540,47 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}
// TapSLog creates an operator that logs only successful values with a message and passes them through unchanged.
// TapSLog creates an operator that logs both successful values and errors with a message,
// and passes the ReaderIOResult through unchanged.
//
// This function is useful for debugging and monitoring values as they flow through a ReaderIOResult
// computation chain. Unlike SLog which logs both successes and errors, TapSLog only logs when the
// computation is successful. If the computation contains an error, no logging occurs and the error
// is propagated unchanged.
// computation chain. It logs both successful values and errors at Info level. It retrieves the logger
// using logging.GetLoggerFromContext, which returns either:
// - The logger stored in the context via logging.WithLogger, or
// - The global logger (set via logging.SetLogger or slog.Default())
//
// The logged output includes:
// - The provided message
// - The value being passed through (as a structured "value" attribute)
// The ReaderIOResult is returned unchanged after logging.
//
// The difference between TapSLog and SLog is their position in the pipeline:
// - SLog is a Kleisli[Result[A], A] used with Chain to intercept the raw Result
// - TapSLog is an Operator[A, A] used directly in a pipe on a ReaderIOResult[A]
//
// Both log the same information (success value or error), but TapSLog is more ergonomic
// when composing ReaderIOResult pipelines with F.Pipe.
//
// The message parameter becomes the main log message text, and the Result value or error
// is attached as a structured logging attribute:
// - For success: Logs message with attribute value=<the actual value>
// - For error: Logs message with attribute error=<the error>
//
// For example, TapSLog[User]("Fetched user") with a successful result produces:
//
// Log message: "Fetched user"
// Structured attribute: value={ID:123 Name:"Alice"}
//
// With an error result, it produces:
//
// Log message: "Fetched user"
// Structured attribute: error="user not found"
//
// Type Parameters:
// - A: The type of the value to log and pass through
// - A: The success type of the ReaderIOResult to log and pass through
//
// Parameters:
// - message: A descriptive message to include in the log entry
//
// Returns:
// - An Operator that logs successful values and returns them unchanged
// - An Operator that logs the Result (value or error) and returns the ReaderIOResult unchanged
//
// Example with simple value logging:
//
@@ -680,7 +595,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// )
//
// result := pipeline(t.Context())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Returns: result.Of("Alice")
//
// Example in a processing pipeline:
@@ -695,36 +610,36 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// )
//
// result := processOrder(t.Context())()
// // Logs each successful step with the intermediate values
// // If any step fails, subsequent TapSLog calls don't log
// // Logs each step with its value or error
//
// Example with error handling:
//
// pipeline := F.Pipe3(
// fetchData(id),
// TapSLog[Data]("Data fetched"),
// Chain(func(d Data) ReaderIOResult[Result] {
// Chain(func(d Data) ReaderIOResult[Data] {
// if d.IsValid() {
// return Of(processData(d))
// }
// return Left[Result](errors.New("invalid data"))
// return Left[Data](errors.New("invalid data"))
// }),
// TapSLog[Result]("Data processed"),
// TapSLog[Data]("Data processed"),
// )
//
// // If fetchData succeeds: logs "Data fetched" with the data
// // If processing succeeds: logs "Data processed" with the result
// // If processing fails: "Data processed" is NOT logged (error propagates)
// // If fetchData succeeds: logs "Data fetched" value={...}
// // If fetchData fails: logs "Data fetched" error="..."
// // If processing succeeds: logs "Data processed" value={...}
// // If processing fails: logs "Data processed" error="invalid data"
//
// Use Cases:
// - Debugging: Inspect intermediate successful values in a computation pipeline
// - Monitoring: Track successful data flow through complex operations
// - Troubleshooting: Identify where successful computations stop (last logged value before error)
// - Auditing: Log important successful values for compliance or security
// - Development: Understand data transformations during development
// - Debugging: Inspect intermediate values and errors in a computation pipeline
// - Monitoring: Track both successful and failed data flow through complex operations
// - Troubleshooting: Identify where errors are introduced or propagated
// - Auditing: Log important values and failures for compliance or security
// - Development: Understand data transformations and error paths during development
//
// Note: This function only logs successful values. Errors are silently propagated without logging.
// For logging both successes and errors, use SLog instead.
// Note: This function logs both successful values and errors. It is equivalent to SLog
// but expressed as an Operator for direct use in F.Pipe pipelines on ReaderIOResult values.
//
//go:inline
func TapSLog[A any](message string) Operator[A, A] {

View File

@@ -0,0 +1,415 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"bytes"
"errors"
"log/slog"
"strings"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/logging"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestTapSLogComprehensive_Success verifies TapSLog logs successful values
func TestTapSLogComprehensive_Success(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
t.Run("logs integer success value", func(t *testing.T) {
buf.Reset()
pipeline := F.Pipe2(
Of(42),
TapSLog[int]("Integer value"),
Map(N.Mul(2)),
)
res := pipeline(t.Context())()
// Verify result is correct
assert.Equal(t, 84, F.Pipe1(res, getOrZero))
// Verify logging occurred
logOutput := buf.String()
assert.Contains(t, logOutput, "Integer value", "Should log the message")
assert.Contains(t, logOutput, "value=42", "Should log the success value")
assert.NotContains(t, logOutput, "error", "Should not contain error keyword for success")
})
t.Run("logs string success value", func(t *testing.T) {
buf.Reset()
pipeline := F.Pipe1(
Of("hello world"),
TapSLog[string]("String value"),
)
res := pipeline(t.Context())()
// Verify result is correct
assert.True(t, F.Pipe1(res, isRight[string]))
// Verify logging occurred
logOutput := buf.String()
assert.Contains(t, logOutput, "String value")
assert.Contains(t, logOutput, `value="hello world"`)
})
t.Run("logs struct success value", func(t *testing.T) {
buf.Reset()
type User struct {
ID int
Name string
}
user := User{ID: 123, Name: "Alice"}
pipeline := F.Pipe1(
Of(user),
TapSLog[User]("User struct"),
)
res := pipeline(t.Context())()
// Verify result is correct
assert.True(t, F.Pipe1(res, isRight[User]))
// Verify logging occurred with struct fields
logOutput := buf.String()
assert.Contains(t, logOutput, "User struct")
assert.Contains(t, logOutput, "ID:123")
assert.Contains(t, logOutput, "Name:Alice")
})
t.Run("logs multiple success values in pipeline", func(t *testing.T) {
buf.Reset()
step1 := F.Pipe2(
Of(10),
TapSLog[int]("Initial value"),
Map(N.Mul(2)),
)
pipeline := F.Pipe2(
step1,
TapSLog[int]("After doubling"),
Map(N.Add(5)),
)
res := pipeline(t.Context())()
// Verify result is correct
assert.Equal(t, 25, getOrZero(res))
// Verify both log entries
logOutput := buf.String()
assert.Contains(t, logOutput, "Initial value")
assert.Contains(t, logOutput, "value=10")
assert.Contains(t, logOutput, "After doubling")
assert.Contains(t, logOutput, "value=20")
})
}
// TestTapSLogComprehensive_Error verifies TapSLog behavior with errors
func TestTapSLogComprehensive_Error(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
t.Run("logs error values", func(t *testing.T) {
buf.Reset()
testErr := errors.New("test error")
pipeline := F.Pipe2(
Left[int](testErr),
TapSLog[int]("Error case"),
Map(N.Mul(2)),
)
res := pipeline(t.Context())()
// Verify error is preserved
assert.True(t, F.Pipe1(res, isLeft[int]))
// Verify logging occurred for error
logOutput := buf.String()
assert.Contains(t, logOutput, "Error case", "Should log the message")
assert.Contains(t, logOutput, "error", "Should contain error keyword")
assert.Contains(t, logOutput, "test error", "Should log the error message")
assert.NotContains(t, logOutput, "value=", "Should not log 'value=' for errors")
})
t.Run("preserves error through pipeline", func(t *testing.T) {
buf.Reset()
originalErr := errors.New("original error")
step1 := F.Pipe2(
Left[int](originalErr),
TapSLog[int]("First tap"),
Map(N.Mul(2)),
)
pipeline := F.Pipe2(
step1,
TapSLog[int]("Second tap"),
Map(N.Add(5)),
)
res := pipeline(t.Context())()
// Verify error is preserved
assert.True(t, isLeft(res))
// Verify both taps logged the error
logOutput := buf.String()
errorCount := strings.Count(logOutput, "original error")
assert.Equal(t, 2, errorCount, "Both TapSLog calls should log the error")
assert.Contains(t, logOutput, "First tap")
assert.Contains(t, logOutput, "Second tap")
})
t.Run("logs error after successful operation", func(t *testing.T) {
buf.Reset()
pipeline := F.Pipe3(
Of(10),
TapSLog[int]("Before error"),
Chain(func(n int) ReaderIOResult[int] {
return Left[int](errors.New("chain error"))
}),
TapSLog[int]("After error"),
)
res := pipeline(t.Context())()
// Verify error is present
assert.True(t, F.Pipe1(res, isLeft[int]))
// Verify both logs
logOutput := buf.String()
assert.Contains(t, logOutput, "Before error")
assert.Contains(t, logOutput, "value=10")
assert.Contains(t, logOutput, "After error")
assert.Contains(t, logOutput, "chain error")
})
}
// TestTapSLogComprehensive_EdgeCases verifies TapSLog with edge cases
func TestTapSLogComprehensive_EdgeCases(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
t.Run("logs zero value", func(t *testing.T) {
buf.Reset()
pipeline := F.Pipe1(
Of(0),
TapSLog[int]("Zero value"),
)
res := pipeline(t.Context())()
assert.Equal(t, 0, F.Pipe1(res, getOrZero))
logOutput := buf.String()
assert.Contains(t, logOutput, "Zero value")
assert.Contains(t, logOutput, "value=0")
})
t.Run("logs empty string", func(t *testing.T) {
buf.Reset()
pipeline := F.Pipe1(
Of(""),
TapSLog[string]("Empty string"),
)
res := pipeline(t.Context())()
assert.True(t, F.Pipe1(res, isRight[string]))
logOutput := buf.String()
assert.Contains(t, logOutput, "Empty string")
assert.Contains(t, logOutput, `value=""`)
})
t.Run("logs nil pointer", func(t *testing.T) {
buf.Reset()
type Data struct {
Value string
}
var nilData *Data
pipeline := F.Pipe1(
Of(nilData),
TapSLog[*Data]("Nil pointer"),
)
res := pipeline(t.Context())()
assert.True(t, F.Pipe1(res, isRight[*Data]))
logOutput := buf.String()
assert.Contains(t, logOutput, "Nil pointer")
// Nil representation may vary, but should be logged
assert.NotEmpty(t, logOutput)
})
t.Run("respects logger level - disabled", func(t *testing.T) {
buf.Reset()
// Create logger that only logs errors
errorLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelError,
}))
oldLogger := logging.SetLogger(errorLogger)
defer logging.SetLogger(oldLogger)
pipeline := F.Pipe1(
Of(42),
TapSLog[int]("Should not log"),
)
res := pipeline(t.Context())()
assert.Equal(t, 42, F.Pipe1(res, getOrZero))
// Should have no logs since level is ERROR
logOutput := buf.String()
assert.Empty(t, logOutput, "Should not log when level is disabled")
})
}
// TestTapSLogComprehensive_Integration verifies TapSLog in realistic scenarios
func TestTapSLogComprehensive_Integration(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
t.Run("complex pipeline with mixed success and error", func(t *testing.T) {
buf.Reset()
// Simulate a data processing pipeline
validatePositive := func(n int) ReaderIOResult[int] {
if n > 0 {
return Of(n)
}
return Left[int](errors.New("number must be positive"))
}
step1 := F.Pipe3(
Of(5),
TapSLog[int]("Input received"),
Map(N.Mul(2)),
TapSLog[int]("After multiplication"),
)
pipeline := F.Pipe2(
step1,
Chain(validatePositive),
TapSLog[int]("After validation"),
)
res := pipeline(t.Context())()
assert.Equal(t, 10, getOrZero(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "Input received")
assert.Contains(t, logOutput, "value=5")
assert.Contains(t, logOutput, "After multiplication")
assert.Contains(t, logOutput, "value=10")
assert.Contains(t, logOutput, "After validation")
assert.Contains(t, logOutput, "value=10")
})
t.Run("error propagation with logging", func(t *testing.T) {
buf.Reset()
validatePositive := func(n int) ReaderIOResult[int] {
if n > 0 {
return Of(n)
}
return Left[int](errors.New("number must be positive"))
}
step1 := F.Pipe3(
Of(-5),
TapSLog[int]("Input received"),
Map(N.Mul(2)),
TapSLog[int]("After multiplication"),
)
pipeline := F.Pipe2(
step1,
Chain(validatePositive),
TapSLog[int]("After validation"),
)
res := pipeline(t.Context())()
assert.True(t, isLeft(res))
logOutput := buf.String()
// First two taps should log success
assert.Contains(t, logOutput, "Input received")
assert.Contains(t, logOutput, "value=-5")
assert.Contains(t, logOutput, "After multiplication")
assert.Contains(t, logOutput, "value=-10")
// Last tap should log error
assert.Contains(t, logOutput, "After validation")
assert.Contains(t, logOutput, "number must be positive")
})
}
// Helper functions for tests
func getOrZero(res Result[int]) int {
val, err := result.Unwrap(res)
if err == nil {
return val
}
return 0
}
func isRight[A any](res Result[A]) bool {
return result.IsRight(res)
}
func isLeft[A any](res Result[A]) bool {
return result.IsLeft(res)
}

View File

@@ -13,6 +13,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/logging"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
@@ -53,6 +54,11 @@ func TestLogEntryExitSuccess(t *testing.T) {
assert.Contains(t, logOutput, "TestOperation")
assert.Contains(t, logOutput, "ID=")
assert.Contains(t, logOutput, "duration=")
// Verify entry log appears before exit log
enteringIdx := strings.Index(logOutput, "[entering]")
exitingIdx := strings.Index(logOutput, "[exiting ]")
assert.Greater(t, exitingIdx, enteringIdx, "Exit log should appear after entry log")
}
// TestLogEntryExitError tests error operation logging
@@ -81,6 +87,11 @@ func TestLogEntryExitError(t *testing.T) {
assert.Contains(t, logOutput, "test error")
assert.Contains(t, logOutput, "ID=")
assert.Contains(t, logOutput, "duration=")
// Verify entry log appears before error log
enteringIdx := strings.Index(logOutput, "[entering]")
throwingIdx := strings.Index(logOutput, "[throwing]")
assert.Greater(t, throwingIdx, enteringIdx, "Error log should appear after entry log")
}
// TestLogEntryExitNested tests nested operations with different IDs
@@ -119,6 +130,48 @@ func TestLogEntryExitNested(t *testing.T) {
exitCount := strings.Count(logOutput, "[exiting ]")
assert.Equal(t, 2, enterCount, "Should have 2 entering logs")
assert.Equal(t, 2, exitCount, "Should have 2 exiting logs")
// Verify log ordering: Each operation logs entry before exit
// Note: Due to Chain semantics, OuterOp completes before InnerOp starts
lines := strings.Split(logOutput, "\n")
var logSequence []string
for _, line := range lines {
if strings.Contains(line, "OuterOp") && strings.Contains(line, "[entering]") {
logSequence = append(logSequence, "OuterOp-entering")
} else if strings.Contains(line, "OuterOp") && strings.Contains(line, "[exiting ]") {
logSequence = append(logSequence, "OuterOp-exiting")
} else if strings.Contains(line, "InnerOp") && strings.Contains(line, "[entering]") {
logSequence = append(logSequence, "InnerOp-entering")
} else if strings.Contains(line, "InnerOp") && strings.Contains(line, "[exiting ]") {
logSequence = append(logSequence, "InnerOp-exiting")
}
}
// Verify each operation's entry comes before its exit
assert.Equal(t, 4, len(logSequence), "Should have 4 log entries")
// Find indices
outerEnterIdx := -1
outerExitIdx := -1
innerEnterIdx := -1
innerExitIdx := -1
for i, log := range logSequence {
switch log {
case "OuterOp-entering":
outerEnterIdx = i
case "OuterOp-exiting":
outerExitIdx = i
case "InnerOp-entering":
innerEnterIdx = i
case "InnerOp-exiting":
innerExitIdx = i
}
}
// Verify entry before exit for each operation
assert.Greater(t, outerExitIdx, outerEnterIdx, "OuterOp exit should come after OuterOp entry")
assert.Greater(t, innerExitIdx, innerEnterIdx, "InnerOp exit should come after InnerOp entry")
}
// TestLogEntryExitWithCallback tests custom log level and callback
@@ -172,76 +225,6 @@ func TestLogEntryExitDisabled(t *testing.T) {
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
}
// TestLogEntryExitF tests custom entry/exit callbacks
func TestLogEntryExitF(t *testing.T) {
var entryCount, exitCount int
onEntry := func(ctx context.Context) IO[context.Context] {
return func() context.Context {
entryCount++
return ctx
}
}
onExit := func(res Result[string]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
return func() any {
exitCount++
return nil
}
}
}
operation := F.Pipe1(
Of("test"),
LogEntryExitF(onEntry, onExit),
)
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
}
// TestLogEntryExitFWithError tests custom callbacks with error
func TestLogEntryExitFWithError(t *testing.T) {
var entryCount, exitCount int
var capturedError error
onEntry := func(ctx context.Context) IO[context.Context] {
return func() context.Context {
entryCount++
return ctx
}
}
onExit := func(res Result[string]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
return func() any {
exitCount++
if result.IsLeft(res) {
_, capturedError = result.Unwrap(res)
}
return nil
}
}
}
testErr := errors.New("custom error")
operation := F.Pipe1(
Left[string](testErr),
LogEntryExitF(onEntry, onExit),
)
res := operation(t.Context())()
assert.True(t, result.IsLeft(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
assert.Equal(t, testErr, capturedError, "Should capture the error")
}
// TestLoggingIDUniqueness tests that logging IDs are unique
func TestLoggingIDUniqueness(t *testing.T) {
var buf bytes.Buffer
@@ -287,7 +270,8 @@ func TestLogEntryExitWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(t.Context())
cancelFct, ctx := pair.Unpack(logging.WithLogger(contextLogger)(t.Context()))
defer cancelFct()
operation := F.Pipe1(
Of("context value"),
@@ -546,7 +530,8 @@ func TestTapSLogWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(t.Context())
cancelFct, ctx := pair.Unpack(logging.WithLogger(contextLogger)(t.Context()))
defer cancelFct()
operation := F.Pipe2(
Of("test value"),
@@ -660,3 +645,138 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
assert.Contains(t, logOutput, "warning error")
assert.Contains(t, logOutput, "level=WARN")
}
// TestTapSLogPreservesResult tests that TapSLog doesn't modify the result
func TestTapSLogPreservesResult(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
// Test with success value
successOp := F.Pipe2(
Of(42),
TapSLog[int]("Success value"),
Map(N.Mul(2)),
)
successRes := successOp(t.Context())()
assert.Equal(t, result.Of(84), successRes)
// Test with error value
testErr := errors.New("test error")
errorOp := F.Pipe2(
Left[int](testErr),
TapSLog[int]("Error value"),
Map(N.Mul(2)),
)
errorRes := errorOp(t.Context())()
assert.True(t, result.IsLeft(errorRes))
// Verify the error is preserved
_, err := result.Unwrap(errorRes)
assert.Equal(t, testErr, err)
}
// TestTapSLogChainBehavior tests that TapSLog properly chains with other operations
func TestTapSLogChainBehavior(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
// Create a pipeline with multiple TapSLog calls
step1 := F.Pipe2(
Of(1),
TapSLog[int]("Step 1"),
Map(N.Mul(2)),
)
step2 := F.Pipe2(
step1,
TapSLog[int]("Step 2"),
Map(N.Mul(3)),
)
pipeline := F.Pipe1(
step2,
TapSLog[int]("Step 3"),
)
res := pipeline(t.Context())()
assert.Equal(t, result.Of(6), res)
logOutput := buf.String()
// Verify all steps were logged
assert.Contains(t, logOutput, "Step 1")
assert.Contains(t, logOutput, "value=1")
assert.Contains(t, logOutput, "Step 2")
assert.Contains(t, logOutput, "value=2")
assert.Contains(t, logOutput, "Step 3")
assert.Contains(t, logOutput, "value=6")
}
// TestTapSLogWithNilValue tests TapSLog with nil pointer values
func TestTapSLogWithNilValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
type Data struct {
Value string
}
// Test with nil pointer
var nilData *Data
operation := F.Pipe1(
Of(nilData),
TapSLog[*Data]("Nil pointer value"),
)
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "Nil pointer value")
// The exact representation of nil may vary, but it should be logged
assert.NotEmpty(t, logOutput)
}
// TestTapSLogLogsErrors verifies that TapSLog DOES log errors
// TapSLog uses SLog internally, which logs both success values and errors
func TestTapSLogLogsErrors(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
testErr := errors.New("test error message")
pipeline := F.Pipe2(
Left[int](testErr),
TapSLog[int]("Error logging test"),
Map(N.Mul(2)),
)
res := pipeline(t.Context())()
// Verify the error is preserved
assert.True(t, result.IsLeft(res))
// Verify logging occurred for the error
logOutput := buf.String()
assert.NotEmpty(t, logOutput, "TapSLog should log when the Result is an error")
assert.Contains(t, logOutput, "Error logging test")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "test error message")
}

View File

@@ -19,6 +19,11 @@ import (
"context"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/pair"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
@@ -34,21 +39,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 ReaderIOResult
// - 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 ReaderIOResult[A] and returns a ReaderIOResult[B]
// - A Kleisli arrow that takes a ReaderIOResult[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) RIOR.Kleisli[R, ReaderIOResult[A], B] {
return function.Flow2(
Local[A](f),
Map(g),
RIOR.Map[R](g),
)
}
@@ -62,14 +70,168 @@ 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:
// - A Kleisli arrow that takes a ReaderIOResult[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, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIOR.Kleisli[R, ReaderIOResult[A], A] {
return Local[A](f)
}
// ContramapIOK changes the context during the execution of a ReaderIOResult using an IO effect.
// This is the contravariant functor operation with IO effects.
//
// ContramapIOK is an alias for LocalIOK and is useful for adapting a ReaderIOResult to work with
// a modified context when the transformation itself requires side effects.
//
// Type Parameters:
// - A: The success type (unchanged)
//
// Parameters:
// - f: An IO Kleisli arrow that transforms the context with side effects
//
// Returns:
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
//
// See Also:
// - Contramap: For pure context transformations
// - LocalIOK: The underlying implementation
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return Local[A](f)
func ContramapIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOK[A](f)
}
// LocalIOK transforms the context using an IO-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation itself is wrapped in an IO effect.
//
// The function f takes a context and returns an IO effect that produces a ContextCancel
// (a pair of CancelFunc and the new Context). This allows the context transformation to
// perform side effects.
//
// # Use Cases
//
// This function is useful for sharing information via the Context that is computed through
// side effects that cannot fail, such as:
// - Generating unique request IDs or trace IDs
// - Recording timestamps or metrics
// - Logging context information
// - Computing derived values from existing context data
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IO-based Kleisli function that transforms the context
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Generate a request ID via side effect and add to context
// addRequestID := func(ctx context.Context) io.IO[ContextCancel] {
// return func() ContextCancel {
// // Side effect: generate unique ID
// requestID := uuid.New().String()
// // Share the ID via context
// newCtx := context.WithValue(ctx, "requestID", requestID)
// return pair.MakePair(func() {}, newCtx)
// }
// }
// adapted := LocalIOK[int](addRequestID)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOResultK: For context transformations that can fail
//
//go:inline
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOResultK[A](function.Flow2(f, ioresult.FromIO))
}
// LocalIOResultK transforms the context using an IOResult-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation can fail with an error.
//
// The function f takes a context and returns an IOResult that produces either an error or a ContextCancel
// (a pair of CancelFunc and the new Context). If the transformation fails, the error is propagated
// and the original ReaderIOResult is not executed.
//
// # Use Cases
//
// This function is particularly useful for sharing information via the Context that is computed
// through side effects, such as:
// - Loading configuration from a file or database
// - Fetching authentication tokens from an external service
// - Computing derived values that require I/O operations
// - Validating and enriching context with data from external sources
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IOResult-based Kleisli function that transforms the context and may fail
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Load configuration via side effect and add to context
// loadConfig := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
// return func() result.Result[ContextCancel] {
// // Side effect: read from file system
// config, err := os.ReadFile("config.json")
// if err != nil {
// return result.Left[ContextCancel](err)
// }
// // Share the loaded config via context
// newCtx := context.WithValue(ctx, "config", config)
// return result.Of(pair.MakePair(func() {}, newCtx))
// }
// }
// adapted := LocalIOResultK[int](loadConfig)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOK: For context transformations with side effects that cannot fail
func LocalIOResultK[A any](f ioresult.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
p, err := result.Unwrap(f(ctx)())
if err != nil {
return result.Left[A](err)
}
// unwrap
otherCancel, otherCtx := pair.Unpack(p)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}

View File

@@ -20,6 +20,10 @@ import (
"strconv"
"testing"
"github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -36,9 +40,9 @@ func TestPromapBasic(t *testing.T) {
}
}
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
@@ -61,9 +65,9 @@ func TestContramapBasic(t *testing.T) {
}
}
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)
@@ -73,6 +77,105 @@ func TestContramapBasic(t *testing.T) {
})
}
// TestContramapIOK tests ContramapIOK functionality
func TestContramapIOK(t *testing.T) {
t.Run("transforms context with IO effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("requestID"); v != nil {
return R.Of(v.(string))
}
return R.Of("no-id")
}
}
addRequestID := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
// Simulate generating a request ID via side effect
requestID := "req-12345"
newCtx := context.WithValue(ctx, "requestID", requestID)
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := ContramapIOK[string](addRequestID)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("req-12345"), result)
})
t.Run("preserves value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("counter"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addCounter := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "counter", 999)
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := ContramapIOK[int](addCounter)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(999), result)
})
t.Run("calls cancel function", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addData := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "data", "value")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return pair.MakePair(cancelFunc, newCtx)
}
}
adapted := ContramapIOK[string](addData)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
t.Run("handles cancelled context", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addData := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "data", "value")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := ContramapIOK[string](addData)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalBasic tests basic Local functionality
func TestLocalBasic(t *testing.T) {
t.Run("adds value to context", func(t *testing.T) {
@@ -85,9 +188,9 @@ func TestLocalBasic(t *testing.T) {
}
}
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)
@@ -96,3 +199,311 @@ func TestLocalBasic(t *testing.T) {
assert.Equal(t, R.Of("Alice"), result)
})
}
// TestLocalIOK_Success tests LocalIOK with successful context transformation
func TestLocalIOK_Success(t *testing.T) {
t.Run("transforms context with IO effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Bob")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("Bob"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("count"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addCount := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "count", 42)
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[int](addCount)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(42), result)
})
}
// TestLocalIOK_CancelledContext tests LocalIOK with cancelled context
func TestLocalIOK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Charlie")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Dave")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return pair.MakePair(cancelFunc, newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
}
// TestLocalIOResultK_Success tests LocalIOResultK with successful context transformation
func TestLocalIOResultK_Success(t *testing.T) {
t.Run("transforms context with IOResult effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("role"); v != nil {
return R.Of(v.(string))
}
return R.Of("guest")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "admin")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("admin"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("score"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addScore := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "score", 100)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[int](addScore)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalIOResultK_Failure tests LocalIOResultK with failed context transformation
func TestLocalIOResultK_Failure(t *testing.T) {
t.Run("propagates transformation error", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
result := adapted(t.Context())()
assert.True(t, R.IsLeft(result))
_, err := R.UnwrapError(result)
assert.Equal(t, assert.AnError, err)
})
t.Run("does not execute original computation on transformation failure", func(t *testing.T) {
executed := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
executed = true
return R.Of("should not execute")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, executed, "original computation should not execute")
})
}
// TestLocalIOResultK_CancelledContext tests LocalIOResultK with cancelled context
func TestLocalIOResultK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOResultK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOResultK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after successful execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return R.Of(pair.MakePair(cancelFunc, newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
t.Run("does not call cancel function on transformation failure", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
_ = cancelFunc // avoid unused warning
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, cancelCalled, "cancel function should not be called on failure")
})
}
// TestLocalIOResultK_Integration tests integration with other operations
func TestLocalIOResultK_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("value"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addValue := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "value", 10)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
double := func(x int) int { return x * 2 }
adapted := F.Flow2(
LocalIOResultK[int](addValue),
Map(double),
)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(20), result)
})
}

View File

@@ -28,10 +28,10 @@ import (
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
const (
@@ -1010,12 +1010,15 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
// - R: The input environment type that f transforms into context.Context
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
// - f: A function that transforms the input environment R into context.Context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
// - A Kleisli arrow that runs the computation with the transformed context
//
// Note: When R is context.Context, this simplifies to an Operator[A, A]
//
// Example:
//
@@ -1025,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 {
@@ -1046,27 +1049,19 @@ 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(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)()
}
}
}
//
//go:inline
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIOR.Kleisli[R, ReaderIOResult[A], A] {
return readerio.Local[Result[A]](f)
}
// WithTimeout adds a timeout to the context for a ReaderIOResult computation.
@@ -1123,9 +1118,10 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// )
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
return Local[A](
func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithTimeout(ctx, timeout))
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
@@ -1188,7 +1184,7 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// )
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
return Local[A](func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithDeadline(ctx, deadline))
})
}

View File

@@ -55,6 +55,10 @@ type (
// Either[A] is equivalent to Either[error, A] from the either package.
Either[A any] = either.Either[error, A]
// Result represents a computation that can either succeed with a value of type A
// or fail with an error. This is an alias for result.Result[A].
//
// Result[A] is equivalent to Either[error, A]
Result[A any] = result.Result[A]
// Lazy represents a deferred computation that produces a value of type A when executed.
@@ -73,6 +77,10 @@ type (
// IOEither[A] is equivalent to func() Either[error, A]
IOEither[A any] = ioeither.IOEither[error, A]
// IOResult represents a side-effectful computation that can fail with an error.
// This combines IO (side effects) with Result (error handling).
//
// IOResult[A] is equivalent to func() Result[A]
IOResult[A any] = ioresult.IOResult[A]
// Reader represents a computation that depends on a context of type R.
@@ -118,6 +126,13 @@ type (
// result := fetchUser("123")(ctx)()
ReaderIOResult[A any] = RIOR.ReaderIOResult[context.Context, A]
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain operations.
//
// Kleisli[A, B] is equivalent to func(A) ReaderIOResult[B]
Kleisli[A, B any] = reader.Reader[A, ReaderIOResult[B]]
// Operator represents a transformation from one ReaderIOResult to another.
@@ -133,26 +148,76 @@ type (
// result := toUpper(computation)
Operator[A, B any] = Kleisli[ReaderIOResult[A], B]
ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// ReaderResult represents a context-dependent computation that can fail.
// This is specialized to use context.Context as the context type.
//
// ReaderResult[A] is equivalent to func(context.Context) Result[A]
ReaderResult[A any] = readerresult.ReaderResult[A]
// ReaderEither represents a context-dependent computation that can fail.
// It takes a context of type R and produces an Either[E, A].
//
// ReaderEither[R, E, A] is equivalent to func(R) Either[E, A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
// ReaderOption represents a context-dependent computation that may not produce a value.
// It takes a context of type R and produces an Option[A].
//
// ReaderOption[R, A] is equivalent to func(R) Option[A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// Endomorphism represents a function from a type to itself.
// It is used for transformations that preserve the type.
//
// Endomorphism[A] is equivalent to func(A) A
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Consumer represents a function that consumes a value without producing a result.
// It is used for side effects like logging or updating state.
//
// Consumer[A] is equivalent to func(A)
Consumer[A any] = consumer.Consumer[A]
// Prism represents an optic for working with sum types (tagged unions).
// It provides a way to focus on a specific variant of a sum type.
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
// Lens represents an optic for working with product types (records/structs).
// It provides a way to focus on a specific field of a product type.
Lens[S, T any] = lens.Lens[S, T]
// Trampoline represents a computation that can be executed in a stack-safe manner.
// It is used for tail-recursive computations that would otherwise overflow the stack.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
// Predicate represents a function that tests a value of type A.
// It returns true if the value satisfies the predicate, false otherwise.
//
// Predicate[A] is equivalent to func(A) bool
Predicate[A any] = predicate.Predicate[A]
// Pair represents a tuple of two values of types A and B.
// It is used to group two related values together.
Pair[A, B any] = pair.Pair[A, B]
// IORef represents a mutable reference that can be safely accessed in IO computations.
// It provides thread-safe read and write operations.
IORef[A any] = ioref.IORef[A]
// State represents a stateful computation that transforms a state of type S
// and produces a value of type A.
//
// State[S, A] is equivalent to func(S) Pair[A, S]
State[S, A any] = state.State[S, A]
// Void represents the absence of a value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful value.
Void = function.Void
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

View File

@@ -0,0 +1,213 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioresult"
)
// Eitherize converts a function that returns a value and error into a ReaderReaderIOResult.
//
// This function takes a function that accepts an outer context R and context.Context,
// returning a value T and an error, and converts it into a ReaderReaderIOResult[R, T].
// The error is automatically converted into the Left case of the Result, while successful
// values become the Right case.
//
// This is particularly useful for integrating standard Go error-handling patterns into
// the functional programming style of ReaderReaderIOResult. It is especially helpful
// for adapting interface member functions that accept a context. When you have an
// interface method with signature (receiver, context.Context) (T, error), you can
// use Eitherize to convert it into a ReaderReaderIOResult where the receiver becomes
// the outer reader context R.
//
// # Type Parameters
//
// - R: The outer reader context type (e.g., application configuration)
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes R and context.Context and returns (T, error)
//
// # Returns
//
// - ReaderReaderIOResult[R, T]: A computation that depends on R and context.Context,
// performs IO, and produces a Result[T]
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
// // Implementation that may return an error
// return &User{ID: 1, Name: "Alice"}, nil
// }
//
// // Convert to ReaderReaderIOResult
// fetchUserRR := Eitherize(fetchUser)
//
// // Use in functional composition
// result := F.Pipe1(
// fetchUserRR,
// Map[AppConfig](func(u *User) string { return u.Name }),
// )
//
// // Execute with config and context
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// outcome := result(cfg)(context.Background())()
//
// # Adapting Interface Methods
//
// Eitherize is particularly useful for adapting interface member functions:
//
// type UserRepository interface {
// GetUser(ctx context.Context, id int) (*User, error)
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method by binding the first parameter (receiver)
// repo := &UserRepo{db: db}
// getUserRR := Eitherize(func(id int, ctx context.Context) (*User, error) {
// return repo.GetUser(ctx, id)
// })
//
// // Now getUserRR has type: ReaderReaderIOResult[int, *User]
// // The receiver (repo) is captured in the closure
// // The id becomes the outer reader context R
//
// # See Also
//
// - Eitherize1: For functions that take an additional parameter
// - ioresult.Eitherize2: The underlying conversion function
func Eitherize[R, T any](f func(R, context.Context) (T, error)) ReaderReaderIOResult[R, T] {
return F.Pipe1(
ioresult.Eitherize2(f),
F.Curry2,
)
}
// Eitherize1 converts a function that takes an additional parameter and returns a value
// and error into a Kleisli arrow.
//
// This function takes a function that accepts an outer context R, context.Context, and
// an additional parameter A, returning a value T and an error, and converts it into a
// Kleisli arrow (A -> ReaderReaderIOResult[R, T]). The error is automatically converted
// into the Left case of the Result, while successful values become the Right case.
//
// This is useful for creating composable operations that depend on both contexts and
// an input value, following standard Go error-handling patterns. It is especially helpful
// for adapting interface member functions that accept a context and additional parameters.
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
// the outer reader context R and A becomes the input parameter.
//
// # Type Parameters
//
// - R: The outer reader context type (e.g., application configuration)
// - A: The input parameter type
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes R, context.Context, and A, returning (T, error)
//
// # Returns
//
// - Kleisli[R, A, T]: A function from A to ReaderReaderIOResult[R, T]
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
// // Implementation that may return an error
// return &User{ID: id, Name: "Alice"}, nil
// }
//
// // Convert to Kleisli arrow
// fetchUserKleisli := Eitherize1(fetchUserByID)
//
// // Use in functional composition with Chain
// pipeline := F.Pipe1(
// Of[AppConfig](123),
// Chain[AppConfig](fetchUserKleisli),
// )
//
// // Execute with config and context
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// outcome := pipeline(cfg)(context.Background())()
//
// # Adapting Interface Methods
//
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
//
// type UserRepository interface {
// GetUserByID(ctx context.Context, id int) (*User, error)
// UpdateUser(ctx context.Context, user *User) error
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method - receiver becomes R, id becomes A
// repo := &UserRepo{db: db}
// getUserKleisli := Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
// return r.GetUserByID(ctx, id)
// })
//
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
// // Which is: func(int) ReaderReaderIOResult[*UserRepo, *User]
// // Use it in composition:
// pipeline := F.Pipe1(
// Of[*UserRepo](123),
// Chain[*UserRepo](getUserKleisli),
// )
// result := pipeline(repo)(context.Background())()
//
// # See Also
//
// - Eitherize: For functions without an additional parameter
// - Chain: For composing Kleisli arrows
// - ioresult.Eitherize3: The underlying conversion function
func Eitherize1[R, A, T any](f func(R, context.Context, A) (T, error)) Kleisli[R, A, T] {
return F.Flow2(
F.Bind3of3(ioresult.Eitherize3(f)),
F.Curry2,
)
}

View File

@@ -0,0 +1,507 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type TestConfig struct {
Prefix string
MaxLen int
}
var testConfig = TestConfig{
Prefix: "test",
MaxLen: 100,
}
// TestEitherize_Success tests successful conversion with Eitherize
func TestEitherize_Success(t *testing.T) {
t.Run("converts successful function to ReaderReaderIOResult", func(t *testing.T) {
// Arrange
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix + "-success", nil
}
rr := Eitherize(successFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("test-success"), outcome)
})
t.Run("preserves context values", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("testKey")
expectedValue := "contextValue"
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
value := ctx.Value(key)
if value == nil {
return "", errors.New("context value not found")
}
return value.(string), nil
}
rr := Eitherize(contextFunc)
ctx := context.WithValue(context.Background(), key, expectedValue)
// Act
outcome := rr(testConfig)(ctx)()
// Assert
assert.Equal(t, result.Of(expectedValue), outcome)
})
t.Run("works with different types", func(t *testing.T) {
// Arrange
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.MaxLen, nil
}
rr := Eitherize(intFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(100), outcome)
})
}
// TestEitherize_Failure tests error handling with Eitherize
func TestEitherize_Failure(t *testing.T) {
t.Run("converts error to Left", func(t *testing.T) {
// Arrange
expectedErr := errors.New("operation failed")
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return "", expectedErr
}
rr := Eitherize(failFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
assert.Equal(t, result.Left[string](expectedErr), outcome)
})
t.Run("preserves error message", func(t *testing.T) {
// Arrange
expectedErr := fmt.Errorf("validation error: field is required")
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 0, expectedErr
}
rr := Eitherize(failFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
leftValue := result.MonadFold(outcome,
F.Identity[error],
func(int) error { return nil },
)
assert.Equal(t, expectedErr, leftValue)
})
}
// TestEitherize_EdgeCases tests edge cases for Eitherize
func TestEitherize_EdgeCases(t *testing.T) {
t.Run("handles nil context", func(t *testing.T) {
// Arrange
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
if ctx == nil {
return "nil-context", nil
}
return "non-nil-context", nil
}
rr := Eitherize(nilCtxFunc)
// Act
outcome := rr(testConfig)(nil)()
// Assert
assert.Equal(t, result.Of("nil-context"), outcome)
})
t.Run("handles zero value config", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix, nil
}
rr := Eitherize(zeroFunc)
// Act
outcome := rr(TestConfig{})(context.Background())()
// Assert
assert.Equal(t, result.Of(""), outcome)
})
t.Run("handles pointer types", func(t *testing.T) {
// Arrange
type User struct {
Name string
}
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
return &User{Name: "Alice"}, nil
}
rr := Eitherize(ptrFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsRight(outcome))
user := result.MonadFold(outcome,
func(error) *User { return nil },
F.Identity[*User],
)
assert.NotNil(t, user)
assert.Equal(t, "Alice", user.Name)
})
}
// TestEitherize_Integration tests integration with other operations
func TestEitherize_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
// Arrange
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 42, nil
}
rr := Eitherize(baseFunc)
// Act
pipeline := F.Pipe1(
rr,
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 10, nil
}
secondFunc := func(n int) ReaderReaderIOResult[TestConfig, string] {
return Of[TestConfig](fmt.Sprintf("value: %d", n))
}
// Act
pipeline := F.Pipe1(
Eitherize(firstFunc),
Chain[TestConfig](secondFunc),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("value: 10"), outcome)
})
}
// TestEitherize1_Success tests successful conversion with Eitherize1
func TestEitherize1_Success(t *testing.T) {
t.Run("converts successful function to Kleisli", func(t *testing.T) {
// Arrange
addFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n + cfg.MaxLen, nil
}
kleisli := Eitherize1(addFunc)
// Act
outcome := kleisli(10)(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(110), outcome)
})
t.Run("works with string input", func(t *testing.T) {
// Arrange
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
return cfg.Prefix + "-" + s, nil
}
kleisli := Eitherize1(concatFunc)
// Act
outcome := kleisli("input")(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("test-input"), outcome)
})
t.Run("preserves context in Kleisli", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("multiplier")
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
multiplier := ctx.Value(key)
if multiplier == nil {
return n, nil
}
return n * multiplier.(int), nil
}
kleisli := Eitherize1(multiplyFunc)
ctx := context.WithValue(context.Background(), key, 3)
// Act
outcome := kleisli(5)(testConfig)(ctx)()
// Assert
assert.Equal(t, result.Of(15), outcome)
})
}
// TestEitherize1_Failure tests error handling with Eitherize1
func TestEitherize1_Failure(t *testing.T) {
t.Run("converts error to Left in Kleisli", func(t *testing.T) {
// Arrange
expectedErr := errors.New("division by zero")
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
if n == 0 {
return 0, expectedErr
}
return 100 / n, nil
}
kleisli := Eitherize1(divideFunc)
// Act
outcome := kleisli(0)(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
assert.Equal(t, result.Left[int](expectedErr), outcome)
})
t.Run("preserves error context", func(t *testing.T) {
// Arrange
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
if len(s) > cfg.MaxLen {
return "", fmt.Errorf("string too long: %d > %d", len(s), cfg.MaxLen)
}
return s, nil
}
kleisli := Eitherize1(validateFunc)
longString := string(make([]byte, 200))
// Act
outcome := kleisli(longString)(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
leftValue := result.MonadFold(outcome,
F.Identity[error],
func(string) error { return nil },
)
assert.Contains(t, leftValue.Error(), "string too long")
})
}
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
func TestEitherize1_EdgeCases(t *testing.T) {
t.Run("handles zero value input", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n, nil
}
kleisli := Eitherize1(zeroFunc)
// Act
outcome := kleisli(0)(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(0), outcome)
})
t.Run("handles pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
outcome := kleisli(&Input{Value: 42})(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(42), outcome)
})
t.Run("handles nil pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
outcome := kleisli((*Input)(nil))(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
})
}
// TestEitherize1_Integration tests integration with other operations
func TestEitherize1_Integration(t *testing.T) {
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
doubleFunc := func(n int) ReaderReaderIOResult[TestConfig, int] {
return Of[TestConfig](n * 2)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe2(
Of[TestConfig]("42"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](doubleFunc),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(84), outcome)
})
t.Run("handles error in chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe1(
Of[TestConfig]("not-a-number"),
Chain(parseKleisli),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsLeft(outcome))
})
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
}
parseKleisli := Eitherize1(parseFunc)
formatKleisli := Eitherize1(formatFunc)
// Act
pipeline := F.Pipe2(
Of[TestConfig]("123"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](formatKleisli),
)
outcome := pipeline(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of("test-123"), outcome)
})
}
// TestEitherize_TypeSafety tests type safety across different scenarios
func TestEitherize_TypeSafety(t *testing.T) {
t.Run("Eitherize with complex types", func(t *testing.T) {
// Arrange
type ComplexResult struct {
Data map[string]int
Count int
}
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
return ComplexResult{
Data: map[string]int{"key": 42},
Count: 1,
}, nil
}
rr := Eitherize(complexFunc)
// Act
outcome := rr(testConfig)(context.Background())()
// Assert
assert.True(t, result.IsRight(outcome))
value := result.MonadFold(outcome,
func(error) ComplexResult { return ComplexResult{} },
F.Identity[ComplexResult],
)
assert.Equal(t, 42, value.Data["key"])
assert.Equal(t, 1, value.Count)
})
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
// Arrange
type Input struct {
ID int
}
type Output struct {
Name string
}
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
}
kleisli := Eitherize1(convertFunc)
// Act
outcome := kleisli(Input{ID: 99})(testConfig)(context.Background())()
// Assert
assert.Equal(t, result.Of(Output{Name: "test-99"}), outcome)
})
}

View File

@@ -3,6 +3,7 @@ package readerreaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/context/reader"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
@@ -13,6 +14,17 @@ import (
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A function that transforms R2 to R1
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.Local[context.Context, error, A](f)
@@ -102,6 +114,29 @@ func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReader
return RRIOE.LocalIOEitherK[context.Context, A](f)
}
// LocalResultK transforms the outer environment of a ReaderReaderIOResult using a Result-based Kleisli arrow.
// It allows you to modify the outer environment through a pure computation that can fail before
// passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation is a pure computation that can fail,
// such as parsing, validation, or data transformation that doesn't require IO effects.
//
// The transformation happens in two stages:
// 1. The Result function f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A Result Kleisli arrow that transforms R2 to R1 with pure computation that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalResultK[A, R1, R2 any](f result.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalEitherK[context.Context, A](f)
@@ -162,6 +197,90 @@ func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(
return RRIOE.LocalReaderIOEitherK[A](f)
}
// LocalReaderK transforms the outer environment of a ReaderReaderIOResult using a Reader-based Kleisli arrow.
// It allows you to modify the outer environment through a pure computation that depends on the inner context
// before passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation is a pure computation that requires access
// to the inner context (e.g., context.Context) but cannot fail. Common use cases include:
// - Extracting configuration from context values
// - Computing derived environment values based on context
// - Transforming environment based on context metadata
//
// The transformation happens in two stages:
// 1. The Reader function f is executed with the R2 outer environment and inner context to produce an R1 value
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A Reader Kleisli arrow that transforms R2 to R1 using the inner context
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
// Example Usage:
//
// type ctxKey string
// const configKey ctxKey = "config"
//
// // Extract config from context and transform environment
// extractConfig := func(path string) reader.Reader[DetailedConfig] {
// return func(ctx context.Context) DetailedConfig {
// if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
// return cfg
// }
// return DetailedConfig{Host: "localhost", Port: 8080}
// }
// }
//
// // Use the config
// useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
// return func(ctx context.Context) ioresult.IOResult[string] {
// return func() result.Result[string] {
// return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
// }
// }
// }
//
// // Compose using LocalReaderK
// adapted := LocalReaderK[string](extractConfig)(useConfig)
// ctx := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
// result := adapted("config.json")(ctx)() // Result: "api.example.com:443"
//
//go:inline
func LocalReaderK[A, R1, R2 any](f reader.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderK[error, A](f)
}
// LocalReaderReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderReaderIOResult-based Kleisli arrow.
// It allows you to modify the outer environment through a computation that depends on both the outer environment
// and the inner context, and can perform IO effects that may fail.
//
// This is the most powerful Local variant, useful when the outer environment transformation requires:
// - Access to both the outer environment (R2) and inner context (context.Context)
// - IO operations that can fail
// - Complex transformations that need the full computational context
//
// The transformation happens in three stages:
// 1. The ReaderReaderIOResult effect f is executed with the R2 outer environment and inner context
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A ReaderReaderIOResult Kleisli arrow that transforms R2 to R1 with full context-aware IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalReaderReaderIOEitherK[A, R1, R2 any](f Kleisli[R2, R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderReaderIOEitherK[A](f)

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/reader"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
@@ -426,3 +427,226 @@ func TestLocalReaderIOResultK(t *testing.T) {
assert.True(t, result.IsLeft(resErr))
})
}
// TestLocalReaderK tests LocalReaderK functionality
func TestLocalReaderK(t *testing.T) {
ctx := context.Background()
t.Run("basic Reader transformation", func(t *testing.T) {
// Reader that transforms string path to SimpleConfig using context
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
// Could extract values from context here
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderK
adapted := LocalReaderK[string](loadConfig)(useConfig)
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
})
t.Run("extract config from context", func(t *testing.T) {
type ctxKey string
const configKey ctxKey = "config"
// Reader that extracts config from context
extractConfig := func(path string) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
return cfg
}
// Default config if not in context
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
// Use the config
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
}
}
}
adapted := LocalReaderK[string](extractConfig)(useConfig)
// With context value
ctxWithConfig := context.WithValue(ctx, configKey, DetailedConfig{Host: "api.example.com", Port: 443})
res := adapted("ignored")(ctxWithConfig)()
assert.Equal(t, result.Of("api.example.com:443"), res)
// Without context value (uses default)
resDefault := adapted("ignored")(ctx)()
assert.Equal(t, result.Of("localhost:8080"), resDefault)
})
t.Run("context-aware transformation", func(t *testing.T) {
type ctxKey string
const multiplierKey ctxKey = "multiplier"
// Reader that uses context to compute environment
computeValue := func(base int) reader.Reader[int] {
return func(ctx context.Context) int {
if mult, ok := ctx.Value(multiplierKey).(int); ok {
return base * mult
}
return base
}
}
// Use the computed value
formatValue := func(val int) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Value: %d", val))
}
}
}
adapted := LocalReaderK[string](computeValue)(formatValue)
// With multiplier in context
ctxWithMult := context.WithValue(ctx, multiplierKey, 10)
res := adapted(5)(ctxWithMult)()
assert.Equal(t, result.Of("Value: 50"), res)
// Without multiplier (uses base value)
resBase := adapted(5)(ctx)()
assert.Equal(t, result.Of("Value: 5"), resBase)
})
t.Run("compose multiple LocalReaderK", func(t *testing.T) {
type ctxKey string
const prefixKey ctxKey = "prefix"
// First transformation: int -> string using context
intToString := func(n int) reader.Reader[string] {
return func(ctx context.Context) string {
if prefix, ok := ctx.Value(prefixKey).(string); ok {
return fmt.Sprintf("%s-%d", prefix, n)
}
return fmt.Sprintf("%d", n)
}
}
// Second transformation: string -> SimpleConfig
stringToConfig := func(s string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: len(s) * 100}
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose transformations
step1 := LocalReaderK[string](stringToConfig)(formatConfig)
step2 := LocalReaderK[string](intToString)(step1)
// With prefix in context
ctxWithPrefix := context.WithValue(ctx, prefixKey, "test")
res := step2(42)(ctxWithPrefix)()
// "test-42" has length 7, so port = 700
assert.Equal(t, result.Of("Port: 700"), res)
// Without prefix
resNoPrefix := step2(42)(ctx)()
// "42" has length 2, so port = 200
assert.Equal(t, result.Of("Port: 200"), resNoPrefix)
})
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
// Reader transformation (pure, cannot fail)
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
}
adapted := LocalReaderK[string](loadConfig)(failingOperation)
res := adapted("config.json")(ctx)()
// Error from the ReaderReaderIOResult should propagate
assert.True(t, result.IsLeft(res))
})
t.Run("real-world: environment selection based on context", func(t *testing.T) {
type Environment string
const (
Dev Environment = "dev"
Prod Environment = "prod"
)
type ctxKey string
const envKey ctxKey = "environment"
type EnvConfig struct {
Name string
}
// Reader that selects config based on context environment
selectConfig := func(envName EnvConfig) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
env := Dev
if e, ok := ctx.Value(envKey).(Environment); ok {
env = e
}
switch env {
case Prod:
return DetailedConfig{Host: "api.production.com", Port: 443}
default:
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
}
// Use the selected config
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Connecting to %s:%d", cfg.Host, cfg.Port))
}
}
}
adapted := LocalReaderK[string](selectConfig)(useConfig)
// Production environment
ctxProd := context.WithValue(ctx, envKey, Prod)
resProd := adapted(EnvConfig{Name: "app"})(ctxProd)()
assert.Equal(t, result.Of("Connecting to api.production.com:443"), resProd)
// Development environment (default)
resDev := adapted(EnvConfig{Name: "app"})(ctx)()
assert.Equal(t, result.Of("Connecting to localhost:8080"), resDev)
})
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/IBM/fp-go/v2/logging"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -104,7 +105,8 @@ func TestSLogWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(t.Context())
cancelFct, ctx := pair.Unpack(logging.WithLogger(contextLogger)(t.Context()))
defer cancelFct()
res1 := result.Of("test value")
logged := SLog[string]("Context logger test")(res1)(ctx)

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

@@ -21,8 +21,9 @@ import (
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/statet"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
SRIOE "github.com/IBM/fp-go/v2/statereaderioeither"
)
// Left creates a StateReaderIOResult that represents a failed computation with the given error.
@@ -202,21 +203,42 @@ func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
// Combinators
// Local runs a computation with a modified context.
// The function f transforms the context before passing it to the computation.
// The function f transforms the context before passing it to the computation,
// returning both a new context and a CancelFunc that should be called to release resources.
//
// This is useful for:
// - Adding values to the context
// - Setting timeouts or deadlines
// - Modifying context metadata
//
// The CancelFunc is automatically called after the computation completes to ensure proper cleanup.
//
// Type Parameters:
// - S: The state type
// - A: The result type
// - R: The input environment type that f transforms into context.Context
//
// Parameters:
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
//
// Returns:
// - A Kleisli arrow that takes a StateReaderIOResult[S, A] and returns a StateReaderIOEither[S, R, error, A]
//
// Note: When R is context.Context, the return type simplifies to func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A]
//
// Example:
//
// // Modify context before running computation
// withTimeout := statereaderioresult.Local[AppState](
// func(ctx context.Context) context.Context {
// ctx, _ = context.WithTimeout(ctx, 60*time.Second)
// return ctx
// }
// // Add a timeout to a specific operation
// 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)
func Local[S, A any](f func(context.Context) context.Context) func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return func(ma StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return function.Flow2(ma, RIOR.Local[Pair[S, A]](f))
func Local[S, A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) SRIOE.Kleisli[S, R, error, StateReaderIOResult[S, A], A] {
return func(ma StateReaderIOResult[S, A]) SRIOE.StateReaderIOEither[S, R, error, A] {
return function.Flow2(ma, RIORES.Local[Pair[S, A]](f))
}
}

View File

@@ -25,6 +25,7 @@ import (
"github.com/IBM/fp-go/v2/io"
IOR "github.com/IBM/fp-go/v2/ioresult"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/pair"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
@@ -264,8 +265,8 @@ func TestLocal(t *testing.T) {
// Modify context before running computation
result := Local[testState, string](
func(c context.Context) context.Context {
return context.WithValue(c, "key", "value2")
func(c context.Context) ContextCancel {
return pair.MakePair[context.CancelFunc](func() {}, context.WithValue(c, "key", "value2"))
},
)(comp)

View File

@@ -16,6 +16,8 @@
package statereaderioresult
import (
"context"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
@@ -84,4 +86,11 @@ type (
Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]
Predicate[A any] = predicate.Predicate[A]
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

View File

@@ -25,6 +25,31 @@ import (
"github.com/IBM/fp-go/v2/readerio"
)
// Do creates an Effect with an initial state value.
// This is the starting point for do-notation style effect composition,
// allowing you to build up complex state transformations step by step.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
//
// # Parameters
//
// - empty: The initial state value
//
// # Returns
//
// - Effect[C, S]: An effect that produces the initial state
//
// # Example
//
// type State struct {
// Name string
// Age int
// }
// eff := effect.Do[MyContext](State{})
//
//go:inline
func Do[C, S any](
empty S,
@@ -32,6 +57,40 @@ func Do[C, S any](
return readerreaderioresult.Of[C](empty)
}
// Bind executes an effectful computation and binds its result to the state.
// This is the core operation for do-notation, allowing you to sequence effects
// while accumulating results in a state structure.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the effect
//
// # Parameters
//
// - setter: A function that takes the effect result and returns a state updater
// - f: An effectful computation that depends on the current state
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.Bind(
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// func(s State) Effect[MyContext, int] {
// return effect.Of[MyContext](30)
// },
// )(effect.Do[MyContext](State{}))
//
//go:inline
func Bind[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -40,6 +99,39 @@ func Bind[C, S1, S2, T any](
return readerreaderioresult.Bind(setter, f)
}
// Let computes a pure value from the current state and binds it to the state.
// Unlike Bind, this doesn't perform any effects - it's for pure computations.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The input state type
// - S2: The output state type
// - T: The type of computed value
//
// # Parameters
//
// - setter: A function that takes the computed value and returns a state updater
// - f: A pure function that computes a value from the current state
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.Let[MyContext](
// func(nameLen int) func(State) State {
// return func(s State) State {
// s.NameLength = nameLen
// return s
// }
// },
// func(s State) int {
// return len(s.Name)
// },
// )(stateEff)
//
//go:inline
func Let[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -48,6 +140,37 @@ func Let[C, S1, S2, T any](
return readerreaderioresult.Let[C](setter, f)
}
// LetTo binds a constant value to the state.
// This is useful for setting fixed values in your state structure.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The input state type
// - S2: The output state type
// - T: The type of the constant value
//
// # Parameters
//
// - setter: A function that takes the constant and returns a state updater
// - b: The constant value to bind
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.LetTo[MyContext](
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// 42,
// )(stateEff)
//
//go:inline
func LetTo[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -56,6 +179,30 @@ func LetTo[C, S1, S2, T any](
return readerreaderioresult.LetTo[C](setter, b)
}
// BindTo wraps a value in an initial state structure.
// This is typically used to start a bind chain by converting a simple value
// into a state structure.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The state type to create
// - T: The type of the input value
//
// # Parameters
//
// - setter: A function that creates a state from the value
//
// # Returns
//
// - Operator[C, T, S1]: A function that wraps the value in state
//
// # Example
//
// eff := effect.BindTo[MyContext](func(name string) State {
// return State{Name: name}
// })(effect.Of[MyContext]("Alice"))
//
//go:inline
func BindTo[C, S1, T any](
setter func(T) S1,
@@ -63,6 +210,39 @@ func BindTo[C, S1, T any](
return readerreaderioresult.BindTo[C](setter)
}
// ApS applies an effect and binds its result to the state using a setter function.
// This is similar to Bind but takes a pre-existing effect rather than a function
// that creates an effect from the state.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the effect
//
// # Parameters
//
// - setter: A function that takes the effect result and returns a state updater
// - fa: The effect to apply
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// ageEffect := effect.Of[MyContext](30)
// eff := effect.ApS(
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// ageEffect,
// )(stateEff)
//
//go:inline
func ApS[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -71,6 +251,33 @@ func ApS[C, S1, S2, T any](
return readerreaderioresult.ApS(setter, fa)
}
// ApSL applies an effect and updates a field in the state using a lens.
// This provides a more ergonomic way to update nested state structures.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - fa: The effect producing the new field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// ageEffect := effect.Of[MyContext](30)
// eff := effect.ApSL(ageLens, ageEffect)(stateEff)
//
//go:inline
func ApSL[C, S, T any](
lens Lens[S, T],
@@ -79,6 +286,37 @@ func ApSL[C, S, T any](
return readerreaderioresult.ApSL(lens, fa)
}
// BindL executes an effectful computation on a field and updates it using a lens.
// The effect function receives the current field value and produces a new value.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - f: An effectful function that transforms the field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.BindL(
// ageLens,
// func(age int) Effect[MyContext, int] {
// return effect.Of[MyContext](age + 1)
// },
// )(stateEff)
//
//go:inline
func BindL[C, S, T any](
lens Lens[S, T],
@@ -87,6 +325,35 @@ func BindL[C, S, T any](
return readerreaderioresult.BindL(lens, f)
}
// LetL computes a new field value from the current value using a lens.
// This is a pure transformation of a field within the state.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - f: A pure function that transforms the field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.LetL[MyContext](
// ageLens,
// func(age int) int { return age * 2 },
// )(stateEff)
//
//go:inline
func LetL[C, S, T any](
lens Lens[S, T],
@@ -95,6 +362,31 @@ func LetL[C, S, T any](
return readerreaderioresult.LetL[C](lens, f)
}
// LetToL sets a field to a constant value using a lens.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - b: The constant value to set
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.LetToL[MyContext](ageLens, 42)(stateEff)
//
//go:inline
func LetToL[C, S, T any](
lens Lens[S, T],

32
v2/effect/common_test.go Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"context"
)
// TestContext is a common test context type used across effect tests
type TestContext struct {
Value string
}
// runEffect is a helper function to run an effect with a context and return the result
func runEffect[C, A any](eff Effect[C, A], ctx C) (A, error) {
ioResult := Provide[A](ctx)(eff)
readerResult := RunSync(ioResult)
return readerResult(context.Background())
}

View File

@@ -1,6 +1,22 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"github.com/IBM/fp-go/v2/context/reader"
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/io"
@@ -8,31 +24,182 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// Local transforms the context required by an effect using a pure function.
// This allows you to adapt an effect that requires one context type to work
// with a different context type by providing a transformation function.
//
// # Type Parameters
//
// - C1: The outer context type (what you have)
// - C2: The inner context type (what the effect needs)
// - A: The value type produced by the effect
//
// # Parameters
//
// - acc: A pure function that transforms C1 to C2
//
// # Returns
//
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
//
// # Example
//
// type AppConfig struct { DB DatabaseConfig }
// type DatabaseConfig struct { Host string }
// dbEffect := effect.Of[DatabaseConfig]("connected")
// appEffect := effect.Local[AppConfig, DatabaseConfig, string](
// func(app AppConfig) DatabaseConfig { return app.DB },
// )(dbEffect)
//
//go:inline
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Local[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
// Contramap is an alias for Local, following the contravariant functor naming convention.
// It transforms the context required by an effect using a pure function.
//
// # Type Parameters
//
// - C1: The outer context type (what you have)
// - C2: The inner context type (what the effect needs)
// - A: The value type produced by the effect
//
// # Parameters
//
// - acc: A pure function that transforms C1 to C2
//
// # Returns
//
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
//
//go:inline
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Contramap[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
// LocalIOK transforms the context using an IO-based function.
// This allows the context transformation itself to perform I/O operations.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: An IO function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) io.IO[Config] {
// return func() Config { /* load from file */ }
// }
// transform := effect.LocalIOK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOK[A](f)
}
// LocalIOResultK transforms the context using an IOResult-based function.
// This allows the context transformation to perform I/O and handle errors.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: An IOResult function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) ioresult.IOResult[Config] {
// return func() result.Result[Config] {
// // load from file, may fail
// }
// }
// transform := effect.LocalIOResultK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOResultK[A](f)
}
// LocalResultK transforms the context using a Result-based function.
// This allows the context transformation to fail with an error.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: A Result function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// validateConfig := func(raw RawConfig) result.Result[Config] {
// if raw.IsValid() {
// return result.Of(raw.ToConfig())
// }
// return result.Left[Config](errors.New("invalid"))
// }
// transform := effect.LocalResultK[string](validateConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalResultK[A](f)
}
// LocalThunkK transforms the context using a Thunk (ReaderIOResult) function.
// This allows the context transformation to depend on context.Context, perform I/O, and handle errors.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: A Thunk function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) readerioresult.ReaderIOResult[Config] {
// return func(ctx context.Context) ioresult.IOResult[Config] {
// // load from file with context, may fail
// }
// }
// transform := effect.LocalThunkK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderIOResultK[A](f)
@@ -101,10 +268,89 @@ func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Eff
// - Local/Contramap: Pure context transformation (C2 -> C1)
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalThunkK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
//
//go:inline
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
}
// LocalReaderK transforms the context of an Effect using a Reader-based Kleisli arrow.
// It allows you to modify the context through a pure computation that depends on the runtime context
// before passing it to the Effect.
//
// This is useful when the context transformation is a pure computation that requires access
// to the runtime context (context.Context) but cannot fail. Common use cases include:
// - Extracting configuration from context values
// - Computing derived context values based on runtime context
// - Transforming context based on runtime metadata
//
// The transformation happens in two stages:
// 1. The Reader function f is executed with the C2 context and runtime context to produce a C1 value
// 2. The resulting C1 value is passed as the context to the Effect[C1, A]
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (required by the original effect)
// - C2: The outer context type (provided to the transformed effect)
//
// # Parameters
//
// - f: A Reader Kleisli arrow that transforms C2 to C1 using the runtime context
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect to use C2
//
// # Example
//
// type ctxKey string
// const configKey ctxKey = "config"
//
// type DetailedConfig struct {
// Host string
// Port int
// }
//
// type SimpleConfig struct {
// Port int
// }
//
// // Extract config from runtime context and transform
// extractConfig := func(path string) reader.Reader[DetailedConfig] {
// return func(ctx context.Context) DetailedConfig {
// if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
// return cfg
// }
// return DetailedConfig{Host: "localhost", Port: 8080}
// }
// }
//
// // Effect that uses DetailedConfig
// configEffect := effect.Of[DetailedConfig]("connected")
//
// // Transform to use string path instead
// transform := effect.LocalReaderK[string](extractConfig)
// pathEffect := transform(configEffect)
//
// // Run with runtime context containing config
// ctx := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
// ioResult := effect.Provide[string]("config.json")(pathEffect)
// readerResult := effect.RunSync(ioResult)
// result, err := readerResult(ctx) // Uses config from context
//
// # Comparison with other Local functions
//
// - Local/Contramap: Pure context transformation (C2 -> C1)
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
// - LocalReaderK: Reader-based pure transformation with runtime context access (C2 -> Reader[C1])
// - LocalThunkK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
//
//go:inline
func LocalReaderK[A, C1, C2 any](f reader.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderK[A](f)
}

View File

@@ -19,7 +19,9 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/IBM/fp-go/v2/context/reader"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/stretchr/testify/assert"
)
@@ -44,11 +46,11 @@ func TestLocal(t *testing.T) {
}
// Apply Local to transform the context
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -70,11 +72,11 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value + " transformed"}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "original",
Number: 100,
})(outerEffect)
@@ -93,10 +95,10 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -122,12 +124,12 @@ func TestLocal(t *testing.T) {
level3Effect := Of[Level3]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
local23 := Local[string](func(l2 Level2) Level3 {
return Level3{C: l2.B + "-c"}
})
// Transform Level1 -> Level2
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{B: l1.A + "-b"}
})
@@ -136,7 +138,7 @@ func TestLocal(t *testing.T) {
level1Effect := local12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -165,11 +167,11 @@ func TestLocal(t *testing.T) {
return app.DB
}
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
kleisli := Local[string](accessor)
appEffect := kleisli(dbEffect)
// Run with full AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
DB: DatabaseConfig{
Host: "localhost",
Port: 5432,
@@ -195,21 +197,21 @@ func TestContramap(t *testing.T) {
}
// Test Local
localKleisli := Local[OuterContext, InnerContext, int](accessor)
localKleisli := Local[int](accessor)
localEffect := localKleisli(innerEffect)
// Test Contramap
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
contramapKleisli := Contramap[int](accessor)
contramapEffect := contramapKleisli(innerEffect)
outerCtx := OuterContext{Value: "test", Number: 100}
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localIO := Provide[int](outerCtx)(localEffect)
localReader := RunSync(localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapIO := Provide[int](outerCtx)(contramapEffect)
contramapReader := RunSync(contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
@@ -225,10 +227,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value + " modified"}
}
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
kleisli := Contramap[string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "original",
Number: 50,
})(outerEffect)
@@ -247,10 +249,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
kleisli := Contramap[int](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, int](OuterContext{
ioResult := Provide[int](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -278,12 +280,12 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect3 := Of[Config3]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
local23 := Local[string](func(c2 Config2) Config3 {
return Config3{Info: c2.Data}
})
// Use Contramap for second transformation
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
contramap12 := Contramap[string](func(c1 Config1) Config2 {
return Config2{Data: c1.Value}
})
@@ -292,7 +294,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect1 := contramap12(effect2)
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
ioResult := Provide[string](Config1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -326,7 +328,7 @@ func TestLocalEffectK(t *testing.T) {
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -356,7 +358,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -384,7 +386,7 @@ func TestLocalEffectK(t *testing.T) {
transformK := LocalEffectK[string](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -417,7 +419,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ioResult := Provide[string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -456,7 +458,7 @@ func TestLocalEffectK(t *testing.T) {
level1Effect := transform12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -497,7 +499,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
@@ -534,14 +536,14 @@ func TestLocalEffectK(t *testing.T) {
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
ioResult := Provide[string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
ioResult2 := Provide[string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync(ioResult2)
result, err2 := readerResult2(context.Background())
@@ -569,7 +571,7 @@ func TestLocalEffectK(t *testing.T) {
})
// Use Local for second transformation (pure)
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
@@ -578,7 +580,7 @@ func TestLocalEffectK(t *testing.T) {
effect1 := local12(effect2)
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
ioResult := Provide[string](Level1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -610,7 +612,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[int](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
ioResult := Provide[int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -618,3 +620,347 @@ func TestLocalEffectK(t *testing.T) {
assert.Equal(t, 60, result) // 3 * 10 * 2
})
}
func TestLocalReaderK(t *testing.T) {
t.Run("basic Reader transformation", func(t *testing.T) {
type SimpleConfig struct {
Port int
}
// Reader that transforms string path to SimpleConfig using runtime context
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
// Could extract values from runtime context here
return SimpleConfig{Port: 8080}
}
}
// Effect that uses the config
configEffect := Of[SimpleConfig]("connected")
// Transform using LocalReaderK
transform := LocalReaderK[string](loadConfig)
pathEffect := transform(configEffect)
// Run with path
ioResult := Provide[string]("config.json")(pathEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
t.Run("extract config from runtime context", func(t *testing.T) {
type ctxKey string
const configKey ctxKey = "config"
type DetailedConfig struct {
Host string
Port int
}
// Reader that extracts config from runtime context
extractConfig := func(path string) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
return cfg
}
// Default config if not in runtime context
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
// Effect that uses the config
configEffect := Chain(func(cfg DetailedConfig) Effect[DetailedConfig, string] {
return Of[DetailedConfig](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DetailedConfig]())
transform := LocalReaderK[string](extractConfig)
pathEffect := transform(configEffect)
// With config in runtime context
ctxWithConfig := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
ioResult := Provide[string]("ignored")(pathEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxWithConfig)
assert.NoError(t, err)
assert.Equal(t, "api.example.com:443", result)
// Without config in runtime context (uses default)
ioResult2 := Provide[string]("ignored")(pathEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "localhost:8080", result2)
})
t.Run("runtime context-aware transformation", func(t *testing.T) {
type ctxKey string
const multiplierKey ctxKey = "multiplier"
// Reader that uses runtime context to compute context
computeValue := func(base int) reader.Reader[int] {
return func(ctx context.Context) int {
if mult, ok := ctx.Value(multiplierKey).(int); ok {
return base * mult
}
return base
}
}
// Effect that uses the computed value
valueEffect := Chain(func(val int) Effect[int, string] {
return Of[int](fmt.Sprintf("Value: %d", val))
})(readerreaderioresult.Ask[int]())
transform := LocalReaderK[string](computeValue)
baseEffect := transform(valueEffect)
// With multiplier in runtime context
ctxWithMult := context.WithValue(context.Background(), multiplierKey, 10)
ioResult := Provide[string](5)(baseEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxWithMult)
assert.NoError(t, err)
assert.Equal(t, "Value: 50", result)
// Without multiplier (uses base value)
ioResult2 := Provide[string](5)(baseEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "Value: 5", result2)
})
t.Run("compose multiple LocalReaderK", func(t *testing.T) {
type ctxKey string
const prefixKey ctxKey = "prefix"
// First transformation: int -> string using runtime context
intToString := func(n int) reader.Reader[string] {
return func(ctx context.Context) string {
if prefix, ok := ctx.Value(prefixKey).(string); ok {
return fmt.Sprintf("%s-%d", prefix, n)
}
return fmt.Sprintf("%d", n)
}
}
// Second transformation: string -> SimpleConfig
type SimpleConfig struct {
Port int
}
stringToConfig := func(s string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: len(s) * 100}
}
}
// Effect that uses the config
configEffect := Chain(func(cfg SimpleConfig) Effect[SimpleConfig, string] {
return Of[SimpleConfig](fmt.Sprintf("Port: %d", cfg.Port))
})(readerreaderioresult.Ask[SimpleConfig]())
// Compose transformations
step1 := LocalReaderK[string](stringToConfig)
step2 := LocalReaderK[string](intToString)
effect1 := step1(configEffect)
effect2 := step2(effect1)
// With prefix in runtime context
ctxWithPrefix := context.WithValue(context.Background(), prefixKey, "test")
ioResult := Provide[string](42)(effect2)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxWithPrefix)
assert.NoError(t, err)
// "test-42" has length 7, so port = 700
assert.Equal(t, "Port: 700", result)
// Without prefix
ioResult2 := Provide[string](42)(effect2)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
// "42" has length 2, so port = 200
assert.Equal(t, "Port: 200", result2)
})
t.Run("error propagation from Effect", func(t *testing.T) {
type SimpleConfig struct {
Port int
}
// Reader transformation (pure, cannot fail)
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// Effect that returns an error
expectedErr := assert.AnError
failingEffect := Fail[SimpleConfig, string](expectedErr)
transform := LocalReaderK[string](loadConfig)
pathEffect := transform(failingEffect)
ioResult := Provide[string]("config.json")(pathEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
// Error from the Effect should propagate
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("real-world: environment selection based on runtime context", func(t *testing.T) {
type Environment string
const (
Dev Environment = "dev"
Prod Environment = "prod"
)
type ctxKey string
const envKey ctxKey = "environment"
type EnvConfig struct {
Name string
}
type DetailedConfig struct {
Host string
Port int
}
// Reader that selects config based on runtime context environment
selectConfig := func(envName EnvConfig) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
env := Dev
if e, ok := ctx.Value(envKey).(Environment); ok {
env = e
}
switch env {
case Prod:
return DetailedConfig{Host: "api.production.com", Port: 443}
default:
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
}
// Effect that uses the selected config
configEffect := Chain(func(cfg DetailedConfig) Effect[DetailedConfig, string] {
return Of[DetailedConfig](fmt.Sprintf("Connecting to %s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DetailedConfig]())
transform := LocalReaderK[string](selectConfig)
envEffect := transform(configEffect)
// Production environment
ctxProd := context.WithValue(context.Background(), envKey, Prod)
ioResult := Provide[string](EnvConfig{Name: "app"})(envEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxProd)
assert.NoError(t, err)
assert.Equal(t, "Connecting to api.production.com:443", result)
// Development environment (default)
ioResult2 := Provide[string](EnvConfig{Name: "app"})(envEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "Connecting to localhost:8080", result2)
})
t.Run("composes with other Local functions", func(t *testing.T) {
type Level1 struct {
Value string
}
type Level2 struct {
Data string
}
type Level3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Level3]("result")
// Use LocalReaderK for first transformation (with runtime context access)
localReaderK23 := LocalReaderK[string](func(l2 Level2) reader.Reader[Level3] {
return func(ctx context.Context) Level3 {
return Level3{Info: l2.Data}
}
})
// Use Local for second transformation (pure)
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
// Compose them
effect2 := localReaderK23(effect3)
effect1 := local12(effect2)
// Run
ioResult := Provide[string](Level1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("runtime context deadline awareness", func(t *testing.T) {
type Config struct {
HasDeadline bool
}
// Reader that checks runtime context for deadline
checkContext := func(path string) reader.Reader[Config] {
return func(ctx context.Context) Config {
_, hasDeadline := ctx.Deadline()
return Config{HasDeadline: hasDeadline}
}
}
// Effect that uses the config
configEffect := Chain(func(cfg Config) Effect[Config, string] {
return Of[Config](fmt.Sprintf("Has deadline: %v", cfg.HasDeadline))
})(readerreaderioresult.Ask[Config]())
transform := LocalReaderK[string](checkContext)
pathEffect := transform(configEffect)
// Without deadline
ioResult := Provide[string]("config.json")(pathEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "Has deadline: false", result)
// With deadline
ctxWithDeadline, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
ioResult2 := Provide[string]("config.json")(pathEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(ctxWithDeadline)
assert.NoError(t, err2)
assert.Equal(t, "Has deadline: true", result2)
})
}

View File

@@ -1,51 +1,614 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
// FromThunk lifts a Thunk (context-independent IO computation with error handling) into an Effect.
// This allows you to integrate computations that don't need the effect's context type C
// into effect chains. The Thunk will be executed with the runtime context when the effect runs.
//
// # Type Parameters
//
// - C: The context type required by the effect (not used by the thunk)
// - A: The type of the success value
//
// # Parameters
//
// - f: A Thunk[A] that performs IO with error handling
//
// # Returns
//
// - Effect[C, A]: An effect that ignores its context and executes the thunk
//
// # Example
//
// thunk := func(ctx context.Context) io.IO[result.Result[int]] {
// return func() result.Result[int] {
// // Perform IO operation
// return result.Of(42)
// }
// }
//
// eff := effect.FromThunk[MyContext](thunk)
// // eff can be used in any context but executes the thunk
//
//go:inline
func FromThunk[C, A any](f Thunk[A]) Effect[C, A] {
return reader.Of[C](f)
}
//go:inline
func FromResult[C, A any](r Result[A]) Effect[C, A] {
return readerreaderioresult.FromEither[C](r)
}
// Succeed creates a successful Effect that produces the given value.
// This is the primary way to lift a pure value into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - a: The value to wrap in a successful effect
//
// # Returns
//
// - Effect[C, A]: An effect that always succeeds with the given value
//
// # Example
//
// eff := effect.Succeed[MyContext](42)
// result, err := runEffect(eff, myContext)
// // result == 42, err == nil
func Succeed[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
// Fail creates a failed Effect with the given error.
// This is used to represent computations that have failed.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value (never produced)
//
// # Parameters
//
// - err: The error that caused the failure
//
// # Returns
//
// - Effect[C, A]: An effect that always fails with the given error
//
// # Example
//
// eff := effect.Fail[MyContext, int](errors.New("failed"))
// _, err := runEffect(eff, myContext)
// // err == errors.New("failed")
func Fail[C, A any](err error) Effect[C, A] {
return readerreaderioresult.Left[C, A](err)
}
// Of creates a successful Effect that produces the given value.
// This is an alias for Succeed and follows the pointed functor convention.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - a: The value to wrap in a successful effect
//
// # Returns
//
// - Effect[C, A]: An effect that always succeeds with the given value
//
// # Example
//
// eff := effect.Of[MyContext]("hello")
// result, err := runEffect(eff, myContext)
// // result == "hello", err == nil
func Of[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
// Map transforms the success value of an Effect using the provided function.
// If the effect fails, the error is propagated unchanged.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: The transformation function to apply to the success value
//
// # Returns
//
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
//
// # Example
//
// eff := effect.Of[MyContext](42)
// mapped := effect.Map[MyContext](func(x int) string {
// return strconv.Itoa(x)
// })(eff)
// // mapped produces "42"
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
return readerreaderioresult.Map[C](f)
}
// Chain sequences two effects, where the second effect depends on the result of the first.
// This is the monadic bind operation (flatMap) for effects.
// If the first effect fails, the second is not executed.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes the result of the first effect and returns a new effect
//
// # Returns
//
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
//
// # Example
//
// eff := effect.Of[MyContext](42)
// chained := effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext](strconv.Itoa(x * 2))
// })(eff)
// // chained produces "84"
//
//go:inline
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.Chain(f)
}
//go:inline
func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirst(f)
}
// ChainIOK chains an effect with a function that returns an IO action.
// This is useful for integrating IO-based computations (synchronous side effects)
// into effect chains. The IO action is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns IO[B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the IO-returning function with the effect
//
// # Example
//
// performIO := func(n int) io.IO[string] {
// return func() string {
// // Perform synchronous side effect
// return fmt.Sprintf("Value: %d", n)
// }
// }
//
// eff := effect.Of[MyContext](42)
// chained := effect.ChainIOK[MyContext](performIO)(eff)
// // chained produces "Value: 42"
//
//go:inline
func ChainIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainIOK[C](f)
}
// ChainFirstIOK chains an effect with a function that returns an IO action,
// but discards the result and returns the original value.
// This is useful for performing side effects (like logging) without changing the value.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Processing: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// logged := effect.ChainFirstIOK[MyContext](logValue)(eff)
// // Prints "Processing: 42" but still produces 42
//
//go:inline
func ChainFirstIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// TapIOK is an alias for ChainFirstIOK.
// It chains an effect with a function that returns an IO action for side effects,
// but preserves the original value. This is useful for logging, debugging, or
// performing actions without changing the result.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Value: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// tapped := effect.TapIOK[MyContext](logValue)(eff)
// // Prints "Value: 42" but still produces 42
//
//go:inline
func TapIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// Ap applies a function wrapped in an Effect to a value wrapped in an Effect.
// This is the applicative apply operation, useful for applying effects in parallel.
//
// # Type Parameters
//
// - B: The output value type
// - C: The context type required by the effects
// - A: The input value type
//
// # Parameters
//
// - fa: The effect containing the value to apply the function to
//
// # Returns
//
// - Operator[C, func(A) B, B]: A function that applies the function effect to the value effect
//
// # Example
//
// fnEff := effect.Of[MyContext](func(x int) int { return x * 2 })
// valEff := effect.Of[MyContext](21)
// result := effect.Ap[int](valEff)(fnEff)
// // result produces 42
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
return readerreaderioresult.Ap[B](fa)
}
// Suspend delays the evaluation of an effect until it is run.
// This is useful for recursive effects or when you need lazy evaluation.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - fa: A lazy computation that produces an effect
//
// # Returns
//
// - Effect[C, A]: An effect that evaluates the lazy computation when run
//
// # Example
//
// var recursiveEff func(int) Effect[MyContext, int]
// recursiveEff = func(n int) Effect[MyContext, int] {
// if n <= 0 {
// return effect.Of[MyContext](0)
// }
// return effect.Suspend(func() Effect[MyContext, int] {
// return effect.Map[MyContext](func(x int) int {
// return x + n
// })(recursiveEff(n - 1))
// })
// }
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
return readerreaderioresult.Defer(fa)
}
// Tap executes a side effect for its effect, but returns the original value.
// This is useful for logging, debugging, or performing actions without changing the result.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type
// - ANY: The type produced by the side effect (ignored)
//
// # Parameters
//
// - f: A function that performs a side effect based on the value
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the side effect but preserves the original value
//
// # Example
//
// eff := effect.Of[MyContext](42)
// tapped := effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
// fmt.Println("Value:", x)
// return effect.Of[MyContext, any](nil)
// })(eff)
// // Prints "Value: 42" but still produces 42
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
return readerreaderioresult.Tap(f)
}
// Ternary creates a conditional effect based on a predicate.
// If the predicate returns true, onTrue is executed; otherwise, onFalse is executed.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - pred: A predicate function to test the input value
// - onTrue: The effect to execute if the predicate is true
// - onFalse: The effect to execute if the predicate is false
//
// # Returns
//
// - Kleisli[C, A, B]: A function that conditionally executes one of two effects
//
// # Example
//
// kleisli := effect.Ternary(
// func(x int) bool { return x > 10 },
// func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext]("large")
// },
// func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext]("small")
// },
// )
// result := kleisli(15) // produces "large"
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
return function.Ternary(pred, onTrue, onFalse)
}
// ChainResultK chains an effect with a function that returns a Result.
// This is useful for integrating Result-based computations into effect chains.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Result[B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Result-returning function with the effect
//
// # Example
//
// parseIntResult := result.Eitherize1(strconv.Atoi)
// eff := effect.Of[MyContext]("42")
// chained := effect.ChainResultK[MyContext](parseIntResult)(eff)
// // chained produces 42 as an int
//
//go:inline
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainResultK[C](f)
}
// ChainReaderK chains an effect with a function that returns a Reader.
// This is useful for integrating Reader-based computations (pure context-dependent functions)
// into effect chains. The Reader is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Reader[C, B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Reader-returning function with the effect
//
// # Example
//
// type Config struct { Multiplier int }
//
// getMultiplied := func(n int) reader.Reader[Config, int] {
// return func(cfg Config) int {
// return n * cfg.Multiplier
// }
// }
//
// eff := effect.Of[Config](5)
// chained := effect.ChainReaderK[Config](getMultiplied)(eff)
// // With Config{Multiplier: 3}, produces 15
//
//go:inline
func ChainReaderK[C, A, B any](f reader.Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainReaderK(f)
}
// ChainThunkK chains an effect with a function that returns a Thunk.
// This is useful for integrating Thunk-based computations (context-independent IO with error handling)
// into effect chains. The Thunk is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Thunk[B] (readerioresult.Kleisli[A, B])
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Thunk-returning function with the effect
//
// # Example
//
// performIO := func(n int) readerioresult.ReaderIOResult[string] {
// return func(ctx context.Context) io.IO[result.Result[string]] {
// return func() result.Result[string] {
// // Perform IO operation that doesn't need effect context
// return result.Of(fmt.Sprintf("Processed: %d", n))
// }
// }
// }
//
// eff := effect.Of[MyContext](42)
// chained := effect.ChainThunkK[MyContext](performIO)(eff)
// // chained produces "Processed: 42"
//
//go:inline
func ChainThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, B] {
return fromreader.ChainReaderK(
Chain[C, A, B],
FromThunk[C, B],
f,
)
}
// ChainReaderIOK chains an effect with a function that returns a ReaderIO.
// This is useful for integrating ReaderIO-based computations (context-dependent IO operations)
// into effect chains. The ReaderIO is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns ReaderIO[C, B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the ReaderIO-returning function with the effect
//
// # Example
//
// type Config struct { LogPrefix string }
//
// logAndDouble := func(n int) readerio.ReaderIO[Config, int] {
// return func(cfg Config) io.IO[int] {
// return func() int {
// fmt.Printf("%s: %d\n", cfg.LogPrefix, n)
// return n * 2
// }
// }
// }
//
// eff := effect.Of[Config](21)
// chained := effect.ChainReaderIOK[Config](logAndDouble)(eff)
// // Logs "prefix: 21" and produces 42
//
//go:inline
func ChainReaderIOK[C, A, B any](f readerio.Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainReaderIOK(f)
}
// Read provides a context to an effect, partially applying it.
// This converts an Effect[C, A] to a Thunk[A] by supplying the required context.
//
// # Type Parameters
//
// - A: The type of the success value
// - C: The context type
//
// # Parameters
//
// - c: The context to provide to the effect
//
// # Returns
//
// - func(Effect[C, A]) Thunk[A]: A function that converts an effect to a thunk
//
// # Example
//
// ctx := MyContext{Value: "test"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Read[int](ctx)(eff)
// // thunk is now a Thunk[int] that can be run without context
//
//go:inline
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
return readerreaderioresult.Read[A](c)
}

View File

@@ -0,0 +1,649 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestSucceed tests the Succeed function
func TestSucceed_Success(t *testing.T) {
t.Run("creates successful effect with int", func(t *testing.T) {
eff := Succeed[TestConfig](42)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestConfig]("hello")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("hello"), outcome)
})
t.Run("creates successful effect with zero value", func(t *testing.T) {
eff := Succeed[TestConfig](0)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(0), outcome)
})
}
// TestFail tests the Fail function
func TestFail_Failure(t *testing.T) {
t.Run("creates failed effect with error", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("preserves error message", func(t *testing.T) {
testErr := errors.New("specific error message")
eff := Fail[TestConfig, string](testErr)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
extractedErr := result.MonadFold(outcome,
F.Identity[error],
func(string) error { return nil },
)
assert.Equal(t, testErr, extractedErr)
})
}
// TestOf tests the Of function
func TestOf_Success(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Of[TestConfig](100)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(100), outcome)
})
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test"
eff1 := Of[TestConfig](value)
eff2 := Succeed[TestConfig](value)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
})
}
// TestMap tests the Map function
func TestMap_Success(t *testing.T) {
t.Run("transforms success value", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(84), outcome)
})
t.Run("transforms type", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) string { return strconv.Itoa(x) }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x + 5 }),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestMap_Failure(t *testing.T) {
t.Run("propagates error unchanged", func(t *testing.T) {
testErr := errors.New("test error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestChain tests the Chain function
func TestChain_Success(t *testing.T) {
t.Run("sequences two effects", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Chain(func(x int) Effect[TestConfig, string] {
return Of[TestConfig](strconv.Itoa(x))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple effects", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Chain(func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x + 5)
}),
Chain(func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x * 2)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestChain_Failure(t *testing.T) {
t.Run("propagates error from first effect", func(t *testing.T) {
testErr := errors.New("first error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Chain(func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("should not execute")
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
t.Run("propagates error from second effect", func(t *testing.T) {
testErr := errors.New("second error")
eff := F.Pipe1(
Of[TestConfig](42),
Chain(func(x int) Effect[TestConfig, string] {
return Fail[TestConfig, string](testErr)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
}
// TestChainIOK tests the ChainIOK function
func TestChainIOK_Success(t *testing.T) {
t.Run("chains with IO action", func(t *testing.T) {
counter := 0
eff := F.Pipe1(
Of[TestConfig](42),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
counter++
return fmt.Sprintf("Value: %d", x)
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 42"), outcome)
assert.Equal(t, 1, counter)
})
t.Run("chains multiple IO actions", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "first")
return x + 5
}
}),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "second")
return x * 2
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainIOK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
executed = true
return "should not execute"
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainFirstIOK tests the ChainFirstIOK function
func TestChainFirstIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("logged: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"logged: 42"}, log)
})
t.Run("chains multiple side effects", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "first")
return nil
}
}),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "second")
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainFirstIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTapIOK tests the TapIOK function
func TestTapIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("tapped: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("is equivalent to ChainFirstIOK", func(t *testing.T) {
log1 := []string{}
log2 := []string{}
eff1 := F.Pipe1(
Of[TestConfig](10),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log1 = append(log1, "tap")
return nil
}
}),
)
eff2 := F.Pipe1(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log2 = append(log2, "tap")
return nil
}
}),
)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
assert.Equal(t, log1, log2)
})
}
func TestTapIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainResultK tests the ChainResultK function
func TestChainResultK_Success(t *testing.T) {
t.Run("chains with Result-returning function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("42"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("chains multiple Result operations", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe2(
Of[TestConfig]("10"),
ChainResultK[TestConfig](parseIntResult),
ChainResultK[TestConfig](func(x int) result.Result[string] {
return result.Of(fmt.Sprintf("Value: %d", x*2))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 20"), outcome)
})
}
func TestChainResultK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Fail[TestConfig, string](testErr),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("not a number"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
})
}
// TestAp tests the Ap function
func TestAp_Success(t *testing.T) {
t.Run("applies function effect to value effect", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Of[TestConfig](21)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("applies function with different types", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) string { return strconv.Itoa(x) })
valEff := Of[TestConfig](42)
eff := Ap[string](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
}
func TestAp_Failure(t *testing.T) {
t.Run("propagates error from function effect", func(t *testing.T) {
testErr := errors.New("function error")
fnEff := Fail[TestConfig, func(int) int](testErr)
valEff := Of[TestConfig](42)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from value effect", func(t *testing.T) {
testErr := errors.New("value error")
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Fail[TestConfig, int](testErr)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestSuspend tests the Suspend function
func TestSuspend_Success(t *testing.T) {
t.Run("delays evaluation of effect", func(t *testing.T) {
counter := 0
eff := Suspend(func() Effect[TestConfig, int] {
counter++
return Of[TestConfig](42)
})
assert.Equal(t, 0, counter, "should not evaluate immediately")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, 1, counter, "should evaluate when run")
assert.Equal(t, result.Of(42), outcome)
})
t.Run("enables recursive effects", func(t *testing.T) {
var factorial func(int) Effect[TestConfig, int]
factorial = func(n int) Effect[TestConfig, int] {
if n <= 1 {
return Of[TestConfig](1)
}
return Suspend(func() Effect[TestConfig, int] {
return F.Pipe1(
factorial(n-1),
Map[TestConfig](func(x int) int { return x * n }),
)
})
}
outcome := factorial(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of(120), outcome)
})
}
// TestTap tests the Tap function
func TestTap_Success(t *testing.T) {
t.Run("executes side effect but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, fmt.Sprintf("tapped: %d", x))
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("chains multiple taps", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, "first")
return Of[TestConfig, any](nil)
}),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, "second")
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestTap_Failure(t *testing.T) {
t.Run("propagates error without executing tap", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Tap(func(x int) Effect[TestConfig, any] {
executed = true
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTernary tests the Ternary function
func TestTernary_Success(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(15)(testConfig)(context.Background())()
assert.Equal(t, result.Of("large"), outcome)
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of("small"), outcome)
})
t.Run("works with boundary value", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x >= 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("gte")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("lt")
},
)
outcome := kleisli(10)(testConfig)(context.Background())()
assert.Equal(t, result.Of("gte"), outcome)
})
}
// TestRead tests the Read function
func TestRead_Success(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
eff := Of[TestConfig](42)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("converts effect to thunk", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x * testConfig.Multiplier }),
)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("works with different contexts", func(t *testing.T) {
cfg1 := TestConfig{Multiplier: 2, Prefix: "A", DatabaseURL: ""}
cfg2 := TestConfig{Multiplier: 5, Prefix: "B", DatabaseURL: ""}
// Create an effect that uses the context's Multiplier
eff := F.Pipe1(
Of[TestConfig](10),
ChainReaderK(func(x int) reader.Reader[TestConfig, int] {
return func(cfg TestConfig) int {
return x * cfg.Multiplier
}
}),
)
thunk1 := Read[int](cfg1)(eff)
thunk2 := Read[int](cfg2)(eff)
outcome1 := thunk1(context.Background())()
outcome2 := thunk2(context.Background())()
assert.Equal(t, result.Of(20), outcome1)
assert.Equal(t, result.Of(50), outcome2)
})
}
func TestRead_Failure(t *testing.T) {
t.Run("propagates error from effect", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}

View File

@@ -0,0 +1,213 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"context"
"errors"
"strconv"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestRead(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
ctx := TestContext{Value: "test-context"}
eff := Of[TestContext](42)
thunk := Read[int](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("provides context to failing effect", func(t *testing.T) {
expectedErr := errors.New("read error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("provides context to chained effects", func(t *testing.T) {
ctx := TestContext{Value: "base"}
eff := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x * 2))
})(Of[TestContext](21))
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, "42", value)
})
t.Run("works with different context types", func(t *testing.T) {
type CustomContext struct {
ID int
Name string
}
ctx := CustomContext{ID: 100, Name: "custom"}
eff := Of[CustomContext]("result")
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, "result", value)
})
t.Run("can be composed with RunSync", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](100)
thunk := Read[int](ctx)(eff)
readerResult := RunSync(thunk)
value, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, value)
})
}
func TestChainResultK(t *testing.T) {
t.Run("chains successful Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Of[TestContext]("42")
chained := ChainResultK[TestContext](parseIntResult)(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("chains failing Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Of[TestContext]("not-a-number")
chained := ChainResultK[TestContext](parseIntResult)(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})
t.Run("propagates error from original effect", func(t *testing.T) {
expectedErr := errors.New("original error")
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Fail[TestContext, string](expectedErr)
chained := ChainResultK[TestContext](parseIntResult)(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple Result functions", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
formatResult := func(x int) result.Result[string] {
return result.Of("value: " + strconv.Itoa(x))
}
eff := Of[TestContext]("42")
chained := ChainResultK[TestContext](formatResult)(
ChainResultK[TestContext](parseIntResult)(eff),
)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", result)
})
t.Run("integrates with other effect operations", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Map[TestContext](func(x int) string {
return "final: " + strconv.Itoa(x)
})(ChainResultK[TestContext](parseIntResult)(Of[TestContext]("100")))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "final: 100", result)
})
t.Run("works with custom Result functions", func(t *testing.T) {
validatePositive := func(x int) result.Result[int] {
if x > 0 {
return result.Of(x)
}
return result.Left[int](errors.New("must be positive"))
}
parseIntResult := result.Eitherize1(strconv.Atoi)
// Test with positive number
eff1 := ChainResultK[TestContext](validatePositive)(
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("42")),
)
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.Equal(t, 42, result1)
// Test with negative number
eff2 := ChainResultK[TestContext](validatePositive)(
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("-5")),
)
_, err2 := runEffect(eff2, TestContext{Value: "test"})
assert.Error(t, err2)
assert.Contains(t, err2.Error(), "must be positive")
})
t.Run("preserves error context", func(t *testing.T) {
customError := errors.New("custom validation error")
validateFunc := func(s string) result.Result[string] {
if len(s) > 0 {
return result.Of(s)
}
return result.Left[string](customError)
}
eff := ChainResultK[TestContext](validateFunc)(Of[TestContext](""))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, customError, err)
})
}

File diff suppressed because it is too large Load Diff

208
v2/effect/eitherize.go Normal file
View File

@@ -0,0 +1,208 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"context"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
)
// Eitherize converts a function that returns a value and error into an Effect.
//
// This function takes a function that accepts a context C and context.Context,
// returning a value T and an error, and converts it into an Effect[C, T].
// The error is automatically converted into a failure, while successful
// values become successes.
//
// This is particularly useful for integrating standard Go error-handling patterns into
// the effect system. It is especially helpful for adapting interface member functions
// that accept a context. When you have an interface method with signature
// (receiver, context.Context) (T, error), you can use Eitherize to convert it into
// an Effect where the receiver becomes the context C.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes C and context.Context and returns (T, error)
//
// # Returns
//
// - Effect[C, T]: An effect that depends on C, performs IO, and produces T
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
// // Implementation that may return an error
// return &User{ID: 1, Name: "Alice"}, nil
// }
//
// // Convert to Effect
// fetchUserEffect := effect.Eitherize(fetchUser)
//
// // Use in functional composition
// pipeline := F.Pipe1(
// fetchUserEffect,
// effect.Map[AppConfig](func(u *User) string { return u.Name }),
// )
//
// // Execute with config
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
//
// # Adapting Interface Methods
//
// Eitherize is particularly useful for adapting interface member functions:
//
// type UserRepository interface {
// GetUser(ctx context.Context, id int) (*User, error)
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method by binding the first parameter (receiver)
// repo := &UserRepo{db: db}
// getUserEffect := effect.Eitherize(func(id int, ctx context.Context) (*User, error) {
// return repo.GetUser(ctx, id)
// })
//
// // Now getUserEffect has type: Effect[int, *User]
// // The receiver (repo) is captured in the closure
// // The id becomes the context C
//
// # See Also
//
// - Eitherize1: For functions that take an additional parameter
// - readerreaderioresult.Eitherize: The underlying implementation
//
//go:inline
func Eitherize[C, T any](f func(C, context.Context) (T, error)) Effect[C, T] {
return readerreaderioresult.Eitherize(f)
}
// Eitherize1 converts a function that takes an additional parameter and returns a value
// and error into a Kleisli arrow.
//
// This function takes a function that accepts a context C, context.Context, and
// an additional parameter A, returning a value T and an error, and converts it into a
// Kleisli arrow (A -> Effect[C, T]). The error is automatically converted into a failure,
// while successful values become successes.
//
// This is useful for creating composable operations that depend on context and
// an input value, following standard Go error-handling patterns. It is especially helpful
// for adapting interface member functions that accept a context and additional parameters.
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
// the context C and A becomes the input parameter.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input parameter type
// - T: The success value type
//
// # Parameters
//
// - f: A function that takes C, context.Context, and A, returning (T, error)
//
// # Returns
//
// - Kleisli[C, A, T]: A function from A to Effect[C, T]
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // A function using standard Go error handling
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
// // Implementation that may return an error
// return &User{ID: id, Name: "Alice"}, nil
// }
//
// // Convert to Kleisli arrow
// fetchUserKleisli := effect.Eitherize1(fetchUserByID)
//
// // Use in functional composition with Chain
// pipeline := F.Pipe1(
// effect.Succeed[AppConfig](123),
// effect.Chain[AppConfig](fetchUserKleisli),
// )
//
// // Execute with config
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
//
// # Adapting Interface Methods
//
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
//
// type UserRepository interface {
// GetUserByID(ctx context.Context, id int) (*User, error)
// UpdateUser(ctx context.Context, user *User) error
// }
//
// type UserRepo struct {
// db *sql.DB
// }
//
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
// // Implementation
// return &User{ID: id}, nil
// }
//
// // Adapt the method - receiver becomes C, id becomes A
// repo := &UserRepo{db: db}
// getUserKleisli := effect.Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
// return r.GetUserByID(ctx, id)
// })
//
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
// // Which is: func(int) Effect[*UserRepo, *User]
// // Use it in composition:
// pipeline := F.Pipe1(
// effect.Succeed[*UserRepo](123),
// effect.Chain[*UserRepo](getUserKleisli),
// )
// result, err := effect.RunSync(effect.Provide[*User](repo)(pipeline))(context.Background())
//
// # See Also
//
// - Eitherize: For functions without an additional parameter
// - Chain: For composing Kleisli arrows
// - readerreaderioresult.Eitherize1: The underlying implementation
//
//go:inline
func Eitherize1[C, A, T any](f func(C, context.Context, A) (T, error)) Kleisli[C, A, T] {
return readerreaderioresult.Eitherize1(f)
}

507
v2/effect/eitherize_test.go Normal file
View File

@@ -0,0 +1,507 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestEitherize_Success tests successful conversion with Eitherize
func TestEitherize_Success(t *testing.T) {
t.Run("converts successful function to Effect", func(t *testing.T) {
// Arrange
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix + "-success", nil
}
eff := Eitherize(successFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-success", result)
})
t.Run("preserves context values", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("testKey")
expectedValue := "contextValue"
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
value := ctx.Value(key)
if value == nil {
return "", errors.New("context value not found")
}
return value.(string), nil
}
eff := Eitherize(contextFunc)
// Act
ioResult := Provide[string](testConfig)(eff)
readerResult := RunSync(ioResult)
ctx := context.WithValue(context.Background(), key, expectedValue)
result, err := readerResult(ctx)
// Assert
assert.NoError(t, err)
assert.Equal(t, expectedValue, result)
})
t.Run("works with different types", func(t *testing.T) {
// Arrange
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.Multiplier, nil
}
eff := Eitherize(intFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 3, result)
})
}
// TestEitherize_Failure tests error handling with Eitherize
func TestEitherize_Failure(t *testing.T) {
t.Run("converts error to failure", func(t *testing.T) {
// Arrange
expectedErr := errors.New("operation failed")
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return "", expectedErr
}
eff := Eitherize(failFunc)
// Act
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("preserves error message", func(t *testing.T) {
// Arrange
expectedErr := fmt.Errorf("validation error: field is required")
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return 0, expectedErr
}
eff := Eitherize(failFunc)
// Act
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
// TestEitherize_EdgeCases tests edge cases for Eitherize
func TestEitherize_EdgeCases(t *testing.T) {
t.Run("handles nil context", func(t *testing.T) {
// Arrange
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
if ctx == nil {
return "nil-context", nil
}
return "non-nil-context", nil
}
eff := Eitherize(nilCtxFunc)
// Act
ioResult := Provide[string](testConfig)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(nil)
// Assert
assert.NoError(t, err)
assert.Equal(t, "nil-context", result)
})
t.Run("handles zero value config", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
return cfg.Prefix, nil
}
eff := Eitherize(zeroFunc)
// Act
result, err := runEffect(eff, TestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, "", result)
})
t.Run("handles pointer types", func(t *testing.T) {
// Arrange
type User struct {
Name string
}
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
return &User{Name: cfg.Prefix}, nil
}
eff := Eitherize(ptrFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "LOG", result.Name)
})
}
// TestEitherize_Integration tests integration with other operations
func TestEitherize_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
// Arrange
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.Multiplier, nil
}
eff := Eitherize(baseFunc)
// Act
pipeline := F.Pipe1(
eff,
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "3", result)
})
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
return cfg.Multiplier, nil
}
secondFunc := func(n int) Effect[TestConfig, string] {
return Succeed[TestConfig](fmt.Sprintf("value: %d", n))
}
// Act
pipeline := F.Pipe1(
Eitherize(firstFunc),
Chain[TestConfig](secondFunc),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "value: 3", result)
})
}
// TestEitherize1_Success tests successful conversion with Eitherize1
func TestEitherize1_Success(t *testing.T) {
t.Run("converts successful function to Kleisli", func(t *testing.T) {
// Arrange
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n * cfg.Multiplier, nil
}
kleisli := Eitherize1(multiplyFunc)
// Act
eff := kleisli(10)
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 30, result)
})
t.Run("works with string input", func(t *testing.T) {
// Arrange
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
return cfg.Prefix + "-" + s, nil
}
kleisli := Eitherize1(concatFunc)
// Act
eff := kleisli("input")
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-input", result)
})
t.Run("preserves context in Kleisli", func(t *testing.T) {
// Arrange
type ctxKey string
key := ctxKey("factor")
scaleFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
factor := ctx.Value(key)
if factor == nil {
return n * cfg.Multiplier, nil
}
return n * factor.(int), nil
}
kleisli := Eitherize1(scaleFunc)
// Act
eff := kleisli(5)
ioResult := Provide[int](testConfig)(eff)
readerResult := RunSync(ioResult)
ctx := context.WithValue(context.Background(), key, 7)
result, err := readerResult(ctx)
// Assert
assert.NoError(t, err)
assert.Equal(t, 35, result)
})
}
// TestEitherize1_Failure tests error handling with Eitherize1
func TestEitherize1_Failure(t *testing.T) {
t.Run("converts error to failure in Kleisli", func(t *testing.T) {
// Arrange
expectedErr := errors.New("division by zero")
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
if n == 0 {
return 0, expectedErr
}
return 100 / n, nil
}
kleisli := Eitherize1(divideFunc)
// Act
eff := kleisli(0)
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("preserves error context", func(t *testing.T) {
// Arrange
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
if len(s) > 10 {
return "", fmt.Errorf("string too long: %d > 10", len(s))
}
return s, nil
}
kleisli := Eitherize1(validateFunc)
// Act
eff := kleisli("this-string-is-too-long")
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "string too long")
})
}
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
func TestEitherize1_EdgeCases(t *testing.T) {
t.Run("handles zero value input", func(t *testing.T) {
// Arrange
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
return n, nil
}
kleisli := Eitherize1(zeroFunc)
// Act
eff := kleisli(0)
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
t.Run("handles pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value * cfg.Multiplier, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
eff := kleisli(&Input{Value: 7})
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 21, result)
})
t.Run("handles nil pointer input", func(t *testing.T) {
// Arrange
type Input struct {
Value int
}
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
if in == nil {
return 0, errors.New("nil input")
}
return in.Value, nil
}
kleisli := Eitherize1(ptrFunc)
// Act
eff := kleisli((*Input)(nil))
_, err := runEffect(eff, testConfig)
// Assert
assert.Error(t, err)
assert.Contains(t, err.Error(), "nil input")
})
}
// TestEitherize1_Integration tests integration with other operations
func TestEitherize1_Integration(t *testing.T) {
t.Run("composes with Chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
doubleFunc := func(n int) Effect[TestConfig, int] {
return Succeed[TestConfig](n * 2)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe2(
Succeed[TestConfig]("42"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](doubleFunc),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 84, result)
})
t.Run("handles error in chain", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
parseKleisli := Eitherize1(parseFunc)
// Act
pipeline := F.Pipe1(
Succeed[TestConfig]("not-a-number"),
Chain[TestConfig](parseKleisli),
)
_, err := runEffect(pipeline, testConfig)
// Assert
assert.Error(t, err)
})
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
// Arrange
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
return strconv.Atoi(s)
}
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
}
parseKleisli := Eitherize1(parseFunc)
formatKleisli := Eitherize1(formatFunc)
// Act
pipeline := F.Pipe2(
Succeed[TestConfig]("123"),
Chain[TestConfig](parseKleisli),
Chain[TestConfig](formatKleisli),
)
result, err := runEffect(pipeline, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-123", result)
})
}
// TestEitherize_TypeSafety tests type safety across different scenarios
func TestEitherize_TypeSafety(t *testing.T) {
t.Run("Eitherize with complex types", func(t *testing.T) {
// Arrange
type ComplexResult struct {
Data map[string]int
Count int
}
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
return ComplexResult{
Data: map[string]int{cfg.Prefix: cfg.Multiplier},
Count: cfg.Multiplier,
}, nil
}
eff := Eitherize(complexFunc)
// Act
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, 3, result.Data["LOG"])
assert.Equal(t, 3, result.Count)
})
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
// Arrange
type Input struct {
ID int
}
type Output struct {
Name string
}
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
}
kleisli := Eitherize1(convertFunc)
// Act
eff := kleisli(Input{ID: 99})
result, err := runEffect(eff, testConfig)
// Assert
assert.NoError(t, err)
assert.Equal(t, "LOG-99", result.Name)
})
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
@@ -5,10 +20,66 @@ import (
"github.com/IBM/fp-go/v2/monoid"
)
// ApplicativeMonoid creates a monoid for effects using applicative semantics.
// This combines effects by running both and combining their results using the provided monoid.
// If either effect fails, the combined effect fails.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type that has a monoid instance
//
// # Parameters
//
// - m: The monoid instance for combining values of type A
//
// # Returns
//
// - Monoid[Effect[C, A]]: A monoid for combining effects
//
// # Example
//
// stringMonoid := monoid.MakeMonoid(
// func(a, b string) string { return a + b },
// "",
// )
// effectMonoid := effect.ApplicativeMonoid[MyContext](stringMonoid)
// eff1 := effect.Of[MyContext]("Hello")
// eff2 := effect.Of[MyContext](" World")
// combined := effectMonoid.Concat(eff1, eff2)
// // combined produces "Hello World"
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.ApplicativeMonoid[C](m)
}
// AlternativeMonoid creates a monoid for effects using alternative semantics.
// This tries the first effect, and if it fails, tries the second effect.
// If both succeed, their results are combined using the provided monoid.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type that has a monoid instance
//
// # Parameters
//
// - m: The monoid instance for combining values of type A
//
// # Returns
//
// - Monoid[Effect[C, A]]: A monoid for combining effects with fallback behavior
//
// # Example
//
// stringMonoid := monoid.MakeMonoid(
// func(a, b string) string { return a + b },
// "",
// )
// effectMonoid := effect.AlternativeMonoid[MyContext](stringMonoid)
// eff1 := effect.Fail[MyContext, string](errors.New("failed"))
// eff2 := effect.Of[MyContext]("fallback")
// combined := effectMonoid.Concat(eff1, eff2)
// // combined produces "fallback" (first failed, so second is used)
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.AlternativeMonoid[C](m)
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
@@ -5,6 +20,39 @@ import (
"github.com/IBM/fp-go/v2/retry"
)
// Retrying executes an effect with retry logic based on a policy and check predicate.
// The effect is retried according to the policy until either:
// - The effect succeeds and the check predicate returns false
// - The retry policy is exhausted
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - policy: The retry policy defining retry limits and delays
// - action: An effectful computation that receives retry status and produces a value
// - check: A predicate that determines if the result should trigger a retry
//
// # Returns
//
// - Effect[C, A]: An effect that retries according to the policy
//
// # Example
//
// policy := retry.LimitRetries(3)
// eff := effect.Retrying[MyContext, string](
// policy,
// func(status retry.RetryStatus) Effect[MyContext, string] {
// return fetchData() // may fail
// },
// func(result Result[string]) bool {
// return result.IsLeft() // retry on error
// },
// )
// // Retries up to 3 times if fetchData fails
func Retrying[C, A any](
policy retry.RetryPolicy,
action Kleisli[C, retry.RetryStatus, A],

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
@@ -8,10 +23,64 @@ import (
"github.com/IBM/fp-go/v2/result"
)
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
// Provide supplies a context to an effect, converting it to a Thunk.
// This is the first step in running an effect - it eliminates the context dependency
// by providing the required context value.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - c: The context value to provide to the effect
//
// # Returns
//
// - func(Effect[C, A]) ReaderIOResult[A]: A function that converts an effect to a thunk
//
// # Example
//
// ctx := MyContext{APIKey: "secret"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Provide[MyContext, int](ctx)(eff)
// // thunk is now a ReaderIOResult[int] that can be run
func Provide[A, C any](c C) func(Effect[C, A]) ReaderIOResult[A] {
return readerreaderioresult.Read[A](c)
}
// RunSync executes a Thunk synchronously, converting it to a standard Go function.
// This is the final step in running an effect - it executes the IO operations
// and returns the result as a standard (value, error) tuple.
//
// # Type Parameters
//
// - A: The type of the success value
//
// # Parameters
//
// - fa: The thunk to execute
//
// # Returns
//
// - readerresult.ReaderResult[A]: A function that takes a context.Context and returns (A, error)
//
// # Example
//
// ctx := MyContext{APIKey: "secret"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Provide[MyContext, int](ctx)(eff)
// readerResult := effect.RunSync(thunk)
// value, err := readerResult(context.Background())
// // value == 42, err == nil
//
// # Complete Example
//
// // Typical usage pattern:
// result, err := effect.RunSync(
// effect.Provide[MyContext, string](myContext)(myEffect),
// )(context.Background())
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return result.Unwrap(fa(ctx)())

View File

@@ -28,7 +28,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test-value"}
eff := Of[TestContext]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -45,7 +45,7 @@ func TestProvide(t *testing.T) {
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config]("connected")
ioResult := Provide[Config, string](cfg)(eff)
ioResult := Provide[string](cfg)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -58,7 +58,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -74,7 +74,7 @@ func TestProvide(t *testing.T) {
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -89,7 +89,7 @@ func TestProvide(t *testing.T) {
return Of[TestContext]("result")
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -104,7 +104,7 @@ func TestProvide(t *testing.T) {
return "mapped"
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -118,7 +118,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -130,7 +130,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
bgCtx := context.Background()
@@ -145,7 +145,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -162,7 +162,7 @@ func TestRunSync(t *testing.T) {
return Of[TestContext](x + 10)
})(Of[TestContext](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -174,7 +174,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
// Run multiple times
@@ -200,7 +200,7 @@ func TestRunSync(t *testing.T) {
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext](user)
ioResult := Provide[TestContext, User](ctx)(eff)
ioResult := Provide[User](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -222,7 +222,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Of[AppConfig]("API call successful")
// Provide config and run
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
result, err := RunSync(Provide[string](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
@@ -238,7 +238,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
_, err := RunSync(Provide[string](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
@@ -253,7 +253,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(Of[TestContext](21)))
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
result, err := RunSync(Provide[string](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
@@ -281,7 +281,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return State{X: x}
})(Of[TestContext](10)))
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
result, err := RunSync(Provide[State](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
@@ -300,11 +300,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
innerEff := Of[InnerCtx]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
transformedEff := Local[string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
result, err := RunSync(Provide[string](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
@@ -318,7 +318,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(input)
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
result, err := RunSync(Provide[[]int](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)

View File

@@ -1,7 +1,55 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
// TraverseArray applies an effectful function to each element of an array,
// collecting the results into a new array. If any effect fails, the entire
// traversal fails and returns the first error encountered.
//
// This is useful for performing effectful operations on collections while
// maintaining the sequential order of results.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - f: An effectful function to apply to each element
//
// # Returns
//
// - Kleisli[C, []A, []B]: A function that transforms an array of A to an effect producing an array of B
//
// # Example
//
// parseIntEff := func(s string) Effect[MyContext, int] {
// val, err := strconv.Atoi(s)
// if err != nil {
// return effect.Fail[MyContext, int](err)
// }
// return effect.Of[MyContext](val)
// }
// input := []string{"1", "2", "3"}
// eff := effect.TraverseArray[MyContext](parseIntEff)(input)
// // eff produces []int{1, 2, 3}
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
return readerreaderioresult.TraverseArray(f)
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
@@ -17,21 +32,61 @@ import (
)
type (
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
IO[A any] = io.IO[A]
IOEither[E, A any] = ioeither.IOEither[E, A]
Lazy[A any] = lazy.Lazy[A]
IOResult[A any] = ioresult.IOResult[A]
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
Monoid[A any] = monoid.Monoid[A]
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
Thunk[A any] = ReaderIOResult[A]
Predicate[A any] = predicate.Predicate[A]
Result[A any] = result.Result[A]
Lens[S, T any] = lens.Lens[S, T]
// Either represents a value that can be either a Left (error) or Right (success).
Either[E, A any] = either.Either[E, A]
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
// Reader represents a computation that depends on a context R and produces a value A.
Reader[R, A any] = reader.Reader[R, A]
// ReaderIO represents a computation that depends on a context R and produces an IO action returning A.
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// IO represents a synchronous side effect that produces a value A.
IO[A any] = io.IO[A]
// IOEither represents a synchronous side effect that can fail with error E or succeed with value A.
IOEither[E, A any] = ioeither.IOEither[E, A]
// Lazy represents a lazily evaluated computation that produces a value A.
Lazy[A any] = lazy.Lazy[A]
// IOResult represents a synchronous side effect that can fail with an error or succeed with value A.
IOResult[A any] = ioresult.IOResult[A]
// ReaderIOResult represents a computation that depends on context and performs IO with error handling.
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
Monoid[A any] = monoid.Monoid[A]
// Effect represents an effectful computation that:
// - Requires a context of type C
// - Can perform I/O operations
// - Can fail with an error
// - Produces a value of type A on success
//
// This is the core type of the effect package, providing a complete effect system
// for managing dependencies, errors, and side effects in a composable way.
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
// Thunk represents a computation that performs IO with error handling but doesn't require context.
// It's equivalent to ReaderIOResult and is used as an intermediate step when providing context to an Effect.
Thunk[A any] = ReaderIOResult[A]
// Predicate represents a function that tests a value of type A and returns a boolean.
Predicate[A any] = predicate.Predicate[A]
// Result represents a computation result that can be either an error (Left) or a success value (Right).
Result[A any] = result.Result[A]
// Lens represents an optic for focusing on a field T within a structure S.
Lens[S, T any] = lens.Lens[S, T]
// Kleisli represents a function from A to Effect[C, B], enabling monadic composition.
// It's the fundamental building block for chaining effectful computations.
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
// It's used for lifting operations over effects.
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
)

View File

@@ -379,7 +379,7 @@ func TestMonadChainLeft(t *testing.T) {
func TestChainLeft(t *testing.T) {
t.Run("Curried function transforms Left value", func(t *testing.T) {
// Create a reusable error handler
handleNotFound := ChainLeft[error, string](func(err error) Either[string, int] {
handleNotFound := ChainLeft(func(err error) Either[string, int] {
if err.Error() == "not found" {
return Right[string](0)
}
@@ -391,7 +391,7 @@ func TestChainLeft(t *testing.T) {
})
t.Run("Curried function with Right value", func(t *testing.T) {
handler := ChainLeft[error, string](func(err error) Either[string, int] {
handler := ChainLeft(func(err error) Either[string, int] {
return Left[int]("should not be called")
})
@@ -401,7 +401,7 @@ func TestChainLeft(t *testing.T) {
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
// Create error transformer
toStringError := ChainLeft[int, string](func(code int) Either[string, string] {
toStringError := ChainLeft(func(code int) Either[string, string] {
return Left[string](fmt.Sprintf("Error: %d", code))
})
@@ -414,12 +414,12 @@ func TestChainLeft(t *testing.T) {
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
// First handler: convert error to string
handler1 := ChainLeft[error, string](func(err error) Either[string, int] {
handler1 := ChainLeft(func(err error) Either[string, int] {
return Left[int](err.Error())
})
// Second handler: add prefix to string error
handler2 := ChainLeft[string, string](func(s string) Either[string, int] {
handler2 := ChainLeft(func(s string) Either[string, int] {
return Left[int]("Handled: " + s)
})

View File

@@ -55,5 +55,7 @@ type (
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
// Pair represents a tuple of two values of types L and R.
// It's commonly used to return multiple values from functions or to group related data.
Pair[L, R any] = pair.Pair[L, R]
)

View File

@@ -40,7 +40,7 @@
// increment := N.Add(1)
//
// // Compose them (RIGHT-TO-LEFT execution)
// composed := endomorphism.Compose(double, increment)
// composed := endomorphism.MonadCompose(double, increment)
// result := composed(5) // increment(5) then double: (5 + 1) * 2 = 12
//
// // Chain them (LEFT-TO-RIGHT execution)
@@ -61,11 +61,11 @@
// monoid := endomorphism.Monoid[int]()
//
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(
// combined := M.ConcatAll(monoid)([]endomorphism.Endomorphism[int]{
// N.Mul(2), // applied third
// N.Add(1), // applied second
// N.Mul(3), // applied first
// )
// })
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
//
// # Monad Operations
@@ -87,7 +87,7 @@
// increment := N.Add(1)
//
// // Compose: RIGHT-TO-LEFT (mathematical composition)
// composed := endomorphism.Compose(double, increment)
// composed := endomorphism.MonadCompose(double, increment)
// result1 := composed(5) // increment(5) * 2 = (5 + 1) * 2 = 12
//
// // MonadChain: LEFT-TO-RIGHT (sequential application)

View File

@@ -111,15 +111,19 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
// This is the functor map operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - g is applied first to the input
// - ma is applied first to the input
// - f is applied to the result
//
// Note: unlike most other packages where MonadMap takes (fa, f) with the container
// first, here f (the morphism) comes first to match the right-to-left composition
// convention: MonadMap(f, ma) = f ∘ ma.
//
// Parameters:
// - f: The function to map (outer function)
// - g: The endomorphism to map over (inner function)
// - f: The function to map (outer function, applied second)
// - ma: The endomorphism to map over (inner function, applied first)
//
// Returns:
// - A new endomorphism that applies g, then f
// - A new endomorphism that applies ma, then f
//
// Example:
//
@@ -127,8 +131,8 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
// increment := N.Add(1)
// mapped := endomorphism.MonadMap(double, increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
return MonadCompose(f, g)
func MonadMap[A any](f, ma Endomorphism[A]) Endomorphism[A] {
return MonadCompose(f, ma)
}
// Compose returns a function that composes an endomorphism with another, executing right to left.
@@ -386,3 +390,91 @@ func Join[A any](f Kleisli[A]) Endomorphism[A] {
return f(a)(a)
}
}
// Read captures a value and returns a function that applies endomorphisms to it.
//
// This function implements a "reader" pattern for endomorphisms. It takes a value
// and returns a function that can apply any endomorphism to that captured value.
// This is useful for creating reusable evaluation contexts where you want to apply
// different transformations to the same initial value.
//
// The returned function has the signature func(Endomorphism[A]) A, which means
// it takes an endomorphism and returns the result of applying that endomorphism
// to the captured value.
//
// # Type Parameters
//
// - A: The type of the value being captured and transformed
//
// # Parameters
//
// - a: The value to capture for later transformation
//
// # Returns
//
// - A function that applies endomorphisms to the captured value
//
// # Example - Basic Usage
//
// // Capture a value
// applyTo5 := Read(5)
//
// // Apply different endomorphisms to the same value
// doubled := applyTo5(N.Mul(2)) // 10
// incremented := applyTo5(N.Add(1)) // 6
// squared := applyTo5(func(x int) int { return x * x }) // 25
//
// # Example - Reusable Evaluation Context
//
// type Config struct {
// Timeout int
// Retries int
// }
//
// baseConfig := Config{Timeout: 30, Retries: 3}
// applyToBase := Read(baseConfig)
//
// // Apply different transformations to the same base config
// withLongTimeout := applyToBase(func(c Config) Config {
// c.Timeout = 60
// return c
// })
//
// withMoreRetries := applyToBase(func(c Config) Config {
// c.Retries = 5
// return c
// })
//
// # Example - Testing Different Transformations
//
// // Useful for testing multiple transformations on the same input
// testValue := "hello"
// applyToTest := Read(testValue)
//
// upperCase := applyToTest(strings.ToUpper) // "HELLO"
// withSuffix := applyToTest(func(s string) string {
// return s + " world"
// }) // "hello world"
//
// # Use Cases
//
// 1. **Testing**: Apply multiple transformations to the same test value
// 2. **Configuration**: Create variations of a base configuration
// 3. **Data Processing**: Evaluate different processing pipelines on the same data
// 4. **Benchmarking**: Compare different endomorphisms on the same input
// 5. **Functional Composition**: Build evaluation contexts for composed operations
//
// # Relationship to Other Functions
//
// Read is complementary to other endomorphism operations:
// - Build applies an endomorphism to the zero value
// - Read applies endomorphisms to a specific captured value
// - Reduce applies multiple endomorphisms sequentially
// - ConcatAll composes multiple endomorphisms
//
//go:inline
func Read[A any](a A) func(Endomorphism[A]) A {
return func(f Endomorphism[A]) A {
return f(a)
}
}

View File

@@ -1071,3 +1071,226 @@ func TestReduceWithBuild(t *testing.T) {
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
}
// TestRead tests the Read function
func TestRead(t *testing.T) {
t.Run("applies endomorphism to captured value", func(t *testing.T) {
applyTo5 := Read(5)
result := applyTo5(double)
assert.Equal(t, 10, result, "Read should apply double to captured value 5")
result2 := applyTo5(increment)
assert.Equal(t, 6, result2, "Read should apply increment to captured value 5")
result3 := applyTo5(square)
assert.Equal(t, 25, result3, "Read should apply square to captured value 5")
})
t.Run("captures value for reuse", func(t *testing.T) {
applyTo10 := Read(10)
// Apply multiple different endomorphisms to the same captured value
doubled := applyTo10(double)
incremented := applyTo10(increment)
negated := applyTo10(negate)
assert.Equal(t, 20, doubled, "Should double 10")
assert.Equal(t, 11, incremented, "Should increment 10")
assert.Equal(t, -10, negated, "Should negate 10")
})
t.Run("works with identity", func(t *testing.T) {
applyTo42 := Read(42)
result := applyTo42(Identity[int]())
assert.Equal(t, 42, result, "Read with identity should return original value")
})
t.Run("works with composed endomorphisms", func(t *testing.T) {
applyTo5 := Read(5)
// Compose: double then increment (RIGHT-TO-LEFT)
composed := MonadCompose(increment, double)
result := applyTo5(composed)
assert.Equal(t, 11, result, "Read should work with composed endomorphisms: (5 * 2) + 1 = 11")
})
t.Run("works with chained endomorphisms", func(t *testing.T) {
applyTo5 := Read(5)
// Chain: double then increment (LEFT-TO-RIGHT)
chained := MonadChain(double, increment)
result := applyTo5(chained)
assert.Equal(t, 11, result, "Read should work with chained endomorphisms: (5 * 2) + 1 = 11")
})
t.Run("works with ConcatAll", func(t *testing.T) {
applyTo5 := Read(5)
// ConcatAll composes RIGHT-TO-LEFT
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
result := applyTo5(combined)
// Execution: square(5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 52, result, "Read should work with ConcatAll")
})
t.Run("works with different types", func(t *testing.T) {
// Test with string
applyToHello := Read("hello")
toUpper := func(s string) string { return s + " WORLD" }
result := applyToHello(toUpper)
assert.Equal(t, "hello WORLD", result, "Read should work with strings")
// Test with struct
type Point struct {
X, Y int
}
applyToPoint := Read(Point{X: 3, Y: 4})
scaleX := func(p Point) Point {
p.X *= 2
return p
}
result2 := applyToPoint(scaleX)
assert.Equal(t, Point{X: 6, Y: 4}, result2, "Read should work with structs")
})
t.Run("creates independent evaluation contexts", func(t *testing.T) {
applyTo5 := Read(5)
applyTo10 := Read(10)
// Same endomorphism, different contexts
result5 := applyTo5(double)
result10 := applyTo10(double)
assert.Equal(t, 10, result5, "First context should double 5")
assert.Equal(t, 20, result10, "Second context should double 10")
})
t.Run("useful for testing transformations", func(t *testing.T) {
testValue := 100
applyToTest := Read(testValue)
// Test multiple transformations on the same value
transformations := []struct {
name string
endo Endomorphism[int]
expected int
}{
{"double", double, 200},
{"increment", increment, 101},
{"negate", negate, -100},
{"square", square, 10000},
}
for _, tt := range transformations {
t.Run(tt.name, func(t *testing.T) {
result := applyToTest(tt.endo)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("works with monoid operations", func(t *testing.T) {
applyTo5 := Read(5)
// Use monoid to combine endomorphisms
combined := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
double,
increment,
})
result := applyTo5(combined)
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
assert.Equal(t, 12, result, "Read should work with monoid operations")
})
t.Run("configuration example", func(t *testing.T) {
type Config struct {
Timeout int
Retries int
}
baseConfig := Config{Timeout: 30, Retries: 3}
applyToBase := Read(baseConfig)
withLongTimeout := func(c Config) Config {
c.Timeout = 60
return c
}
withMoreRetries := func(c Config) Config {
c.Retries = 5
return c
}
result1 := applyToBase(withLongTimeout)
assert.Equal(t, Config{Timeout: 60, Retries: 3}, result1)
result2 := applyToBase(withMoreRetries)
assert.Equal(t, Config{Timeout: 30, Retries: 5}, result2)
// Original is unchanged
result3 := applyToBase(Identity[Config]())
assert.Equal(t, baseConfig, result3)
})
}
// TestReadWithBuild tests the relationship between Read and Build
func TestReadWithBuild(t *testing.T) {
t.Run("Read applies to specific value, Build to zero value", func(t *testing.T) {
endo := double
// Build applies to zero value
builtResult := Build(endo)
assert.Equal(t, 0, builtResult, "Build should apply to zero value: 0 * 2 = 0")
// Read applies to specific value
readResult := Read(5)(endo)
assert.Equal(t, 10, readResult, "Read should apply to captured value: 5 * 2 = 10")
})
t.Run("Read can evaluate Build results", func(t *testing.T) {
// Build an endomorphism
builder := ConcatAll([]Endomorphism[int]{double, increment})
// Apply it to zero value
builtValue := Build(builder)
// RIGHT-TO-LEFT: increment(0) = 1, double(1) = 2
assert.Equal(t, 2, builtValue)
// Now use Read to apply the same builder to a different value
readValue := Read(5)(builder)
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
assert.Equal(t, 12, readValue)
})
}
// BenchmarkRead benchmarks the Read function
func BenchmarkRead(b *testing.B) {
applyTo5 := Read(5)
b.Run("simple endomorphism", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = applyTo5(double)
}
})
b.Run("composed endomorphism", func(b *testing.B) {
composed := MonadCompose(double, increment)
for i := 0; i < b.N; i++ {
_ = applyTo5(composed)
}
})
b.Run("ConcatAll endomorphism", func(b *testing.B) {
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
for i := 0; i < b.N; i++ {
_ = applyTo5(combined)
}
})
}

View File

@@ -144,8 +144,8 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// square := func(x int) int { return x * x }
//
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(double, increment, square)
// result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
// combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
// result := combined(5) // double(increment(square(5))) = double(increment(25)) = double(26) = 52
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
return M.MakeMonoid(MonadCompose[A], Identity[A]())
}

View File

@@ -41,20 +41,22 @@ type (
// It's a function from A to Endomorphism[A], used for composing endomorphic operations.
Kleisli[A any] = func(A) Endomorphism[A]
// Operator represents a transformation from one endomorphism to another.
// Operator represents a higher-order transformation on endomorphisms of the same type.
//
// An Operator takes an endomorphism on type A and produces an endomorphism on type B.
// This is useful for lifting operations or transforming endomorphisms in a generic way.
// An Operator takes an endomorphism on type A and produces another endomorphism on type A.
// Since Operator[A] = Endomorphism[Endomorphism[A]] = func(func(A)A) func(A)A,
// both the input and output endomorphisms operate on the same type A.
//
// This is the return type of curried operations such as Compose, Map, and Chain.
//
// Example:
//
// // An operator that converts an int endomorphism to a string endomorphism
// intToString := func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[string] {
// return func(s string) string {
// n, _ := strconv.Atoi(s)
// result := f(n)
// return strconv.Itoa(result)
// }
// // An operator that applies any endomorphism twice
// var applyTwice endomorphism.Operator[int] = func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[int] {
// return func(x int) int { return f(f(x)) }
// }
// double := N.Mul(2)
// result := applyTwice(double) // double ∘ double
// // result(5) = double(double(5)) = double(10) = 20
Operator[A any] = Endomorphism[Endomorphism[A]]
)

View File

@@ -4,7 +4,7 @@ go 1.24
require (
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.2
github.com/urfave/cli/v3 v3.7.0
)
require (

View File

@@ -6,6 +6,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -23,6 +23,8 @@ import (
"log"
"log/slog"
"sync/atomic"
"github.com/IBM/fp-go/v2/pair"
)
// LoggingCallbacks creates a pair of logging callback functions from the provided loggers.
@@ -128,6 +130,7 @@ var loggerInContextKey loggerInContextType
// logger.Info("Processing request")
// }
func GetLoggerFromContext(ctx context.Context) *slog.Logger {
// using idomatic style to avoid import cycle
value, ok := ctx.Value(loggerInContextKey).(*slog.Logger)
if !ok {
return globalLogger.Load()
@@ -135,9 +138,11 @@ func GetLoggerFromContext(ctx context.Context) *slog.Logger {
return value
}
// WithLogger returns an endomorphism that adds a logger to a context.
// An endomorphism is a function that takes a value and returns a value of the same type.
// This function creates a context transformation that embeds the provided logger.
func noop() {}
// WithLogger returns a Kleisli arrow that adds a logger to a context.
// A Kleisli arrow transforms a context into a ContextCancel pair containing
// a no-op cancel function and the new context with the embedded logger.
//
// This is particularly useful in functional programming patterns where you want to
// compose context transformations, or when working with middleware that needs to
@@ -147,7 +152,7 @@ func GetLoggerFromContext(ctx context.Context) *slog.Logger {
// - l: The *slog.Logger to embed in the context
//
// Returns:
// - An Endomorphism[context.Context] function that adds the logger to a context
// - A Kleisli arrow (function from context.Context to ContextCancel) that adds the logger to a context
//
// Example:
//
@@ -156,13 +161,14 @@ func GetLoggerFromContext(ctx context.Context) *slog.Logger {
//
// // Apply it to a context
// ctx := context.Background()
// ctxWithLogger := addLogger(ctx)
// result := addLogger(ctx)
// ctxWithLogger := pair.Second(result)
//
// // Retrieve the logger later
// logger := GetLoggerFromContext(ctxWithLogger)
// logger.Info("Using context logger")
func WithLogger(l *slog.Logger) Endomorphism[context.Context] {
return func(ctx context.Context) context.Context {
return context.WithValue(ctx, loggerInContextKey, l)
func WithLogger(l *slog.Logger) pair.Kleisli[context.CancelFunc, context.Context, context.Context] {
return func(ctx context.Context) ContextCancel {
return pair.MakePair[context.CancelFunc](noop, context.WithValue(ctx, loggerInContextKey, l))
}
}

View File

@@ -17,10 +17,13 @@ package logging
import (
"bytes"
"context"
"log"
"log/slog"
"strings"
"testing"
"github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
)
@@ -288,3 +291,355 @@ func BenchmarkLoggingCallbacks_Logging(b *testing.B) {
infoLog("benchmark message %d", i)
}
}
// TestSetLogger_Success tests setting a new global logger and verifying it returns the old one.
func TestSetLogger_Success(t *testing.T) {
// Save original logger to restore later
originalLogger := GetLogger()
defer SetLogger(originalLogger)
// Create a new logger
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
newLogger := slog.New(handler)
// Set the new logger
oldLogger := SetLogger(newLogger)
// Verify old logger was returned
if oldLogger == nil {
t.Error("Expected SetLogger to return the previous logger")
}
// Verify new logger is now active
currentLogger := GetLogger()
if currentLogger != newLogger {
t.Error("Expected GetLogger to return the newly set logger")
}
}
// TestSetLogger_Multiple tests setting logger multiple times.
func TestSetLogger_Multiple(t *testing.T) {
// Save original logger to restore later
originalLogger := GetLogger()
defer SetLogger(originalLogger)
// Create three loggers
logger1 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
logger2 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
logger3 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
// Set first logger
old1 := SetLogger(logger1)
if GetLogger() != logger1 {
t.Error("Expected logger1 to be active")
}
// Set second logger
old2 := SetLogger(logger2)
if old2 != logger1 {
t.Error("Expected SetLogger to return logger1")
}
if GetLogger() != logger2 {
t.Error("Expected logger2 to be active")
}
// Set third logger
old3 := SetLogger(logger3)
if old3 != logger2 {
t.Error("Expected SetLogger to return logger2")
}
if GetLogger() != logger3 {
t.Error("Expected logger3 to be active")
}
// Restore to original
restored := SetLogger(old1)
if restored != logger3 {
t.Error("Expected SetLogger to return logger3")
}
}
// TestGetLogger_Default tests that GetLogger returns a valid logger by default.
func TestGetLogger_Default(t *testing.T) {
logger := GetLogger()
if logger == nil {
t.Error("Expected GetLogger to return a non-nil logger")
}
// Verify it's usable
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
testLogger := slog.New(handler)
oldLogger := SetLogger(testLogger)
defer SetLogger(oldLogger)
GetLogger().Info("test message")
if !strings.Contains(buf.String(), "test message") {
t.Errorf("Expected logger to log message, got: %s", buf.String())
}
}
// TestGetLogger_AfterSet tests that GetLogger returns the logger set by SetLogger.
func TestGetLogger_AfterSet(t *testing.T) {
originalLogger := GetLogger()
defer SetLogger(originalLogger)
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
customLogger := slog.New(handler)
SetLogger(customLogger)
retrievedLogger := GetLogger()
if retrievedLogger != customLogger {
t.Error("Expected GetLogger to return the custom logger")
}
// Verify it's the same instance by logging
retrievedLogger.Info("test")
if !strings.Contains(buf.String(), "test") {
t.Error("Expected retrieved logger to be the same instance")
}
}
// TestGetLoggerFromContext_WithLogger tests retrieving a logger from context.
func TestGetLoggerFromContext_WithLogger(t *testing.T) {
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
contextLogger := slog.New(handler)
// Create context with logger using WithLogger
ctx := context.Background()
kleisli := WithLogger(contextLogger)
result := kleisli(ctx)
ctxWithLogger := pair.Second(result)
// Retrieve logger from context
retrievedLogger := GetLoggerFromContext(ctxWithLogger)
if retrievedLogger != contextLogger {
t.Error("Expected to retrieve the context logger")
}
// Verify it's the same instance by logging
retrievedLogger.Info("context test")
if !strings.Contains(buf.String(), "context test") {
t.Error("Expected retrieved logger to be the same instance")
}
}
// TestGetLoggerFromContext_WithoutLogger tests that it returns global logger when context has no logger.
func TestGetLoggerFromContext_WithoutLogger(t *testing.T) {
originalLogger := GetLogger()
defer SetLogger(originalLogger)
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
globalLogger := slog.New(handler)
SetLogger(globalLogger)
// Create context without logger
ctx := context.Background()
// Should return global logger
retrievedLogger := GetLoggerFromContext(ctx)
if retrievedLogger != globalLogger {
t.Error("Expected to retrieve the global logger when context has no logger")
}
// Verify it's the same instance
retrievedLogger.Info("global test")
if !strings.Contains(buf.String(), "global test") {
t.Error("Expected retrieved logger to be the global logger")
}
}
// TestGetLoggerFromContext_NilContext tests behavior with nil context value.
func TestGetLoggerFromContext_NilContext(t *testing.T) {
originalLogger := GetLogger()
defer SetLogger(originalLogger)
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
globalLogger := slog.New(handler)
SetLogger(globalLogger)
// Create context with wrong type value
ctx := context.WithValue(context.Background(), loggerInContextKey, "not a logger")
// Should return global logger when type assertion fails
retrievedLogger := GetLoggerFromContext(ctx)
if retrievedLogger != globalLogger {
t.Error("Expected to retrieve the global logger when context value is wrong type")
}
}
// TestWithLogger_CreatesContextWithLogger tests that WithLogger adds logger to context.
func TestWithLogger_CreatesContextWithLogger(t *testing.T) {
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
testLogger := slog.New(handler)
// Create Kleisli arrow
kleisli := WithLogger(testLogger)
// Apply to context
ctx := context.Background()
result := kleisli(ctx)
// Verify result is a ContextCancel pair
cancelFunc := pair.First(result)
newCtx := pair.Second(result)
if cancelFunc == nil {
t.Error("Expected cancel function to be non-nil")
}
if newCtx == nil {
t.Error("Expected new context to be non-nil")
}
// Verify logger is in context
retrievedLogger := GetLoggerFromContext(newCtx)
if retrievedLogger != testLogger {
t.Error("Expected logger to be in the new context")
}
}
// TestWithLogger_CancelFuncIsNoop tests that the cancel function is a no-op.
func TestWithLogger_CancelFuncIsNoop(t *testing.T) {
testLogger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
kleisli := WithLogger(testLogger)
ctx := context.Background()
result := kleisli(ctx)
cancelFunc := pair.First(result)
// Calling cancel should not panic
defer func() {
if r := recover(); r != nil {
t.Errorf("Cancel function panicked: %v", r)
}
}()
cancelFunc()
}
// TestWithLogger_PreservesOriginalContext tests that original context is not modified.
func TestWithLogger_PreservesOriginalContext(t *testing.T) {
originalLogger := GetLogger()
defer SetLogger(originalLogger)
var buf bytes.Buffer
handler := slog.NewTextHandler(&buf, nil)
globalLogger := slog.New(handler)
SetLogger(globalLogger)
testLogger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
kleisli := WithLogger(testLogger)
// Original context without logger
originalCtx := context.Background()
// Apply transformation
result := kleisli(originalCtx)
newCtx := pair.Second(result)
// Original context should still return global logger
originalCtxLogger := GetLoggerFromContext(originalCtx)
if originalCtxLogger != globalLogger {
t.Error("Expected original context to still use global logger")
}
// New context should have the test logger
newCtxLogger := GetLoggerFromContext(newCtx)
if newCtxLogger != testLogger {
t.Error("Expected new context to have the test logger")
}
}
// TestWithLogger_Composition tests composing multiple WithLogger calls.
func TestWithLogger_Composition(t *testing.T) {
logger1 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
logger2 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
kleisli1 := WithLogger(logger1)
kleisli2 := WithLogger(logger2)
ctx := context.Background()
// Apply first transformation
result1 := kleisli1(ctx)
ctx1 := pair.Second(result1)
// Verify first logger
if GetLoggerFromContext(ctx1) != logger1 {
t.Error("Expected first logger in context after first transformation")
}
// Apply second transformation (should override)
result2 := kleisli2(ctx1)
ctx2 := pair.Second(result2)
// Verify second logger (should override first)
if GetLoggerFromContext(ctx2) != logger2 {
t.Error("Expected second logger to override first logger")
}
}
// BenchmarkSetLogger benchmarks setting the global logger.
func BenchmarkSetLogger(b *testing.B) {
logger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
b.ResetTimer()
for i := 0; i < b.N; i++ {
SetLogger(logger)
}
}
// BenchmarkGetLogger benchmarks getting the global logger.
func BenchmarkGetLogger(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
GetLogger()
}
}
// BenchmarkGetLoggerFromContext_WithLogger benchmarks retrieving logger from context.
func BenchmarkGetLoggerFromContext_WithLogger(b *testing.B) {
logger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
kleisli := WithLogger(logger)
ctx := pair.Second(kleisli(context.Background()))
b.ResetTimer()
for i := 0; i < b.N; i++ {
GetLoggerFromContext(ctx)
}
}
// BenchmarkGetLoggerFromContext_WithoutLogger benchmarks retrieving global logger from context.
func BenchmarkGetLoggerFromContext_WithoutLogger(b *testing.B) {
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
GetLoggerFromContext(ctx)
}
}
// BenchmarkWithLogger benchmarks creating context with logger.
func BenchmarkWithLogger(b *testing.B) {
logger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
kleisli := WithLogger(logger)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
kleisli(ctx)
}
}

View File

@@ -16,7 +16,10 @@
package logging
import (
"context"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/pair"
)
type (
@@ -39,4 +42,15 @@ type (
// ctx := context.Background()
// newCtx := addLogger(ctx) // Both ctx and newCtx are context.Context
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Pair represents a tuple of two values of types A and B.
// It is used to group two related values together.
Pair[A, B any] = pair.Pair[A, B]
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

52
v2/monoid/types.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package monoid
import "github.com/IBM/fp-go/v2/function"
// Void is an alias for function.Void, representing the unit type.
//
// The Void type (also known as Unit in functional programming) has exactly one value,
// making it useful for representing the absence of meaningful information. It's similar
// to void in other languages, but as a value rather than the absence of a value.
//
// This type alias is provided in the monoid package for convenience when working with
// VoidMonoid and other monoid operations that may use the unit type.
//
// Common use cases:
// - As a return type for functions that perform side effects but don't return meaningful data
// - As a placeholder type parameter when a type is required but no data needs to be passed
// - In monoid operations where you need to track that operations occurred without caring about results
//
// See also:
// - function.Void: The underlying type definition
// - function.VOID: The single inhabitant of the Void type
// - VoidMonoid: A monoid instance for the Void type
//
// Example:
//
// // Function that performs an action but returns no meaningful data
// func logMessage(msg string) Void {
// fmt.Println(msg)
// return function.VOID
// }
//
// // Using Void in monoid operations
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
type (
Void = function.Void
)

65
v2/monoid/void.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package monoid
import (
"github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
// VoidMonoid creates a Monoid for the Void (unit) type.
//
// The Void type has exactly one value (function.VOID), making it trivial to define
// a monoid. This monoid uses the Last semigroup, which always returns the second
// argument, though since all Void values are identical, the choice of semigroup
// doesn't affect the result.
//
// This monoid is useful in contexts where:
// - A monoid instance is required but no meaningful data needs to be combined
// - You need to track that an operation occurred without caring about its result
// - Building generic abstractions that work with any monoid, including the trivial case
//
// # Monoid Laws
//
// The VoidMonoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always VOID
// - Left Identity: Concat(Empty(), x) = x - always VOID
// - Right Identity: Concat(x, Empty()) = x - always VOID
//
// Returns:
// - A Monoid[Void] instance
//
// Example:
//
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
// empty := m.Empty() // function.VOID
//
// // Useful for tracking operations without data
// type Action = func() Void
// actions := []Action{
// func() Void { fmt.Println("Action 1"); return function.VOID },
// func() Void { fmt.Println("Action 2"); return function.VOID },
// }
// // Execute all actions and combine results
// results := A.Map(func(a Action) Void { return a() })(actions)
// _ = ConcatAll(m)(results) // All actions executed, result is VOID
func VoidMonoid() Monoid[Void] {
return MakeMonoid(
S.Last[Void]().Concat,
function.VOID,
)
}

290
v2/monoid/void_test.go Normal file
View File

@@ -0,0 +1,290 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package monoid
import (
"testing"
"github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestVoidMonoid_Basic tests basic VoidMonoid functionality
func TestVoidMonoid_Basic(t *testing.T) {
m := VoidMonoid()
// Test Empty returns VOID
empty := m.Empty()
assert.Equal(t, function.VOID, empty)
// Test Concat returns VOID (since all Void values are identical)
result := m.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
}
// TestVoidMonoid_Laws verifies VoidMonoid satisfies monoid laws
func TestVoidMonoid_Laws(t *testing.T) {
m := VoidMonoid()
// Since Void has only one value, we test with that value
v := function.VOID
// Left Identity: Concat(Empty(), x) = x
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), v)
assert.Equal(t, v, result, "Left identity law failed")
})
// Right Identity: Concat(x, Empty()) = x
t.Run("right identity", func(t *testing.T) {
result := m.Concat(v, m.Empty())
assert.Equal(t, v, result, "Right identity law failed")
})
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(v, v), v)
right := m.Concat(v, m.Concat(v, v))
assert.Equal(t, left, right, "Associativity law failed")
})
// All results should be VOID
t.Run("all operations return VOID", func(t *testing.T) {
assert.Equal(t, function.VOID, m.Concat(v, v))
assert.Equal(t, function.VOID, m.Empty())
assert.Equal(t, function.VOID, m.Concat(m.Empty(), v))
assert.Equal(t, function.VOID, m.Concat(v, m.Empty()))
})
}
// TestVoidMonoid_ConcatAll tests combining multiple Void values
func TestVoidMonoid_ConcatAll(t *testing.T) {
m := VoidMonoid()
concatAll := ConcatAll(m)
tests := []struct {
name string
input []Void
expected Void
}{
{
name: "empty slice",
input: []Void{},
expected: function.VOID,
},
{
name: "single element",
input: []Void{function.VOID},
expected: function.VOID,
},
{
name: "multiple elements",
input: []Void{function.VOID, function.VOID, function.VOID},
expected: function.VOID,
},
{
name: "many elements",
input: make([]Void, 100),
expected: function.VOID,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize slice with VOID values
for i := range tt.input {
tt.input[i] = function.VOID
}
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestVoidMonoid_Fold tests the Fold function with VoidMonoid
func TestVoidMonoid_Fold(t *testing.T) {
m := VoidMonoid()
fold := Fold(m)
// Fold should behave identically to ConcatAll
voids := []Void{function.VOID, function.VOID, function.VOID}
result := fold(voids)
assert.Equal(t, function.VOID, result)
// Empty fold
emptyResult := fold([]Void{})
assert.Equal(t, function.VOID, emptyResult)
}
// TestVoidMonoid_Reverse tests that Reverse doesn't affect VoidMonoid
func TestVoidMonoid_Reverse(t *testing.T) {
m := VoidMonoid()
reversed := Reverse(m)
// Since all Void values are identical, reverse should have no effect
v := function.VOID
assert.Equal(t, m.Concat(v, v), reversed.Concat(v, v))
assert.Equal(t, m.Empty(), reversed.Empty())
// Test identity laws still hold
assert.Equal(t, v, reversed.Concat(reversed.Empty(), v))
assert.Equal(t, v, reversed.Concat(v, reversed.Empty()))
}
// TestVoidMonoid_ToSemigroup tests conversion to Semigroup
func TestVoidMonoid_ToSemigroup(t *testing.T) {
m := VoidMonoid()
sg := ToSemigroup(m)
// Should work as a semigroup
result := sg.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
// Verify it's the same underlying operation
assert.Equal(t, m.Concat(function.VOID, function.VOID), sg.Concat(function.VOID, function.VOID))
}
// TestVoidMonoid_FunctionMonoid tests VoidMonoid with FunctionMonoid
func TestVoidMonoid_FunctionMonoid(t *testing.T) {
m := VoidMonoid()
funcMonoid := FunctionMonoid[string](m)
// Create functions that return Void
f1 := func(s string) Void { return function.VOID }
f2 := func(s string) Void { return function.VOID }
// Combine functions
combined := funcMonoid.Concat(f1, f2)
// Test combined function
result := combined("test")
assert.Equal(t, function.VOID, result)
// Test empty function
emptyFunc := funcMonoid.Empty()
assert.Equal(t, function.VOID, emptyFunc("anything"))
}
// TestVoidMonoid_PracticalUsage demonstrates practical usage patterns
func TestVoidMonoid_PracticalUsage(t *testing.T) {
m := VoidMonoid()
// Simulate tracking that operations occurred without caring about results
type Action func() Void
actions := []Action{
func() Void { return function.VOID }, // Action 1
func() Void { return function.VOID }, // Action 2
func() Void { return function.VOID }, // Action 3
}
// Execute all actions and collect results
results := make([]Void, len(actions))
for i, action := range actions {
results[i] = action()
}
// Combine all results (all are VOID)
finalResult := ConcatAll(m)(results)
assert.Equal(t, function.VOID, finalResult)
}
// TestVoidMonoid_EdgeCases tests edge cases
func TestVoidMonoid_EdgeCases(t *testing.T) {
m := VoidMonoid()
t.Run("multiple concatenations", func(t *testing.T) {
// Chain multiple Concat operations
result := m.Concat(
m.Concat(
m.Concat(function.VOID, function.VOID),
function.VOID,
),
function.VOID,
)
assert.Equal(t, function.VOID, result)
})
t.Run("concat with empty", func(t *testing.T) {
// Various combinations with Empty()
assert.Equal(t, function.VOID, m.Concat(m.Empty(), m.Empty()))
assert.Equal(t, function.VOID, m.Concat(m.Concat(m.Empty(), function.VOID), m.Empty()))
})
t.Run("large slice", func(t *testing.T) {
// Test with a large number of elements
largeSlice := make([]Void, 10000)
for i := range largeSlice {
largeSlice[i] = function.VOID
}
result := ConcatAll(m)(largeSlice)
assert.Equal(t, function.VOID, result)
})
}
// TestVoidMonoid_TypeSafety verifies type safety
func TestVoidMonoid_TypeSafety(t *testing.T) {
m := VoidMonoid()
// Verify it implements Monoid interface
var _ Monoid[Void] = m
// Verify Empty returns correct type
empty := m.Empty()
var _ Void = empty
// Verify Concat returns correct type
result := m.Concat(function.VOID, function.VOID)
var _ Void = result
}
// BenchmarkVoidMonoid_Concat benchmarks the Concat operation
func BenchmarkVoidMonoid_Concat(b *testing.B) {
m := VoidMonoid()
v := function.VOID
b.ResetTimer()
for b.Loop() {
_ = m.Concat(v, v)
}
}
// BenchmarkVoidMonoid_ConcatAll benchmarks combining multiple Void values
func BenchmarkVoidMonoid_ConcatAll(b *testing.B) {
m := VoidMonoid()
concatAll := ConcatAll(m)
voids := make([]Void, 1000)
for i := range voids {
voids[i] = function.VOID
}
b.ResetTimer()
for b.Loop() {
_ = concatAll(voids)
}
}
// BenchmarkVoidMonoid_Empty benchmarks the Empty operation
func BenchmarkVoidMonoid_Empty(b *testing.B) {
m := VoidMonoid()
b.ResetTimer()
for b.Loop() {
_ = m.Empty()
}
}

View File

@@ -62,7 +62,7 @@ func TestMonadAltBasicFunctionality(t *testing.T) {
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "HELLO", value)
})
@@ -105,7 +105,7 @@ func TestMonadAltBasicFunctionality(t *testing.T) {
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, -5, value)
})
@@ -302,19 +302,19 @@ func TestAltOperator(t *testing.T) {
// Test with "42" - should use base codec
result1 := pipeline.Decode("42")
assert.True(t, either.IsRight(result1))
value1 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result1)
value1 := either.GetOrElse(reader.Of[validation.Errors](0))(result1)
assert.Equal(t, 42, value1)
// Test with "100" - should use fallback1
result2 := pipeline.Decode("100")
assert.True(t, either.IsRight(result2))
value2 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result2)
value2 := either.GetOrElse(reader.Of[validation.Errors](0))(result2)
assert.Equal(t, 100, value2)
// Test with "999" - should use fallback2
result3 := pipeline.Decode("999")
assert.True(t, either.IsRight(result3))
value3 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result3)
value3 := either.GetOrElse(reader.Of[validation.Errors](0))(result3)
assert.Equal(t, 999, value3)
})
}
@@ -449,7 +449,7 @@ func TestAltRoundTrip(t *testing.T) {
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
// Encode
encoded := altCodec.Encode(decoded)
@@ -487,7 +487,7 @@ func TestAltRoundTrip(t *testing.T) {
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
// Encode (uses first codec's encoder, which is identity)
encoded := altCodec.Encode(decoded)
@@ -619,7 +619,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, 10, value, "first success should win")
})
@@ -628,7 +628,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("42")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, 42, value)
})
@@ -637,7 +637,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("invalid")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
assert.Equal(t, 0, value, "should use default zero value")
})
})
@@ -768,21 +768,21 @@ func TestAltMonoid(t *testing.T) {
t.Run("uses primary when it succeeds", func(t *testing.T) {
result := combined.Decode("primary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "from primary", value)
})
t.Run("uses secondary when primary fails", func(t *testing.T) {
result := combined.Decode("secondary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "from secondary", value)
})
t.Run("uses default when both fail", func(t *testing.T) {
result := combined.Decode("other")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "default", value)
})
})
@@ -841,7 +841,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
// Empty (0) comes first, so it wins
assert.Equal(t, 0, value)
})
@@ -852,7 +852,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
assert.Equal(t, 10, value, "codec1 should win")
})
@@ -867,8 +867,8 @@ func TestAltMonoid(t *testing.T) {
assert.True(t, either.IsRight(resultLeft))
assert.True(t, either.IsRight(resultRight))
valueLeft := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultRight)
valueLeft := either.GetOrElse(reader.Of[validation.Errors](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors](-1))(resultRight)
// Both should return 10 (first success)
assert.Equal(t, valueLeft, valueRight)

505
v2/optics/codec/bind.go Normal file
View File

@@ -0,0 +1,505 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codec
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/semigroup"
)
// Do creates the initial empty codec to be used as the starting point for
// do-notation style codec construction.
//
// This is the entry point for building up a struct codec field-by-field using
// the applicative and monadic sequencing operators ApSL, ApSO, and Bind.
// It wraps Empty and lifts a lazily-evaluated default Pair[O, A] into a
// Type[A, O, I] that ignores its input and always succeeds with the default value.
//
// # Type Parameters
//
// - I: The input type for decoding (what the codec reads from)
// - A: The target struct type being built up (what the codec decodes to)
// - O: The output type for encoding (what the codec writes to)
//
// # Parameters
//
// - e: A Lazy[Pair[O, A]] providing the initial default values:
// - pair.Head(e()): The default encoded output O (e.g. an empty monoid value)
// - pair.Tail(e()): The initial zero value of the struct A (e.g. MyStruct{})
//
// # Returns
//
// - A Type[A, O, I] that always decodes to the default A and encodes to the
// default O, regardless of input. This is then transformed by chaining
// ApSL, ApSO, or Bind operators to add fields one by one.
//
// # Example Usage
//
// Building a struct codec using do-notation style:
//
// import (
// "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/lazy"
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/lens"
// "github.com/IBM/fp-go/v2/pair"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Age int
// }
//
// nameLens := lens.MakeLens(
// func(p Person) string { return p.Name },
// func(p Person, name string) Person { p.Name = name; return p },
// )
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, age int) Person { p.Age = age; return p },
// )
//
// personCodec := F.Pipe2(
// codec.Do[any, Person, string](lazy.Of(pair.MakePair("", Person{}))),
// codec.ApSL(S.Monoid, nameLens, codec.String()),
// codec.ApSL(S.Monoid, ageLens, codec.Int()),
// )
//
// # Notes
//
// - Do is typically the first call in a codec pipeline, followed by ApSL, ApSO, or Bind
// - The lazy pair should use the monoid's empty value for O and the zero value for A
// - For convenience, use Struct to create the initial codec for named struct types
//
// # See Also
//
// - Empty: The underlying codec constructor that Do delegates to
// - ApSL: Applicative sequencing for required struct fields via Lens
// - ApSO: Applicative sequencing for optional struct fields via Optional
// - Bind: Monadic sequencing for context-dependent field codecs
//
//go:inline
func Do[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
return Empty[I](e)
}
// ApSL creates an applicative sequencing operator for codecs using a lens.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
// allowing you to build up complex codecs by combining a base codec with a field
// accessed through a lens. It's particularly useful for building struct codecs
// field-by-field in a composable way.
//
// The function combines:
// - Encoding: Extracts the field value using the lens, encodes it with fa, and
// combines it with the base encoding using the monoid
// - Validation: Validates the field using the lens and combines the validation
// with the base validation
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The field type accessed by the lens
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - l: A Lens[S, T] that focuses on a specific field in S
// - fa: A Type[T, O, I] codec for the field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the field
// specified by the lens.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Extract the field T using l.Get
// - Encode T to O using fa.Encode
// - Combine with the base encoding using the monoid
//
// 2. **Validation**: When validating input I:
// - Validate the field using fa.Validate through the lens
// - Combine with the base validation
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/lens"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Age int
// }
//
// // Lenses for Person fields
// nameLens := lens.MakeLens(
// func(p *Person) string { return p.Name },
// func(p *Person, name string) *Person { p.Name = name; return p },
// )
//
// // Build a Person codec field by field
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSL(S.Monoid, nameLens, codec.String),
// // ... add more fields
// )
//
// # Use Cases
//
// - Building struct codecs incrementally
// - Composing codecs for nested structures
// - Creating type-safe serialization/deserialization
// - Implementing Do-notation style codec construction
//
// # Notes
//
// - The monoid determines how encoded outputs are combined
// - The lens must be total (handle all cases safely)
// - This is typically used with other ApS functions to build complete codecs
// - The name is automatically generated for debugging purposes
//
// See also:
// - validate.ApSL: The underlying validation combinator
// - reader.ApplicativeMonoid: The monoid-based applicative instance
// - Lens: The optic for accessing struct fields
func ApSL[S, T, O, I any](
m Monoid[O],
l Lens[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", l, fa)
rm := reader.ApplicativeMonoid[S](m)
encConcat := F.Pipe1(
F.Flow2(
l.Get,
fa.Encode,
),
semigroup.AppendTo(rm),
)
valConcat := validate.ApSL(l, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
encConcat(t.Encode),
)
}
}
// ApSO creates an applicative sequencing operator for codecs using an optional.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs
// with optional fields, allowing you to build up complex codecs by combining a base
// codec with a field that may or may not be present. It's particularly useful for
// building struct codecs with optional fields in a composable way.
//
// The function combines:
// - Encoding: Attempts to extract the optional field value, encodes it if present,
// and combines it with the base encoding using the monoid. If the field is absent,
// only the base encoding is used.
// - Validation: Validates the optional field and combines the validation with the
// base validation using applicative semantics (error accumulation).
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The optional field type accessed by the optional
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - o: An Optional[S, T] that focuses on a field in S that may not exist
// - fa: A Type[T, O, I] codec for the optional field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the optional field
// specified by the optional.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Try to extract the optional field T using o.GetOption
// - If present (Some(T)): Encode T to O using fa.Encode and combine with base using monoid
// - If absent (None): Return only the base encoding unchanged
//
// 2. **Validation**: When validating input I:
// - Validate the optional field using fa.Validate through o.Set
// - Combine with the base validation using applicative semantics
// - Accumulates all validation errors from both base and field
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Difference from ApSL
//
// Unlike ApSL which works with required fields via Lens, ApSO handles optional fields:
// - ApSL: Field always exists, always encoded
// - ApSO: Field may not exist, only encoded when present
// - ApSO uses Optional.GetOption which returns Option[T]
// - ApSO gracefully handles missing fields without errors
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/optional"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Nickname *string // Optional field
// }
//
// // Optional for Person.Nickname
// nicknameOpt := optional.MakeOptional(
// func(p Person) option.Option[string] {
// if p.Nickname != nil {
// return option.Some(*p.Nickname)
// }
// return option.None[string]()
// },
// func(p Person, nick string) Person {
// p.Nickname = &nick
// return p
// },
// )
//
// // Build a Person codec with optional nickname
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
// )
//
// // Encoding with nickname present
// p1 := Person{Name: "Alice", Nickname: ptr("Ali")}
// encoded1 := personCodec.Encode(p1) // Includes nickname
//
// // Encoding with nickname absent
// p2 := Person{Name: "Bob", Nickname: nil}
// encoded2 := personCodec.Encode(p2) // No nickname in output
//
// # Use Cases
//
// - Building struct codecs with optional/nullable fields
// - Handling pointer fields that may be nil
// - Composing codecs for structures with optional nested data
// - Creating flexible serialization that omits absent fields
//
// # Notes
//
// - The monoid determines how encoded outputs are combined when field is present
// - When the optional field is absent, encoding returns base encoding unchanged
// - Validation still accumulates errors even for optional fields
// - The name is automatically generated for debugging purposes
//
// # See Also
//
// - ApSL: For required fields using Lens
// - validate.ApS: The underlying validation combinator
// - Optional: The optic for accessing optional fields
func ApSO[S, T, O, I any](
m Monoid[O],
o Optional[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", o, fa)
encConcat := F.Flow2(
o.GetOption,
option.Map(F.Flow2(
fa.Encode,
semigroup.AppendTo(m),
)),
)
valConcat := validate.ApS(o.Set, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
func(s S) O {
to := t.Encode(s)
return F.Pipe2(
encConcat(s),
option.Flap[O](to),
option.GetOrElse(lazy.Of(to)),
)
},
)
}
}
// Bind creates a monadic sequencing operator for codecs using a lens and a Kleisli arrow.
//
// This function implements the "Bind" (monadic bind / chain) pattern for codecs,
// allowing you to build up complex codecs where the codec for a field depends on
// the current decoded value of the struct. Unlike ApSL which uses a fixed field
// codec, Bind accepts a Kleisli arrow — a function from the current struct value S
// to a Type[T, O, I] — enabling context-sensitive codec construction.
//
// The function combines:
// - Encoding: Evaluates the Kleisli arrow f on the current struct value s to obtain
// the field codec, extracts the field T using the lens, encodes it with that codec,
// and combines it with the base encoding using the monoid.
// - Validation: Validates the base struct first (monadic sequencing), then uses the
// Kleisli arrow to obtain the field codec for the decoded struct value, and validates
// the field through the lens. Errors are propagated but NOT accumulated (fail-fast
// semantics, unlike ApSL which accumulates errors).
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The field type accessed by the lens
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - l: A Lens[S, T] that focuses on a specific field in S
// - f: A Kleisli[S, T, O, I] — a function from S to Type[T, O, I] — that produces
// the field codec based on the current struct value
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the field
// specified by the lens, where the field codec is determined by the Kleisli arrow.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Evaluate f(s) to obtain the field codec fa
// - Extract the field T using l.Get
// - Encode T to O using fa.Encode
// - Combine with the base encoding using the monoid
//
// 2. **Validation**: When validating input I:
// - Run the base validation to obtain a decoded S (fail-fast: stop on base failure)
// - For the decoded S, evaluate f(s) to obtain the field codec fa
// - Validate the input I using fa.Validate
// - Set the validated T into S using l.Set
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Difference from ApSL
//
// Unlike ApSL which uses a fixed field codec:
// - ApSL: Field codec is fixed at construction time; errors are accumulated
// - Bind: Field codec depends on the current struct value (Kleisli arrow); validation
// uses monadic sequencing (fail-fast on base failure)
// - Bind is more powerful but less parallel than ApSL
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/lens"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Config struct {
// Mode string
// Value int
// }
//
// modeLens := lens.MakeLens(
// func(c Config) string { return c.Mode },
// func(c Config, mode string) Config { c.Mode = mode; return c },
// )
//
// // Build a Config codec where the Value codec depends on the Mode
// configCodec := F.Pipe1(
// codec.Struct[Config]("Config"),
// codec.Bind(S.Monoid, modeLens, func(c Config) codec.Type[string, string, any] {
// return codec.String()
// }),
// )
//
// # Use Cases
//
// - Building codecs where a field's codec depends on another field's value
// - Implementing discriminated unions or tagged variants
// - Context-sensitive validation (e.g., validate field B differently based on field A)
// - Dependent type-like patterns in codec construction
//
// # Notes
//
// - The monoid determines how encoded outputs are combined
// - The lens must be total (handle all cases safely)
// - Validation uses monadic (fail-fast) sequencing: if the base codec fails,
// the Kleisli arrow is never evaluated
// - The name is automatically generated for debugging purposes
//
// See also:
// - ApSL: Applicative sequencing with a fixed lens codec (error accumulation)
// - Kleisli: The function type from S to Type[T, O, I]
// - validate.Bind: The underlying validate-level bind combinator
func Bind[S, T, O, I any](
m Monoid[O],
l Lens[S, T],
f Kleisli[S, T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("Bind[%s]", l)
val := F.Curry2(Type[T, O, I].Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
validate.Bind(l.Set, F.Flow2(f, val)),
),
func(s S) O {
return m.Concat(t.Encode(s), f(s).Encode(l.Get(s)))
},
)
}
}

1401
v2/optics/codec/bind_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
@@ -100,7 +101,7 @@ func (t *typeImpl[A, O, I]) Is(i any) Result[A] {
// stringToInt := codec.MakeType(...) // Type[int, string, string]
// intToPositive := codec.MakeType(...) // Type[PositiveInt, int, int]
// composed := codec.Pipe(intToPositive)(stringToInt) // Type[PositiveInt, string, string]
func Pipe[A, B, O, I any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
func Pipe[O, I, A, B any](ab Type[B, A, A]) Operator[A, B, O, I] {
return func(this Type[A, O, I]) Type[B, O, I] {
return MakeType(
fmt.Sprintf("Pipe(%s, %s)", this.Name(), ab.Name()),
@@ -747,3 +748,114 @@ func FromRefinement[A, B any](refinement Refinement[A, B]) Type[B, A, A] {
refinement.ReverseGet,
)
}
// Empty creates a Type codec that ignores input during decoding and uses a default value,
// and ignores the value during encoding, using a default output.
//
// This codec is useful for:
// - Providing default values for optional fields
// - Creating placeholder codecs in generic contexts
// - Implementing constant codecs that always produce the same value
// - Building codecs for phantom types or unit-like types
//
// The codec uses a lazily-evaluated Pair[O, A] to provide both the default output
// for encoding and the default value for decoding. The lazy evaluation ensures that
// the defaults are only computed when needed.
//
// # Type Parameters
//
// - A: The target type (what we decode to and encode from)
// - O: The output type (what we encode to)
// - I: The input type (what we decode from, but is ignored)
//
// # Parameters
//
// - e: A Lazy[Pair[O, A]] that provides the default values:
// - pair.Head(e()): The default output value O used during encoding
// - pair.Tail(e()): The default decoded value A used during decoding
//
// # Returns
//
// - A Type[A, O, I] that:
// - Decode: Always succeeds and returns the default value A, ignoring input I
// - Encode: Always returns the default output O, ignoring the input value A
// - Is: Checks if a value is of type A (standard type checking)
// - Name: Returns "Empty"
//
// # Behavior
//
// Decoding:
// - Ignores the input value completely
// - Always succeeds with validation.Success
// - Returns the default value from pair.Tail(e())
//
// Encoding:
// - Ignores the input value completely
// - Always returns the default output from pair.Head(e())
//
// # Example Usage
//
// Creating a codec with default values:
//
// // Create a codec that always decodes to 42 and encodes to "default"
// defaultCodec := codec.Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
//
// // Decode always returns 42, regardless of input
// result := defaultCodec.Decode("anything") // Success: Right(42)
// result = defaultCodec.Decode(123) // Success: Right(42)
// result = defaultCodec.Decode(nil) // Success: Right(42)
//
// // Encode always returns "default", regardless of input
// encoded := defaultCodec.Encode(100) // Returns: "default"
// encoded = defaultCodec.Encode(0) // Returns: "default"
//
// Using with struct fields for default values:
//
// type Config struct {
// Timeout int
// Retries int
// }
//
// // Codec that provides default retries value
// defaultRetries := codec.Empty[int, int, any](lazy.Of(pair.MakePair(3, 3)))
//
// configCodec := F.Pipe2(
// codec.Struct[Config]("Config"),
// codec.ApSL(S.Monoid, timeoutLens, codec.Int()),
// codec.ApSL(S.Monoid, retriesLens, defaultRetries),
// )
//
// Creating a unit-like codec:
//
// // Codec for a unit type that always produces Void
// unitCodec := codec.Empty[function.Void, function.Void, any](
// lazy.Of(pair.MakePair(function.VOID, function.VOID)),
// )
//
// # Use Cases
//
// - Default values: Provide fallback values when decoding optional fields
// - Constant codecs: Always produce the same value regardless of input
// - Placeholder codecs: Use in generic contexts where a codec is required but not used
// - Unit types: Encode/decode unit-like types that carry no information
// - Testing: Create simple codecs for testing codec composition
//
// # Notes
//
// - The lazy evaluation of the Pair ensures defaults are only computed when needed
// - Both encoding and decoding always succeed (no validation errors)
// - The input values are completely ignored in both directions
// - The Is method still performs standard type checking for type A
// - This codec is useful in applicative composition where some fields have defaults
//
// See also:
// - Id: For identity codecs that preserve values
// - MakeType: For creating custom codecs with validation logic
func Empty[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
return MakeType(
"Empty",
Is[A](),
validate.OfLazy[I](F.Pipe1(e, lazy.Map(pair.Tail[O, A]))),
reader.OfLazy[A](F.Pipe1(e, lazy.Map(pair.Head[O, A]))),
)
}

View File

@@ -7,9 +7,11 @@ import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -19,12 +21,7 @@ func TestString(t *testing.T) {
stringType := String()
result := stringType.Decode("hello")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "hello", value)
assert.Equal(t, validation.Of("hello"), result)
})
t.Run("fails to decode non-string", func(t *testing.T) {
@@ -57,12 +54,7 @@ func TestString(t *testing.T) {
stringType := String()
result := stringType.Decode("")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "error" },
F.Identity[string],
)
assert.Equal(t, "", value)
assert.Equal(t, validation.Of(""), result)
})
}
@@ -71,12 +63,7 @@ func TestInt(t *testing.T) {
intType := Int()
result := intType.Decode(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, validation.Of(42), result)
})
t.Run("fails to decode string as int", func(t *testing.T) {
@@ -109,24 +96,14 @@ func TestInt(t *testing.T) {
intType := Int()
result := intType.Decode(-42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, -42, value)
assert.Equal(t, validation.Of(-42), result)
})
t.Run("decodes zero", func(t *testing.T) {
intType := Int()
result := intType.Decode(0)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
assert.Equal(t, validation.Of(0), result)
})
}
@@ -135,24 +112,14 @@ func TestBool(t *testing.T) {
boolType := Bool()
result := boolType.Decode(true)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) bool { return false },
F.Identity[bool],
)
assert.Equal(t, true, value)
assert.Equal(t, validation.Of(true), result)
})
t.Run("decodes false", func(t *testing.T) {
boolType := Bool()
result := boolType.Decode(false)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) bool { return true },
F.Identity[bool],
)
assert.Equal(t, false, value)
assert.Equal(t, validation.Of(false), result)
})
t.Run("fails to decode int as bool", func(t *testing.T) {
@@ -189,36 +156,21 @@ func TestArray(t *testing.T) {
intArray := Array(Int())
result := intArray.Decode([]int{1, 2, 3})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
})
t.Run("decodes valid string array", func(t *testing.T) {
stringArray := Array(String())
result := stringArray.Decode([]string{"a", "b", "c"})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []string { return nil },
F.Identity[[]string],
)
assert.Equal(t, []string{"a", "b", "c"}, value)
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
})
t.Run("decodes empty array", func(t *testing.T) {
intArray := Array(Int())
result := intArray.Decode([]int{})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{}, value)
assert.Equal(t, validation.Of([]int{}), result)
})
t.Run("fails when array contains invalid element", func(t *testing.T) {
@@ -256,12 +208,7 @@ func TestArray(t *testing.T) {
nestedArray := Array(Array(Int()))
result := nestedArray.Decode([][]int{{1, 2}, {3, 4}})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) [][]int { return nil },
F.Identity[[][]int],
)
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
})
t.Run("fails to decode non-iterable", func(t *testing.T) {
@@ -275,12 +222,7 @@ func TestArray(t *testing.T) {
boolArray := Array(Bool())
result := boolArray.Decode([]bool{true, false, true})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []bool { return nil },
F.Identity[[]bool],
)
assert.Equal(t, []bool{true, false, true}, value)
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
})
t.Run("collects multiple validation errors", func(t *testing.T) {
@@ -360,24 +302,14 @@ func TestTranscodeArray(t *testing.T) {
intTranscode := TranscodeArray(Int())
result := intTranscode.Decode([]any{1, 2, 3})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
})
t.Run("decodes valid string array from string slice", func(t *testing.T) {
stringTranscode := TranscodeArray(String())
result := stringTranscode.Decode([]any{"a", "b", "c"})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []string { return nil },
F.Identity[[]string],
)
assert.Equal(t, []string{"a", "b", "c"}, value)
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
})
t.Run("decodes empty array", func(t *testing.T) {
@@ -411,24 +343,14 @@ func TestTranscodeArray(t *testing.T) {
nestedTranscode := TranscodeArray(TranscodeArray(Int()))
result := nestedTranscode.Decode([][]any{{1, 2}, {3, 4}})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) [][]int { return nil },
F.Identity[[][]int],
)
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
})
t.Run("decodes array of bools", func(t *testing.T) {
boolTranscode := TranscodeArray(Bool())
result := boolTranscode.Decode([]any{true, false, true})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []bool { return nil },
F.Identity[[]bool],
)
assert.Equal(t, []bool{true, false, true}, value)
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
})
t.Run("encodes empty array", func(t *testing.T) {
@@ -481,12 +403,7 @@ func TestTranscodeArrayWithTransformation(t *testing.T) {
arrayTranscode := TranscodeArray(stringToInt)
result := arrayTranscode.Decode([]string{"a", "bb", "ccc"})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
})
t.Run("encodes int slice to string slice", func(t *testing.T) {
@@ -1358,24 +1275,14 @@ func TestId(t *testing.T) {
idCodec := Id[string]()
result := idCodec.Decode("hello")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "hello", value)
assert.Equal(t, validation.Of("hello"), result)
})
t.Run("decodes int successfully", func(t *testing.T) {
idCodec := Id[int]()
result := idCodec.Decode(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, validation.Of(42), result)
})
t.Run("encodes with identity function", func(t *testing.T) {
@@ -1431,13 +1338,7 @@ func TestId(t *testing.T) {
person := Person{Name: "Alice", Age: 30}
result := idCodec.Decode(person)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Person { return Person{} },
F.Identity[Person],
)
assert.Equal(t, person, value)
assert.Equal(t, validation.Of(person), result)
encoded := idCodec.Encode(person)
assert.Equal(t, person, encoded)
@@ -1450,13 +1351,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
arrayCodec := TranscodeArray(intId)
result := arrayCodec.Decode([]int{1, 2, 3, 4, 5})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3, 4, 5}, value)
assert.Equal(t, validation.Of([]int{1, 2, 3, 4, 5}), result)
})
t.Run("Id codec encodes array with identity", func(t *testing.T) {
@@ -1473,13 +1368,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
input := [][]int{{1, 2}, {3, 4}, {5}}
result := nestedCodec.Decode(input)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) [][]int { return nil },
F.Identity[[][]int],
)
assert.Equal(t, input, value)
assert.Equal(t, validation.Of(input), result)
})
}
@@ -1748,7 +1637,7 @@ func TestFromRefinementComposition(t *testing.T) {
positiveCodec := FromRefinement(positiveIntPrism)
// Compose with Int codec using Pipe
composed := Pipe[int, int, int, any](positiveCodec)(Int())
composed := Pipe[int, any](positiveCodec)(Int())
t.Run("ComposedDecodeValid", func(t *testing.T) {
result := composed.Decode(42)
@@ -1849,3 +1738,416 @@ func TestFromRefinementValidationContext(t *testing.T) {
assert.Equal(t, -5, err.Value)
})
}
// TestEmpty_Success tests that Empty always succeeds during decoding
func TestEmpty_Success(t *testing.T) {
t.Run("decodes any input to default value", func(t *testing.T) {
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
// Test with various input types
testCases := []struct {
name string
input any
}{
{"string input", "anything"},
{"int input", 123},
{"nil input", nil},
{"bool input", true},
{"struct input", struct{ X int }{X: 10}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := defaultCodec.Decode(tc.input)
assert.Equal(t, validation.Of(42), result)
})
}
})
t.Run("always returns same default value", func(t *testing.T) {
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("output", "default")))
result1 := defaultCodec.Decode(123)
result2 := defaultCodec.Decode("different")
result3 := defaultCodec.Decode(nil)
assert.True(t, either.IsRight(result1))
assert.True(t, either.IsRight(result2))
assert.True(t, either.IsRight(result3))
value1 := either.MonadFold(result1, func(validation.Errors) string { return "" }, F.Identity[string])
value2 := either.MonadFold(result2, func(validation.Errors) string { return "" }, F.Identity[string])
value3 := either.MonadFold(result3, func(validation.Errors) string { return "" }, F.Identity[string])
assert.Equal(t, "default", value1)
assert.Equal(t, "default", value2)
assert.Equal(t, "default", value3)
})
}
// TestEmpty_Encoding tests that Empty always uses default output during encoding
func TestEmpty_Encoding(t *testing.T) {
t.Run("encodes any value to default output", func(t *testing.T) {
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
// Test with various input values
testCases := []struct {
name string
input int
}{
{"zero value", 0},
{"positive value", 100},
{"negative value", -50},
{"default value", 42},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
encoded := defaultCodec.Encode(tc.input)
assert.Equal(t, "default", encoded)
})
}
})
t.Run("always returns same default output", func(t *testing.T) {
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "ignored")))
encoded1 := defaultCodec.Encode("value1")
encoded2 := defaultCodec.Encode("value2")
encoded3 := defaultCodec.Encode("")
assert.Equal(t, 999, encoded1)
assert.Equal(t, 999, encoded2)
assert.Equal(t, 999, encoded3)
})
}
// TestEmpty_Name tests that Empty has correct name
func TestEmpty_Name(t *testing.T) {
t.Run("has name 'Empty'", func(t *testing.T) {
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(0, 0)))
assert.Equal(t, "Empty", defaultCodec.Name())
})
}
// TestEmpty_TypeChecking tests that Empty performs standard type checking
func TestEmpty_TypeChecking(t *testing.T) {
t.Run("Is checks for correct type", func(t *testing.T) {
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
// Should succeed for int
result := defaultCodec.Is(100)
assert.True(t, either.IsRight(result))
// Should fail for non-int
result = defaultCodec.Is("not an int")
assert.True(t, either.IsLeft(result))
})
t.Run("Is checks for string type", func(t *testing.T) {
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("out", "in")))
// Should succeed for string
result := defaultCodec.Is("hello")
assert.True(t, either.IsRight(result))
// Should fail for non-string
result = defaultCodec.Is(123)
assert.True(t, either.IsLeft(result))
})
}
// TestEmpty_LazyEvaluation tests that the Pair parameter allows dynamic values
func TestEmpty_LazyEvaluation(t *testing.T) {
t.Run("lazy pair allows dynamic values", func(t *testing.T) {
counter := 0
lazyPair := func() pair.Pair[int, int] {
counter++
return pair.MakePair(counter, counter*10)
}
defaultCodec := Empty[any, int, int](lazyPair)
// Each decode can get a different value if the lazy function is dynamic
result1 := defaultCodec.Decode("input1")
value1 := either.MonadFold(result1,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
result2 := defaultCodec.Decode("input2")
value2 := either.MonadFold(result2,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
// Values can be different if lazy function produces different results
assert.True(t, value1 > 0)
assert.True(t, value2 > 0)
})
}
// TestEmpty_WithStructs tests Empty with struct types
func TestEmpty_WithStructs(t *testing.T) {
type Config struct {
Timeout int
Retries int
}
t.Run("provides default struct value", func(t *testing.T) {
defaultConfig := Config{Timeout: 30, Retries: 3}
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, 30, value.Timeout)
assert.Equal(t, 3, value.Retries)
})
t.Run("encodes to default struct", func(t *testing.T) {
defaultConfig := Config{Timeout: 30, Retries: 3}
inputConfig := Config{Timeout: 60, Retries: 5}
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
encoded := defaultCodec.Encode(inputConfig)
assert.Equal(t, 30, encoded.Timeout)
assert.Equal(t, 3, encoded.Retries)
})
}
// TestEmpty_WithPointers tests Empty with pointer types
func TestEmpty_WithPointers(t *testing.T) {
t.Run("provides default pointer value", func(t *testing.T) {
defaultValue := 42
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(&defaultValue, &defaultValue)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) *int { return nil },
F.Identity[*int],
)
require.NotNil(t, value)
assert.Equal(t, 42, *value)
})
t.Run("provides nil pointer as default", func(t *testing.T) {
var nilPtr *int
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(nilPtr, nilPtr)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) *int { return new(int) },
F.Identity[*int],
)
assert.Nil(t, value)
})
}
// TestEmpty_WithSlices tests Empty with slice types
func TestEmpty_WithSlices(t *testing.T) {
t.Run("provides default slice value", func(t *testing.T) {
defaultSlice := []int{1, 2, 3}
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(defaultSlice, defaultSlice)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{1, 2, 3}, value)
})
t.Run("provides empty slice as default", func(t *testing.T) {
emptySlice := []int{}
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(emptySlice, emptySlice)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) []int { return nil },
F.Identity[[]int],
)
assert.Equal(t, []int{}, value)
})
}
// TestEmpty_DifferentInputOutput tests Empty with different input and output types
func TestEmpty_DifferentInputOutput(t *testing.T) {
t.Run("decodes to int, encodes to string", func(t *testing.T) {
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default-output", 42)))
// Decode always returns 42
result := defaultCodec.Decode("any input")
assert.Equal(t, validation.Of(42), result)
// Encode always returns "default-output"
encoded := defaultCodec.Encode(100)
assert.Equal(t, "default-output", encoded)
})
t.Run("decodes to string, encodes to int", func(t *testing.T) {
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "default-value")))
// Decode always returns "default-value"
result := defaultCodec.Decode(123)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "default-value", value)
// Encode always returns 999
encoded := defaultCodec.Encode("any string")
assert.Equal(t, 999, encoded)
})
}
// TestEmpty_EdgeCases tests edge cases for Empty
func TestEmpty_EdgeCases(t *testing.T) {
t.Run("with zero values", func(t *testing.T) {
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(0, 0)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
encoded := defaultCodec.Encode(100)
assert.Equal(t, 0, encoded)
})
t.Run("with empty string", func(t *testing.T) {
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("", "")))
result := defaultCodec.Decode("non-empty")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) string { return "error" },
F.Identity[string],
)
assert.Equal(t, "", value)
encoded := defaultCodec.Encode("non-empty")
assert.Equal(t, "", encoded)
})
t.Run("with false boolean", func(t *testing.T) {
defaultCodec := Empty[any, bool, bool](lazy.Of(pair.MakePair(false, false)))
result := defaultCodec.Decode(true)
assert.Equal(t, validation.Of(false), result)
encoded := defaultCodec.Encode(true)
assert.Equal(t, false, encoded)
})
}
// TestEmpty_Integration tests Empty in composition scenarios
func TestEmpty_Integration(t *testing.T) {
t.Run("composes with other codecs using Pipe", func(t *testing.T) {
// Create a codec that always provides a default int
defaultIntCodec := Empty[any, int, int](lazy.Of(pair.MakePair(42, 42)))
// Create a refinement that only accepts positive integers
positiveIntPrism := prism.MakePrismWithName(
func(n int) option.Option[int] {
if n > 0 {
return option.Some(n)
}
return option.None[int]()
},
func(n int) int { return n },
"PositiveInt",
)
positiveCodec := FromRefinement(positiveIntPrism)
// Compose: always decode to 42, then validate it's positive
composed := Pipe[int, any](positiveCodec)(defaultIntCodec)
// Should succeed because 42 is positive
result := composed.Decode("anything")
assert.Equal(t, validation.Of(42), result)
})
t.Run("used as placeholder in generic contexts", func(t *testing.T) {
// Empty can be used where a codec is required but not actually used
unitCodec := Empty[any, Void, Void](
lazy.Of(pair.MakePair(F.VOID, F.VOID)),
)
result := unitCodec.Decode("ignored")
assert.Equal(t, validation.Of(F.VOID), result)
encoded := unitCodec.Encode(F.VOID)
assert.Equal(t, F.VOID, encoded)
})
}
// TestEmpty_RoundTrip tests that Empty maintains consistency
func TestEmpty_RoundTrip(t *testing.T) {
t.Run("decode then encode returns default output", func(t *testing.T) {
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("output", 42)))
// Decode
result := defaultCodec.Decode("input")
require.True(t, either.IsRight(result))
decoded := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
// Encode
encoded := defaultCodec.Encode(decoded)
// Should get default output, not related to decoded value
assert.Equal(t, "output", encoded)
})
t.Run("multiple round trips are consistent", func(t *testing.T) {
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(100, 50)))
// First round trip
result1 := defaultCodec.Decode("input1")
decoded1 := either.MonadFold(result1,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
encoded1 := defaultCodec.Encode(decoded1)
// Second round trip
result2 := defaultCodec.Decode("input2")
decoded2 := either.MonadFold(result2,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
encoded2 := defaultCodec.Encode(decoded2)
// All decoded values should be the same
assert.Equal(t, 50, decoded1)
assert.Equal(t, 50, decoded2)
// All encoded values should be the same
assert.Equal(t, 100, encoded1)
assert.Equal(t, 100, encoded2)
})
}

Some files were not shown because too many files have changed in this diff Show More