mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-10 13:31:01 +02:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6d30bb642 | ||
|
|
1821f00fbe | ||
|
|
f0ec0b2541 | ||
|
|
ce3c7d9359 | ||
|
|
3ed354cc8c | ||
|
|
0932c8c464 | ||
|
|
475d09e987 | ||
|
|
fd21bdeabf | ||
|
|
6834f72856 | ||
|
|
8cfb7ef659 | ||
|
|
622c87d734 | ||
|
|
2ce406a410 | ||
|
|
3743361b9f | ||
|
|
69d11f0a4b | ||
|
|
e4dd1169c4 | ||
|
|
1657569f1d | ||
|
|
545876d013 | ||
|
|
9492c5d994 | ||
|
|
94b1ea30d1 | ||
|
|
a77d61f632 | ||
|
|
66b2f57d73 | ||
|
|
92eb2a50a2 | ||
|
|
3df1dca146 | ||
|
|
a0132e2e92 | ||
|
|
c6b342908d | ||
|
|
962237492f | ||
|
|
168a6e1072 | ||
|
|
4d67b1d254 | ||
|
|
77a8cc6b09 | ||
|
|
bc8743fdfc | ||
|
|
1837d3f86d | ||
|
|
b2d111e8ec | ||
|
|
ae141c85c6 | ||
|
|
1230b4581b | ||
|
|
70c831c8f9 | ||
|
|
cc0c14c7cf | ||
|
|
19159ad49e | ||
|
|
b9c8fb4ff1 | ||
|
|
4529e720bc | ||
|
|
ef8b2ea65d | ||
|
|
5bd7caafdd | ||
|
|
47ebcd79b1 | ||
|
|
dbad94806e | ||
|
|
c4cac1cb3e |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -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
4
context7.json
Normal 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
318
skills/fp-go-http/SKILL.md
Normal 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+.
|
||||
410
skills/fp-go-logging/SKILL.md
Normal file
410
skills/fp-go-logging/SKILL.md
Normal 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
|
||||
}
|
||||
```
|
||||
520
skills/fp-go-monadic-operations/SKILL.md
Normal file
520
skills/fp-go-monadic-operations/SKILL.md
Normal 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
231
v2/AGENTS.md
Normal 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
|
||||
@@ -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
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go/v2)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
[](https://goreportcard.com/report/github.com/IBM/fp-go/v2)
|
||||
[](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
522
v2/array/array_nil_test.go
Normal 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")
|
||||
}
|
||||
@@ -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
273
v2/cli/README.md
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
199
v2/cli/effect.go
Normal 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
204
v2/cli/effect_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
359
v2/cli/flags.go
Normal 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
287
v2/cli/flags_test.go
Normal 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)))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
130
v2/context/reader/reader.go
Normal 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
142
v2/context/reader/types.go
Normal 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]
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
454
v2/context/readerio/bracket_test.go
Normal file
454
v2/context/readerio/bracket_test.go
Normal 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)()
|
||||
}
|
||||
}
|
||||
@@ -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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
|
||||
90
v2/context/readerioresult/file/types.go
Normal file
90
v2/context/readerioresult/file/types.go
Normal 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]
|
||||
)
|
||||
@@ -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])
|
||||
|
||||
@@ -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] {
|
||||
|
||||
415
v2/context/readerioresult/logging_comprehensive_test.go
Normal file
415
v2/context/readerioresult/logging_comprehensive_test.go
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
213
v2/context/readerreaderioresult/eitherize.go
Normal file
213
v2/context/readerreaderioresult/eitherize.go
Normal 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,
|
||||
)
|
||||
}
|
||||
507
v2/context/readerreaderioresult/eitherize_test.go
Normal file
507
v2/context/readerreaderioresult/eitherize_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
32
v2/effect/common_test.go
Normal 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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
649
v2/effect/effect_additional_test.go
Normal file
649
v2/effect/effect_additional_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
213
v2/effect/effect_missing_test.go
Normal file
213
v2/effect/effect_missing_test.go
Normal 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
208
v2/effect/eitherize.go
Normal 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
507
v2/effect/eitherize_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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]())
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
52
v2/monoid/types.go
Normal 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
65
v2/monoid/void.go
Normal 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
290
v2/monoid/void_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
505
v2/optics/codec/bind.go
Normal 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
1401
v2/optics/codec/bind_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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]))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user