mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-10 13:31:01 +02:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d5dc7ea1f | ||
|
|
69a11bc681 | ||
|
|
a0910b8279 | ||
|
|
029d7be52d | ||
|
|
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 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
continue-on-error: true
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
run: |
|
||||
cd v2
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
continue-on-error: true
|
||||
@@ -134,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).
|
||||
@@ -151,6 +151,11 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
- 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**
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -38,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 pair.Kleisli[context.CancelFunc, context.Context, context.Context], 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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,18 +70,41 @@ func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, contex
|
||||
//
|
||||
// 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 pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
func ContramapIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
|
||||
return LocalIOK[A](f)
|
||||
}
|
||||
@@ -189,8 +216,6 @@ func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A
|
||||
//
|
||||
// - Local: For pure context transformations
|
||||
// - LocalIOK: For context transformations with side effects that cannot fail
|
||||
//
|
||||
//go:inline
|
||||
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] {
|
||||
|
||||
@@ -77,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) {
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"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 (
|
||||
@@ -1011,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:
|
||||
//
|
||||
@@ -1026,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 {
|
||||
@@ -1047,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 pair.Kleisli[context.CancelFunc, context.Context, context.Context]) 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))
|
||||
}
|
||||
otherCancel, otherCtx := pair.Unpack(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.
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ type TestContext struct {
|
||||
|
||||
// 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, C](ctx)(eff)
|
||||
ioResult := Provide[A](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
return readerResult(context.Background())
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
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"
|
||||
@@ -267,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[string, OuterContext, InnerContext](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[string, OuterContext](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[string, OuterContext, InnerContext](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[string, OuterContext](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[string, OuterContext, InnerContext](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[string, OuterContext](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[string, Level2, Level3](func(l2 Level2) Level3 {
|
||||
local23 := Local[string](func(l2 Level2) Level3 {
|
||||
return Level3{C: l2.B + "-c"}
|
||||
})
|
||||
|
||||
// Transform Level1 -> Level2
|
||||
local12 := Local[string, Level1, Level2](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[string, Level1](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[string, AppConfig, DatabaseConfig](accessor)
|
||||
kleisli := Local[string](accessor)
|
||||
appEffect := kleisli(dbEffect)
|
||||
|
||||
// Run with full AppConfig
|
||||
ioResult := Provide[string, AppConfig](AppConfig{
|
||||
ioResult := Provide[string](AppConfig{
|
||||
DB: DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
@@ -195,21 +197,21 @@ func TestContramap(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test Local
|
||||
localKleisli := Local[int, OuterContext, InnerContext](accessor)
|
||||
localKleisli := Local[int](accessor)
|
||||
localEffect := localKleisli(innerEffect)
|
||||
|
||||
// Test Contramap
|
||||
contramapKleisli := Contramap[int, OuterContext, InnerContext](accessor)
|
||||
contramapKleisli := Contramap[int](accessor)
|
||||
contramapEffect := contramapKleisli(innerEffect)
|
||||
|
||||
outerCtx := OuterContext{Value: "test", Number: 100}
|
||||
|
||||
// Run both
|
||||
localIO := Provide[int, OuterContext](outerCtx)(localEffect)
|
||||
localIO := Provide[int](outerCtx)(localEffect)
|
||||
localReader := RunSync(localIO)
|
||||
localResult, localErr := localReader(context.Background())
|
||||
|
||||
contramapIO := Provide[int, OuterContext](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[string, OuterContext, InnerContext](accessor)
|
||||
kleisli := Contramap[string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[string, OuterContext](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[int, OuterContext, InnerContext](accessor)
|
||||
kleisli := Contramap[int](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[int, OuterContext](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[string, Config2, Config3](func(c2 Config2) Config3 {
|
||||
local23 := Local[string](func(c2 Config2) Config3 {
|
||||
return Config3{Info: c2.Data}
|
||||
})
|
||||
|
||||
// Use Contramap for second transformation
|
||||
contramap12 := Contramap[string, Config1, Config2](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[string, Config1](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[string, AppConfig](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[string, OuterCtx](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[string, OuterCtx](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[string, AppContext](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[string, Level1](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[string, AppConfig](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[string, RawConfig](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[string, RawConfig](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[string, Level1, Level2](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[string, Level1](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[int, OuterCtx](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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ func TestChain_Success(t *testing.T) {
|
||||
t.Run("sequences two effects", func(t *testing.T) {
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Chain[TestConfig](func(x int) Effect[TestConfig, string] {
|
||||
Chain(func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig](strconv.Itoa(x))
|
||||
}),
|
||||
)
|
||||
@@ -149,10 +149,10 @@ func TestChain_Success(t *testing.T) {
|
||||
t.Run("chains multiple effects", func(t *testing.T) {
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
Chain[TestConfig](func(x int) Effect[TestConfig, int] {
|
||||
Chain(func(x int) Effect[TestConfig, int] {
|
||||
return Of[TestConfig](x + 5)
|
||||
}),
|
||||
Chain[TestConfig](func(x int) Effect[TestConfig, int] {
|
||||
Chain(func(x int) Effect[TestConfig, int] {
|
||||
return Of[TestConfig](x * 2)
|
||||
}),
|
||||
)
|
||||
@@ -166,7 +166,7 @@ func TestChain_Failure(t *testing.T) {
|
||||
testErr := errors.New("first error")
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
Chain[TestConfig](func(x int) Effect[TestConfig, string] {
|
||||
Chain(func(x int) Effect[TestConfig, string] {
|
||||
return Of[TestConfig]("should not execute")
|
||||
}),
|
||||
)
|
||||
@@ -178,7 +178,7 @@ func TestChain_Failure(t *testing.T) {
|
||||
testErr := errors.New("second error")
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Chain[TestConfig](func(x int) Effect[TestConfig, string] {
|
||||
Chain(func(x int) Effect[TestConfig, string] {
|
||||
return Fail[TestConfig, string](testErr)
|
||||
}),
|
||||
)
|
||||
@@ -503,7 +503,7 @@ func TestTap_Success(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
log = append(log, fmt.Sprintf("tapped: %d", x))
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
@@ -517,11 +517,11 @@ func TestTap_Success(t *testing.T) {
|
||||
log := []string{}
|
||||
eff := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
log = append(log, "first")
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
log = append(log, "second")
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
@@ -538,7 +538,7 @@ func TestTap_Failure(t *testing.T) {
|
||||
executed := false
|
||||
eff := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
|
||||
Tap(func(x int) Effect[TestConfig, any] {
|
||||
executed = true
|
||||
return Of[TestConfig, any](nil)
|
||||
}),
|
||||
@@ -620,7 +620,7 @@ func TestRead_Success(t *testing.T) {
|
||||
// Create an effect that uses the context's Multiplier
|
||||
eff := F.Pipe1(
|
||||
Of[TestConfig](10),
|
||||
ChainReaderK[TestConfig](func(x int) reader.Reader[TestConfig, int] {
|
||||
ChainReaderK(func(x int) reader.Reader[TestConfig, int] {
|
||||
return func(cfg TestConfig) int {
|
||||
return x * cfg.Multiplier
|
||||
}
|
||||
|
||||
@@ -641,8 +641,8 @@ func TestChainThunkK_Integration(t *testing.T) {
|
||||
|
||||
computation := F.Pipe3(
|
||||
Of[TestConfig](5),
|
||||
ChainReaderK[TestConfig](addMultiplier),
|
||||
ChainReaderIOK[TestConfig](logValue),
|
||||
ChainReaderK(addMultiplier),
|
||||
ChainReaderIOK(logValue),
|
||||
ChainThunkK[TestConfig](processThunk),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func TestProvide(t *testing.T) {
|
||||
ctx := TestContext{Value: "test-value"}
|
||||
eff := Of[TestContext]("result")
|
||||
|
||||
ioResult := Provide[string, TestContext](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[string, Config](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[string, TestContext](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[int, SimpleContext](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[string, TestContext](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[string, TestContext](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[int, TestContext](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[string, TestContext](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[int, TestContext](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[int, TestContext](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[int, TestContext](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[User, TestContext](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[string, AppConfig](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[string, AppConfig](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[string, TestContext](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[State, TestContext](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[string, OuterCtx, InnerCtx](func(outer OuterCtx) InnerCtx {
|
||||
transformedEff := Local[string](func(outer OuterCtx) InnerCtx {
|
||||
return InnerCtx{Data: outer.Value + "-transformed"}
|
||||
})(innerEff)
|
||||
|
||||
result, err := RunSync(Provide[string, OuterCtx](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[[]int, TestContext](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)
|
||||
|
||||
@@ -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=
|
||||
|
||||
195
v2/iooption/array_test.go
Normal file
195
v2/iooption/array_test.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// 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 iooption
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTraverseArray_Success(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := TraverseArray(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{2, 4, 6, 8, 10}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_WithNone(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
if n > 0 {
|
||||
return Of(n * 2)
|
||||
}
|
||||
return None[int]()
|
||||
}
|
||||
|
||||
input := []int{1, 2, -3, 4}
|
||||
result := TraverseArray(f)(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
input := []int{}
|
||||
result := TraverseArray(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_Success(t *testing.T) {
|
||||
f := func(idx, n int) IOOption[int] {
|
||||
return Of(n + idx)
|
||||
}
|
||||
|
||||
input := []int{10, 20, 30}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{10, 21, 32}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_WithNone(t *testing.T) {
|
||||
f := func(idx, n int) IOOption[int] {
|
||||
if idx < 2 {
|
||||
return Of(n + idx)
|
||||
}
|
||||
return None[int]()
|
||||
}
|
||||
|
||||
input := []int{10, 20, 30}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
|
||||
f := func(idx, n int) IOOption[int] {
|
||||
return Of(n + idx)
|
||||
}
|
||||
|
||||
input := []int{}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{}), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_AllSome(t *testing.T) {
|
||||
input := []IOOption[int]{
|
||||
Of(1),
|
||||
Of(2),
|
||||
Of(3),
|
||||
}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{1, 2, 3}), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_WithNone(t *testing.T) {
|
||||
input := []IOOption[int]{
|
||||
Of(1),
|
||||
None[int](),
|
||||
Of(3),
|
||||
}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_Empty(t *testing.T) {
|
||||
input := []IOOption[int]{}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{}), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_AllNone(t *testing.T) {
|
||||
input := []IOOption[int]{
|
||||
None[int](),
|
||||
None[int](),
|
||||
None[int](),
|
||||
}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_Composition(t *testing.T) {
|
||||
// Test composing traverse with other operations
|
||||
f := func(n int) IOOption[int] {
|
||||
if n%2 == 0 {
|
||||
return Of(n / 2)
|
||||
}
|
||||
return None[int]()
|
||||
}
|
||||
|
||||
input := []int{2, 4, 6, 8}
|
||||
result := F.Pipe1(
|
||||
input,
|
||||
TraverseArray(f),
|
||||
)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{1, 2, 3, 4}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_WithMap(t *testing.T) {
|
||||
// Test traverse followed by map
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
TraverseArray(f),
|
||||
Map(func(arr []int) int {
|
||||
sum := 0
|
||||
for _, v := range arr {
|
||||
sum += v
|
||||
}
|
||||
return sum
|
||||
}),
|
||||
)()
|
||||
|
||||
assert.Equal(t, O.Some(12), result) // (1*2 + 2*2 + 3*2) = 12
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_UseIndex(t *testing.T) {
|
||||
// Test that index is properly used
|
||||
f := func(idx, n int) IOOption[string] {
|
||||
return Of(fmt.Sprintf("%d", idx*n*2))
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]string{"0", "4", "12"}), result)
|
||||
}
|
||||
|
||||
|
||||
433
v2/iooption/iooption_comprehensive_test.go
Normal file
433
v2/iooption/iooption_comprehensive_test.go
Normal file
@@ -0,0 +1,433 @@
|
||||
// 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 iooption
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
I "github.com/IBM/fp-go/v2/io"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
result := Of(42)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
func TestSome(t *testing.T) {
|
||||
result := Some("test")()
|
||||
assert.Equal(t, O.Some("test"), result)
|
||||
}
|
||||
|
||||
func TestNone(t *testing.T) {
|
||||
result := None[int]()()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
}
|
||||
|
||||
func TestMonadOf(t *testing.T) {
|
||||
result := MonadOf(100)()
|
||||
assert.Equal(t, O.Some(100), result)
|
||||
}
|
||||
|
||||
func TestFromOptionComprehensive(t *testing.T) {
|
||||
t.Run("from Some", func(t *testing.T) {
|
||||
result := FromOption(O.Some(42))()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("from None", func(t *testing.T) {
|
||||
result := FromOption(O.None[int]())()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ioValue := I.Of(42)
|
||||
result := FromIO(ioValue)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("map over Some", func(t *testing.T) {
|
||||
result := MonadMap(Of(5), utils.Double)()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("map over None", func(t *testing.T) {
|
||||
result := MonadMap(None[int](), utils.Double)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("chain Some to Some", func(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
result := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("chain Some to None", func(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return None[int]()
|
||||
}
|
||||
result := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("chain None", func(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
result := MonadChain(None[int](), f)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
f := func(n int) IOOption[string] {
|
||||
if n > 0 {
|
||||
return Of("positive")
|
||||
}
|
||||
return None[string]()
|
||||
}
|
||||
|
||||
t.Run("chain positive", func(t *testing.T) {
|
||||
result := F.Pipe1(Of(5), Chain(f))()
|
||||
assert.Equal(t, O.Some("positive"), result)
|
||||
})
|
||||
|
||||
t.Run("chain negative", func(t *testing.T) {
|
||||
result := F.Pipe1(Of(-5), Chain(f))()
|
||||
assert.Equal(t, O.None[string](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("apply Some function to Some value", func(t *testing.T) {
|
||||
mab := Of(utils.Double)
|
||||
ma := Of(5)
|
||||
result := MonadAp(mab, ma)()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("apply None function", func(t *testing.T) {
|
||||
mab := None[func(int) int]()
|
||||
ma := Of(5)
|
||||
result := MonadAp(mab, ma)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("apply to None value", func(t *testing.T) {
|
||||
mab := Of(utils.Double)
|
||||
ma := None[int]()
|
||||
result := MonadAp(mab, ma)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
ma := Of(5)
|
||||
result := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
ma := Of(5)
|
||||
result := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
ma := Of(5)
|
||||
result := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
t.Run("flatten Some(Some)", func(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
result := Flatten(nested)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("flatten Some(None)", func(t *testing.T) {
|
||||
nested := Of(None[int]())
|
||||
result := Flatten(nested)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("flatten None", func(t *testing.T) {
|
||||
nested := None[IOOption[int]]()
|
||||
result := Flatten(nested)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOptionize0(t *testing.T) {
|
||||
f := func() (int, bool) {
|
||||
return 42, true
|
||||
}
|
||||
result := Optionize0(f)()()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
|
||||
f2 := func() (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
result2 := Optionize0(f2)()()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestOptionize2(t *testing.T) {
|
||||
f := func(a, b int) (int, bool) {
|
||||
if b != 0 {
|
||||
return a / b, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
result := Optionize2(f)(10, 2)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
|
||||
result2 := Optionize2(f)(10, 0)()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestOptionize3(t *testing.T) {
|
||||
f := func(a, b, c int) (int, bool) {
|
||||
if c != 0 {
|
||||
return (a + b) / c, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
result := Optionize3(f)(10, 5, 3)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
|
||||
result2 := Optionize3(f)(10, 5, 0)()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestOptionize4(t *testing.T) {
|
||||
f := func(a, b, c, d int) (int, bool) {
|
||||
if d != 0 {
|
||||
return (a + b + c) / d, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
result := Optionize4(f)(10, 5, 3, 2)()
|
||||
assert.Equal(t, O.Some(9), result)
|
||||
|
||||
result2 := Optionize4(f)(10, 5, 3, 0)()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
callCount := 0
|
||||
ioOpt := func() Option[int] {
|
||||
callCount++
|
||||
return O.Some(42)
|
||||
}
|
||||
|
||||
memoized := Memoize(ioOpt)
|
||||
|
||||
// First call
|
||||
result1 := memoized()
|
||||
assert.Equal(t, O.Some(42), result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Second call should use cached value
|
||||
result2 := memoized()
|
||||
assert.Equal(t, O.Some(42), result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
onNone := I.Of("none")
|
||||
onSome := func(n int) I.IO[string] {
|
||||
return I.Of(fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
t.Run("fold Some", func(t *testing.T) {
|
||||
result := Fold(onNone, onSome)(Of(42))()
|
||||
assert.Equal(t, "42", result)
|
||||
})
|
||||
|
||||
t.Run("fold None", func(t *testing.T) {
|
||||
result := Fold(onNone, onSome)(None[int]())()
|
||||
assert.Equal(t, "none", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefer(t *testing.T) {
|
||||
callCount := 0
|
||||
gen := func() IOOption[int] {
|
||||
callCount++
|
||||
return Of(42)
|
||||
}
|
||||
|
||||
deferred := Defer(gen)
|
||||
|
||||
// Each call should invoke the generator
|
||||
result1 := deferred()
|
||||
assert.Equal(t, O.Some(42), result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := deferred()
|
||||
assert.Equal(t, O.Some(42), result2)
|
||||
assert.Equal(t, 2, callCount)
|
||||
}
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
t.Run("from Right", func(t *testing.T) {
|
||||
either := ET.Right[string](42)
|
||||
result := FromEither(either)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("from Left", func(t *testing.T) {
|
||||
either := ET.Left[int]("error")
|
||||
result := FromEither(either)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
t.Run("first is Some", func(t *testing.T) {
|
||||
result := MonadAlt(Of(1), Of(2))()
|
||||
assert.Equal(t, O.Some(1), result)
|
||||
})
|
||||
|
||||
t.Run("first is None, second is Some", func(t *testing.T) {
|
||||
result := MonadAlt(None[int](), Of(2))()
|
||||
assert.Equal(t, O.Some(2), result)
|
||||
})
|
||||
|
||||
t.Run("both are None", func(t *testing.T) {
|
||||
result := MonadAlt(None[int](), None[int]())()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlt(t *testing.T) {
|
||||
t.Run("first is Some", func(t *testing.T) {
|
||||
result := F.Pipe1(Of(1), Alt(Of(2)))()
|
||||
assert.Equal(t, O.Some(1), result)
|
||||
})
|
||||
|
||||
t.Run("first is None", func(t *testing.T) {
|
||||
result := F.Pipe1(None[int](), Alt(Of(2)))()
|
||||
assert.Equal(t, O.Some(2), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) IOOption[string] {
|
||||
sideEffect = n * 2
|
||||
return Of("side effect")
|
||||
}
|
||||
|
||||
result := MonadChainFirst(Of(5), f)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) IOOption[string] {
|
||||
sideEffect = n * 2
|
||||
return Of("side effect")
|
||||
}
|
||||
|
||||
result := F.Pipe1(Of(5), ChainFirst(f))()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) I.IO[string] {
|
||||
return func() string {
|
||||
sideEffect = n * 2
|
||||
return "side effect"
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadChainFirstIOK(Of(5), f)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) I.IO[string] {
|
||||
return func() string {
|
||||
sideEffect = n * 2
|
||||
return "side effect"
|
||||
}
|
||||
}
|
||||
|
||||
result := F.Pipe1(Of(5), ChainFirstIOK(f))()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestDelay(t *testing.T) {
|
||||
start := time.Now()
|
||||
delay := 50 * time.Millisecond
|
||||
|
||||
result := F.Pipe1(Of(42), Delay[int](delay))()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
assert.True(t, elapsed >= delay, "Expected delay of at least %v, got %v", delay, elapsed)
|
||||
}
|
||||
|
||||
func TestAfter(t *testing.T) {
|
||||
timestamp := time.Now().Add(50 * time.Millisecond)
|
||||
|
||||
result := F.Pipe1(Of(42), After[int](timestamp))()
|
||||
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
assert.True(t, time.Now().After(timestamp) || time.Now().Equal(timestamp))
|
||||
}
|
||||
|
||||
func TestMonadChainIOK(t *testing.T) {
|
||||
f := func(n int) I.IO[string] {
|
||||
return I.Of(fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
t.Run("chain Some", func(t *testing.T) {
|
||||
result := MonadChainIOK(Of(42), f)()
|
||||
assert.Equal(t, O.Some("42"), result)
|
||||
})
|
||||
|
||||
t.Run("chain None", func(t *testing.T) {
|
||||
result := MonadChainIOK(None[int](), f)()
|
||||
assert.Equal(t, O.None[string](), result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
241
v2/ioresult/bracket_test.go
Normal file
241
v2/ioresult/bracket_test.go
Normal file
@@ -0,0 +1,241 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBracket_Success(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
used = true
|
||||
return result.Of("success")
|
||||
}
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Of(F.VOID)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, used, "Resource should be used")
|
||||
assert.True(t, released, "Resource should be released")
|
||||
assert.Equal(t, result.Of("success"), res)
|
||||
}
|
||||
|
||||
func TestBracket_UseFailure(t *testing.T) {
|
||||
acquired := false
|
||||
released := false
|
||||
releaseResult := result.Result[string]{}
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
useErr := errors.New("use error")
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](useErr)
|
||||
}
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
releaseResult = res
|
||||
return result.Of(F.VOID)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, released, "Resource should be released even on use failure")
|
||||
assert.Equal(t, result.Left[string](useErr), res)
|
||||
assert.Equal(t, result.Left[string](useErr), releaseResult)
|
||||
}
|
||||
|
||||
func TestBracket_AcquireFailure(t *testing.T) {
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquireErr := errors.New("acquire error")
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Left[int](acquireErr)
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
used = true
|
||||
return result.Of("success")
|
||||
}
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Of(F.VOID)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.False(t, used, "Use should not be called if acquire fails")
|
||||
assert.False(t, released, "Release should not be called if acquire fails")
|
||||
assert.Equal(t, result.Left[string](acquireErr), res)
|
||||
}
|
||||
|
||||
func TestBracket_ReleaseFailure(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
used = true
|
||||
return result.Of("success")
|
||||
}
|
||||
}
|
||||
|
||||
releaseErr := errors.New("release error")
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Left[F.Void](releaseErr)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, used, "Resource should be used")
|
||||
assert.True(t, released, "Release should be attempted")
|
||||
// When release fails, the release error is returned
|
||||
assert.Equal(t, result.Left[string](releaseErr), res)
|
||||
}
|
||||
|
||||
func TestBracket_BothUseAndReleaseFail(t *testing.T) {
|
||||
acquired := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
useErr := errors.New("use error")
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](useErr)
|
||||
}
|
||||
}
|
||||
|
||||
releaseErr := errors.New("release error")
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Left[F.Void](releaseErr)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, released, "Release should be attempted")
|
||||
// When both fail, the release error is returned
|
||||
assert.Equal(t, result.Left[string](releaseErr), res)
|
||||
}
|
||||
|
||||
func TestBracket_ResourceValue(t *testing.T) {
|
||||
// Test that the acquired resource value is passed correctly
|
||||
var usedValue int
|
||||
var releasedValue int
|
||||
|
||||
acquire := Of(100)
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
usedValue = n
|
||||
return Of("result")
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
releasedValue = n
|
||||
return Of(F.VOID)
|
||||
}
|
||||
|
||||
Bracket(acquire, use, release)()
|
||||
|
||||
assert.Equal(t, 100, usedValue, "Use should receive acquired value")
|
||||
assert.Equal(t, 100, releasedValue, "Release should receive acquired value")
|
||||
}
|
||||
|
||||
func TestBracket_ResultValue(t *testing.T) {
|
||||
// Test that the use result is passed to release
|
||||
var releaseReceivedResult Result[string]
|
||||
|
||||
acquire := Of(42)
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return Of("test result")
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
releaseReceivedResult = res
|
||||
return Of(F.VOID)
|
||||
}
|
||||
|
||||
Bracket(acquire, use, release)()
|
||||
|
||||
assert.Equal(t, result.Of("test result"), releaseReceivedResult)
|
||||
}
|
||||
|
||||
|
||||
581
v2/ioresult/ioresult_comprehensive_test.go
Normal file
581
v2/ioresult/ioresult_comprehensive_test.go
Normal file
@@ -0,0 +1,581 @@
|
||||
// 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 ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := Left[int](err)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
res := Right(42)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
res := Of(42)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestMonadOf(t *testing.T) {
|
||||
res := MonadOf(42)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestLeftIO(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := LeftIO[int](io.Of(err))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
}
|
||||
|
||||
func TestRightIO(t *testing.T) {
|
||||
res := RightIO(io.Of(42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
t.Run("from Right", func(t *testing.T) {
|
||||
either := result.Of(42)
|
||||
res := FromEither(either)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("from Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
either := result.Left[int](err)
|
||||
res := FromEither(either)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
t.Run("from success", func(t *testing.T) {
|
||||
res := FromResult(result.Of(42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("from error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := FromResult(result.Left[int](err))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromEitherI(t *testing.T) {
|
||||
t.Run("with nil error", func(t *testing.T) {
|
||||
res := FromEitherI(42, nil)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := FromEitherI(0, err)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResultI(t *testing.T) {
|
||||
t.Run("with nil error", func(t *testing.T) {
|
||||
res := FromResultI(42, nil)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := FromResultI(0, err)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromOption_Success(t *testing.T) {
|
||||
onNone := func() error {
|
||||
return errors.New("none")
|
||||
}
|
||||
|
||||
t.Run("from Some", func(t *testing.T) {
|
||||
res := FromOption[int](onNone)(O.Some(42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("from None", func(t *testing.T) {
|
||||
res := FromOption[int](onNone)(O.None[int]())()
|
||||
assert.Equal(t, result.Left[int](errors.New("none")), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ioValue := io.Of(42)
|
||||
res := FromIO(ioValue)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
res := FromLazy(lazy)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("map over Right", func(t *testing.T) {
|
||||
res := MonadMap(Of(5), utils.Double)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("map over Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := MonadMap(Left[int](err), utils.Double)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap_Comprehensive(t *testing.T) {
|
||||
double := func(n int) int { return n * 2 }
|
||||
|
||||
t.Run("map Right", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(5), Map(double))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("map Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := F.Pipe1(Left[int](err), Map(double))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("mapTo Right", func(t *testing.T) {
|
||||
res := MonadMapTo(Of(5), "constant")()
|
||||
assert.Equal(t, result.Of("constant"), res)
|
||||
})
|
||||
|
||||
t.Run("mapTo Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := MonadMapTo(Left[int](err), "constant")()
|
||||
assert.Equal(t, result.Left[string](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
res := F.Pipe1(Of(5), MapTo[int]("constant"))()
|
||||
assert.Equal(t, result.Of("constant"), res)
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
f := func(n int) IOResult[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
t.Run("chain Right to Right", func(t *testing.T) {
|
||||
res := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("chain Right to Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
f := func(n int) IOResult[int] {
|
||||
return Left[int](err)
|
||||
}
|
||||
res := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("chain Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := MonadChain(Left[int](err), f)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain_Comprehensive(t *testing.T) {
|
||||
f := func(n int) IOResult[string] {
|
||||
if n > 0 {
|
||||
return Of(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return Left[string](errors.New("negative"))
|
||||
}
|
||||
|
||||
t.Run("chain positive", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(5), Chain(f))()
|
||||
assert.Equal(t, result.Of("5"), res)
|
||||
})
|
||||
|
||||
t.Run("chain negative", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(-5), Chain(f))()
|
||||
assert.Equal(t, result.Left[string](errors.New("negative")), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainEitherK(t *testing.T) {
|
||||
f := func(n int) result.Result[int] {
|
||||
if n > 0 {
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
return result.Left[int](errors.New("non-positive"))
|
||||
}
|
||||
|
||||
t.Run("chain to success", func(t *testing.T) {
|
||||
res := MonadChainEitherK(Of(5), f)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("chain to error", func(t *testing.T) {
|
||||
res := MonadChainEitherK(Of(-5), f)()
|
||||
assert.Equal(t, result.Left[int](errors.New("non-positive")), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainResultK(t *testing.T) {
|
||||
f := func(n int) result.Result[int] {
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
|
||||
res := MonadChainResultK(Of(5), f)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestChainResultK(t *testing.T) {
|
||||
f := func(n int) result.Result[int] {
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
|
||||
res := F.Pipe1(Of(5), ChainResultK(f))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestMonadAp_Comprehensive(t *testing.T) {
|
||||
t.Run("apply Right function to Right value", func(t *testing.T) {
|
||||
mab := Of(utils.Double)
|
||||
ma := Of(5)
|
||||
res := MonadAp(mab, ma)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("apply Left function", func(t *testing.T) {
|
||||
err := errors.New("function error")
|
||||
mab := Left[func(int) int](err)
|
||||
ma := Of(5)
|
||||
res := MonadAp(mab, ma)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("apply to Left value", func(t *testing.T) {
|
||||
err := errors.New("value error")
|
||||
mab := Of(utils.Double)
|
||||
ma := Left[int](err)
|
||||
res := MonadAp(mab, ma)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp_Comprehensive(t *testing.T) {
|
||||
ma := Of(5)
|
||||
res := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
ma := Of(5)
|
||||
res := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
ma := Of(5)
|
||||
res := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestFlatten_Comprehensive(t *testing.T) {
|
||||
t.Run("flatten Right(Right)", func(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
res := Flatten(nested)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("flatten Right(Left)", func(t *testing.T) {
|
||||
err := errors.New("inner error")
|
||||
nested := Of(Left[int](err))
|
||||
res := Flatten(nested)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("flatten Left", func(t *testing.T) {
|
||||
err := errors.New("outer error")
|
||||
nested := Left[IOResult[int]](err)
|
||||
res := Flatten(nested)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTryCatch(t *testing.T) {
|
||||
t.Run("successful function", func(t *testing.T) {
|
||||
f := func() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
res := TryCatch(f, F.Identity[error])()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("failing function", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
f := func() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
res := TryCatch(f, F.Identity[error])()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("with error transformation", func(t *testing.T) {
|
||||
err := errors.New("original")
|
||||
f := func() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
onThrow := func(e error) error {
|
||||
return fmt.Errorf("wrapped: %w", e)
|
||||
}
|
||||
res := TryCatch(f, onThrow)()
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTryCatchError_Comprehensive(t *testing.T) {
|
||||
t.Run("successful function", func(t *testing.T) {
|
||||
f := func() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
res := TryCatchError(f)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("failing function", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
f := func() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
res := TryCatchError(f)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMemoize_Comprehensive(t *testing.T) {
|
||||
callCount := 0
|
||||
ioRes := func() Result[int] {
|
||||
callCount++
|
||||
return result.Of(42)
|
||||
}
|
||||
|
||||
memoized := Memoize(ioRes)
|
||||
|
||||
// First call
|
||||
res1 := memoized()
|
||||
assert.Equal(t, result.Of(42), res1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Second call should use cached value
|
||||
res2 := memoized()
|
||||
assert.Equal(t, result.Of(42), res2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
}
|
||||
|
||||
func TestMonadMapLeft(t *testing.T) {
|
||||
t.Run("map Left error", func(t *testing.T) {
|
||||
err := errors.New("original")
|
||||
f := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
res := MonadMapLeft(Left[int](err), f)()
|
||||
// Result is IOEither[string, int], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("map Right unchanged", func(t *testing.T) {
|
||||
f := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
res := MonadMapLeft(Of(42), f)()
|
||||
// MapLeft changes the error type, so result is IOEither[string, int]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapLeft_Comprehensive(t *testing.T) {
|
||||
f := func(e error) string {
|
||||
return fmt.Sprintf("wrapped: %s", e.Error())
|
||||
}
|
||||
|
||||
t.Run("map Left", func(t *testing.T) {
|
||||
err := errors.New("original")
|
||||
res := F.Pipe1(Left[int](err), MapLeft[int](f))()
|
||||
// Result is IOEither[string, int], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("map Right unchanged", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(42), MapLeft[int](f))()
|
||||
// MapLeft changes the error type, so result is IOEither[string, int]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadBiMap(t *testing.T) {
|
||||
leftF := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
rightF := func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
t.Run("bimap Right", func(t *testing.T) {
|
||||
res := MonadBiMap(Of(42), leftF, rightF)()
|
||||
// BiMap changes both types, so result is IOEither[string, string]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
|
||||
})
|
||||
|
||||
t.Run("bimap Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := MonadBiMap(Left[int](err), leftF, rightF)()
|
||||
// Result is IOEither[string, string], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBiMap_Comprehensive(t *testing.T) {
|
||||
leftF := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
rightF := func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
t.Run("bimap Right", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(42), BiMap(leftF, rightF))()
|
||||
// BiMap changes both types, so result is IOEither[string, string]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
|
||||
})
|
||||
|
||||
t.Run("bimap Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := F.Pipe1(Left[int](err), BiMap(leftF, rightF))()
|
||||
// Result is IOEither[string, string], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFold_Comprehensive(t *testing.T) {
|
||||
onLeft := func(e error) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("error: %s", e.Error()))
|
||||
}
|
||||
onRight := func(n int) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("value: %d", n))
|
||||
}
|
||||
|
||||
t.Run("fold Right", func(t *testing.T) {
|
||||
res := Fold(onLeft, onRight)(Of(42))()
|
||||
assert.Equal(t, "value: 42", res)
|
||||
})
|
||||
|
||||
t.Run("fold Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := Fold(onLeft, onRight)(Left[int](err))()
|
||||
assert.Equal(t, "error: test", res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOrElse_Comprehensive(t *testing.T) {
|
||||
onLeft := func(e error) io.IO[int] {
|
||||
return io.Of(0)
|
||||
}
|
||||
|
||||
t.Run("get Right value", func(t *testing.T) {
|
||||
res := GetOrElse(onLeft)(Of(42))()
|
||||
assert.Equal(t, 42, res)
|
||||
})
|
||||
|
||||
t.Run("get default on Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := GetOrElse(onLeft)(Left[int](err))()
|
||||
assert.Equal(t, 0, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOrElseOf(t *testing.T) {
|
||||
onLeft := func(e error) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
t.Run("get Right value", func(t *testing.T) {
|
||||
res := GetOrElseOf(onLeft)(Of(42))()
|
||||
assert.Equal(t, 42, res)
|
||||
})
|
||||
|
||||
t.Run("get default on Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := GetOrElseOf(onLeft)(Left[int](err))()
|
||||
assert.Equal(t, 0, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainTo(t *testing.T) {
|
||||
t.Run("chain Right to Right", func(t *testing.T) {
|
||||
res := MonadChainTo(Of(1), Of(2))()
|
||||
assert.Equal(t, result.Of(2), res)
|
||||
})
|
||||
|
||||
t.Run("chain Right to Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := MonadChainTo(Of(1), Left[int](err))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("chain Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := MonadChainTo(Left[int](err), Of(2))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainLazyK(t *testing.T) {
|
||||
f := func(n int) Lazy[string] {
|
||||
return func() string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe1(Of(42), ChainLazyK(f))()
|
||||
assert.Equal(t, result.Of("42"), res)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,15 +22,19 @@
|
||||
package codec
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// validateFromParser creates a validation function from a parser that may fail.
|
||||
@@ -338,3 +342,183 @@ func Int64FromString() Type[int64, string, string] {
|
||||
prism.ParseInt64().ReverseGet,
|
||||
)
|
||||
}
|
||||
|
||||
// BoolFromString creates a bidirectional codec for parsing boolean values from strings.
|
||||
// This codec converts string representations of booleans to bool values and vice versa.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string to a bool using strconv.ParseBool
|
||||
// - Encodes: Converts a bool to its string representation using strconv.FormatBool
|
||||
// - Validates: Ensures the string contains a valid boolean value
|
||||
//
|
||||
// The codec accepts the following string values (case-insensitive):
|
||||
// - true: "1", "t", "T", "true", "TRUE", "True"
|
||||
// - false: "0", "f", "F", "false", "FALSE", "False"
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[bool, string, string] codec that handles bool/string conversions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// boolCodec := BoolFromString()
|
||||
//
|
||||
// // Decode valid boolean strings
|
||||
// validation := boolCodec.Decode("true")
|
||||
// // validation is Right(true)
|
||||
//
|
||||
// validation := boolCodec.Decode("1")
|
||||
// // validation is Right(true)
|
||||
//
|
||||
// validation := boolCodec.Decode("false")
|
||||
// // validation is Right(false)
|
||||
//
|
||||
// validation := boolCodec.Decode("0")
|
||||
// // validation is Right(false)
|
||||
//
|
||||
// // Encode a boolean to string
|
||||
// str := boolCodec.Encode(true)
|
||||
// // str is "true"
|
||||
//
|
||||
// str := boolCodec.Encode(false)
|
||||
// // str is "false"
|
||||
//
|
||||
// // Invalid boolean string fails validation
|
||||
// validation := boolCodec.Decode("yes")
|
||||
// // validation is Left(ValidationError{...})
|
||||
//
|
||||
// // Case variations are accepted
|
||||
// validation := boolCodec.Decode("TRUE")
|
||||
// // validation is Right(true)
|
||||
func BoolFromString() Type[bool, string, string] {
|
||||
return MakeType(
|
||||
"BoolFromString",
|
||||
Is[bool](),
|
||||
validateFromParser(strconv.ParseBool),
|
||||
strconv.FormatBool,
|
||||
)
|
||||
}
|
||||
|
||||
func decodeJSON[T any](dec json.Unmarshaler) ReaderResult[[]byte, T] {
|
||||
return func(b []byte) Result[T] {
|
||||
var t T
|
||||
err := dec.UnmarshalJSON(b)
|
||||
return result.TryCatchError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeText[T any](dec encoding.TextUnmarshaler) ReaderResult[[]byte, T] {
|
||||
return func(b []byte) Result[T] {
|
||||
var t T
|
||||
err := dec.UnmarshalText(b)
|
||||
return result.TryCatchError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalText creates a bidirectional codec for types that implement encoding.TextMarshaler
|
||||
// and encoding.TextUnmarshaler. This codec handles binary text serialization formats.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Calls dec.UnmarshalText(b) to deserialize []byte into the target type T
|
||||
// - Encodes: Calls enc.MarshalText() to serialize the value to []byte
|
||||
// - Validates: Returns a failure if UnmarshalText returns an error
|
||||
//
|
||||
// Note: The enc and dec parameters are external marshaler/unmarshaler instances. The
|
||||
// decoded value is the zero value of T after UnmarshalText has been called on dec
|
||||
// (the caller is responsible for ensuring dec holds the decoded state).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The Go type to encode/decode
|
||||
//
|
||||
// Parameters:
|
||||
// - enc: An encoding.TextMarshaler used for encoding values to []byte
|
||||
// - dec: An encoding.TextUnmarshaler used for decoding []byte to the target type
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[T, []byte, []byte] codec that handles text marshaling/unmarshaling
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type MyType struct{ Value string }
|
||||
//
|
||||
// var instance MyType
|
||||
// codec := MarshalText[MyType](instance, &instance)
|
||||
//
|
||||
// // Decode bytes to MyType
|
||||
// result := codec.Decode([]byte(`some text`))
|
||||
//
|
||||
// // Encode MyType to bytes
|
||||
// encoded := codec.Encode(instance)
|
||||
func MarshalText[T any](
|
||||
enc encoding.TextMarshaler,
|
||||
dec encoding.TextUnmarshaler,
|
||||
) Type[T, []byte, []byte] {
|
||||
return MakeType(
|
||||
"UnmarshalText",
|
||||
Is[T](),
|
||||
F.Pipe2(
|
||||
dec,
|
||||
decodeText[T],
|
||||
validate.FromReaderResult,
|
||||
),
|
||||
func(t T) []byte {
|
||||
b, _ := enc.MarshalText()
|
||||
return b
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// MarshalJSON creates a bidirectional codec for types that implement encoding/json's
|
||||
// json.Marshaler and json.Unmarshaler interfaces. This codec handles JSON serialization.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Calls dec.UnmarshalJSON(b) to deserialize []byte JSON into the target type T
|
||||
// - Encodes: Calls enc.MarshalJSON() to serialize the value to []byte JSON
|
||||
// - Validates: Returns a failure if UnmarshalJSON returns an error
|
||||
//
|
||||
// Note: The enc and dec parameters are external marshaler/unmarshaler instances. The
|
||||
// decoded value is the zero value of T after UnmarshalJSON has been called on dec
|
||||
// (the caller is responsible for ensuring dec holds the decoded state).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The Go type to encode/decode
|
||||
//
|
||||
// Parameters:
|
||||
// - enc: A json.Marshaler used for encoding values to JSON []byte
|
||||
// - dec: A json.Unmarshaler used for decoding JSON []byte to the target type
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[T, []byte, []byte] codec that handles JSON marshaling/unmarshaling
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type MyData struct {
|
||||
// Name string `json:"name"`
|
||||
// Value int `json:"value"`
|
||||
// }
|
||||
//
|
||||
// var instance MyData
|
||||
// codec := MarshalJSON[MyData](&instance, &instance)
|
||||
//
|
||||
// // Decode JSON bytes to MyData
|
||||
// result := codec.Decode([]byte(`{"name":"test","value":42}`))
|
||||
//
|
||||
// // Encode MyData to JSON bytes
|
||||
// encoded := codec.Encode(instance)
|
||||
func MarshalJSON[T any](
|
||||
enc json.Marshaler,
|
||||
dec json.Unmarshaler,
|
||||
) Type[T, []byte, []byte] {
|
||||
return MakeType(
|
||||
"UnmarshalJSON",
|
||||
Is[T](),
|
||||
F.Pipe2(
|
||||
dec,
|
||||
decodeJSON[T],
|
||||
validate.FromReaderResult,
|
||||
),
|
||||
func(t T) []byte {
|
||||
b, _ := enc.MarshalJSON()
|
||||
return b
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,7 @@ func TestDo(t *testing.T) {
|
||||
decoder := Do[string](State{})
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{}, value)
|
||||
assert.Equal(t, validation.Of(State{}), result)
|
||||
})
|
||||
|
||||
t.Run("creates decoder with initialized state", func(t *testing.T) {
|
||||
@@ -79,12 +74,7 @@ func TestBind(t *testing.T) {
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 42, y: 10}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 42, y: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
@@ -216,12 +206,7 @@ func TestLet(t *testing.T) {
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 60, y: 10, z: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,47 @@ func Of[I, A any](a A) Decode[I, A] {
|
||||
return readereither.Of[I, Errors](a)
|
||||
}
|
||||
|
||||
// OfLazy converts a lazy computation into a Decode that ignores its input.
|
||||
// The resulting Decode will evaluate the lazy computation when executed and wrap
|
||||
// the result in a successful validation, regardless of the input provided.
|
||||
//
|
||||
// This function is intended solely for deferring the computation of a value, NOT for
|
||||
// representing side effects. The lazy computation should be a pure function that
|
||||
// produces the same result each time it's called (referential transparency). For
|
||||
// operations with side effects, use appropriate effect types like IO or IOResult.
|
||||
//
|
||||
// This is useful for lifting deferred computations into the Decode context without
|
||||
// requiring access to the input, while maintaining the validation wrapper for consistency.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type (ignored by the resulting Decode)
|
||||
// - A: The result type produced by the lazy computation
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: A lazy computation that produces a value of type A (must be pure, no side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A Decode that ignores its input, evaluates the lazy computation, and wraps the result in Validation[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lazyValue := func() int { return 42 }
|
||||
// decoder := decode.OfLazy[string](lazyValue)
|
||||
// result := decoder("any input") // validation.Success(42)
|
||||
//
|
||||
// Example - Deferring expensive computation:
|
||||
//
|
||||
// expensiveCalc := func() Config {
|
||||
// // Expensive but pure computation here
|
||||
// return computeDefaultConfig()
|
||||
// }
|
||||
// decoder := decode.OfLazy[map[string]any](expensiveCalc)
|
||||
// // Computation is deferred until the Decode is executed
|
||||
// result := decoder(inputData) // validation.Success(config)
|
||||
func OfLazy[I, A any](fa Lazy[A]) Decode[I, A] {
|
||||
return readereither.OfLazy[I, Errors](fa)
|
||||
}
|
||||
|
||||
// Left creates a Decode that always fails with the given validation errors.
|
||||
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
|
||||
// into the Decode context.
|
||||
|
||||
@@ -51,6 +51,108 @@ func TestOf(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestOfLazy tests the OfLazy function
|
||||
func TestOfLazy(t *testing.T) {
|
||||
t.Run("evaluates lazy computation ignoring input", func(t *testing.T) {
|
||||
lazyValue := func() int { return 42 }
|
||||
decoder := OfLazy[string](lazyValue)
|
||||
res := decoder("any input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("defers computation until Decode is executed", func(t *testing.T) {
|
||||
executed := false
|
||||
lazyComputation := func() string {
|
||||
executed = true
|
||||
return "computed"
|
||||
}
|
||||
decoder := OfLazy[string](lazyComputation)
|
||||
|
||||
// Computation should not be executed yet
|
||||
assert.False(t, executed, "lazy computation should not be executed during Decode creation")
|
||||
|
||||
// Execute the Decode
|
||||
res := decoder("input")
|
||||
|
||||
// Now computation should be executed
|
||||
assert.True(t, executed, "lazy computation should be executed when Decode runs")
|
||||
assert.Equal(t, validation.Of("computed"), res)
|
||||
})
|
||||
|
||||
t.Run("evaluates lazy computation each time Decode is called", func(t *testing.T) {
|
||||
counter := 0
|
||||
lazyCounter := func() int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
decoder := OfLazy[string](lazyCounter)
|
||||
|
||||
// First execution
|
||||
res1 := decoder("input")
|
||||
assert.Equal(t, validation.Of(1), res1)
|
||||
|
||||
// Second execution
|
||||
res2 := decoder("input")
|
||||
assert.Equal(t, validation.Of(2), res2)
|
||||
|
||||
// Third execution
|
||||
res3 := decoder("input")
|
||||
assert.Equal(t, validation.Of(3), res3)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
lazyString := func() string { return "hello" }
|
||||
decoder1 := OfLazy[int](lazyString)
|
||||
assert.Equal(t, validation.Of("hello"), decoder1(123))
|
||||
|
||||
lazySlice := func() []int { return []int{1, 2, 3} }
|
||||
decoder2 := OfLazy[string](lazySlice)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), decoder2("input"))
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
lazyStruct := func() Person { return Person{Name: "Alice", Age: 30} }
|
||||
decoder3 := OfLazy[map[string]any](lazyStruct)
|
||||
assert.Equal(t, validation.Of(Person{Name: "Alice", Age: 30}), decoder3(map[string]any{}))
|
||||
})
|
||||
|
||||
t.Run("can be composed with other Decode operations", func(t *testing.T) {
|
||||
lazyValue := func() int { return 10 }
|
||||
decoder := MonadMap(
|
||||
OfLazy[string](lazyValue),
|
||||
func(x int) int { return x * 2 },
|
||||
)
|
||||
res := decoder("input")
|
||||
assert.Equal(t, validation.Of(20), res)
|
||||
})
|
||||
|
||||
t.Run("ignores input completely", func(t *testing.T) {
|
||||
lazyValue := func() string { return "constant" }
|
||||
decoder := OfLazy[string](lazyValue)
|
||||
|
||||
// Different inputs should produce same result
|
||||
res1 := decoder("input1")
|
||||
res2 := decoder("input2")
|
||||
|
||||
assert.Equal(t, validation.Of("constant"), res1)
|
||||
assert.Equal(t, validation.Of("constant"), res2)
|
||||
assert.Equal(t, res1, res2)
|
||||
})
|
||||
|
||||
t.Run("always wraps result in success validation", func(t *testing.T) {
|
||||
lazyValue := func() int { return 42 }
|
||||
decoder := OfLazy[string](lazyValue)
|
||||
res := decoder("input")
|
||||
|
||||
// Verify it's a successful validation
|
||||
assert.True(t, either.IsRight(res))
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLeft tests the Left function
|
||||
func TestLeft(t *testing.T) {
|
||||
t.Run("creates decoder that always fails", func(t *testing.T) {
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestEitherEncode(t *testing.T) {
|
||||
|
||||
// TestEitherDecode tests decoding/validation of Either values
|
||||
func TestEitherDecode(t *testing.T) {
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, either.Either[string, int]](either.Left[int]("")))
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](either.Left[int]("")))
|
||||
|
||||
// Create codecs that both work with string input
|
||||
stringCodec := Id[string]()
|
||||
|
||||
@@ -2,6 +2,7 @@ package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
@@ -10,12 +11,15 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/decoder"
|
||||
"github.com/IBM/fp-go/v2/optics/encoder"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"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/readerresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -338,4 +342,156 @@ type (
|
||||
// - ApplicativeMonoid: Combines successful results using inner monoid
|
||||
// - AlternativeMonoid: Combines applicative and alternative behaviors
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
|
||||
// Lens is an optic that focuses on a specific field within a product type S.
|
||||
// It provides a way to get and set a field of type A within a structure of type S.
|
||||
//
|
||||
// A Lens[S, A] represents a relationship between a source type S and a focus type A,
|
||||
// where the focus always exists (unlike Optional which may not exist).
|
||||
//
|
||||
// Lens operations:
|
||||
// - Get: Extract the field value A from structure S
|
||||
// - Set: Update the field value A in structure S, returning a new S
|
||||
//
|
||||
// Lens laws:
|
||||
// 1. GetSet: If you get a value and then set it back, nothing changes
|
||||
// Set(Get(s))(s) = s
|
||||
// 2. SetGet: If you set a value, you can get it back
|
||||
// Get(Set(a)(s)) = a
|
||||
// 3. SetSet: Setting twice is the same as setting once with the final value
|
||||
// Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
//
|
||||
// In the codec context, lenses are used with ApSL to build codecs for struct fields:
|
||||
// - Extract field values for encoding
|
||||
// - Update field values during validation
|
||||
// - Compose codec operations on nested structures
|
||||
//
|
||||
// Example:
|
||||
// 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 },
|
||||
// )
|
||||
//
|
||||
// // Use with ApSL to build a codec
|
||||
// personCodec := F.Pipe1(
|
||||
// codec.Struct[Person]("Person"),
|
||||
// codec.ApSL(S.Monoid, nameLens, codec.String),
|
||||
// )
|
||||
//
|
||||
// See also:
|
||||
// - ApSL: Applicative sequencing with lens
|
||||
// - Optional: For fields that may not exist
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
// Optional is an optic that focuses on a field within a product type S that may not exist.
|
||||
// It provides a way to get and set an optional field of type A within a structure of type S.
|
||||
//
|
||||
// An Optional[S, A] represents a relationship between a source type S and a focus type A,
|
||||
// where the focus may or may not be present (unlike Lens where it always exists).
|
||||
//
|
||||
// Optional operations:
|
||||
// - GetOption: Try to extract the field value, returning Option[A]
|
||||
// - Set: Update the field value if it exists, returning a new S
|
||||
//
|
||||
// Optional laws:
|
||||
// 1. GetSet (No-op on None): If GetOption returns None, Set has no effect
|
||||
// GetOption(s) = None => Set(a)(s) = s
|
||||
// 2. SetGet (Get what you Set): If GetOption returns Some, you can get back what you set
|
||||
// GetOption(s) = Some(_) => GetOption(Set(a)(s)) = Some(a)
|
||||
// 3. SetSet (Last Set Wins): Setting twice is the same as setting once with the final value
|
||||
// Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
//
|
||||
// In the codec context, optionals are used with ApSO to build codecs for optional fields:
|
||||
// - Extract optional field values for encoding (only if present)
|
||||
// - Update optional field values during validation
|
||||
// - Handle nullable or pointer fields gracefully
|
||||
// - Compose codec operations on structures with optional data
|
||||
//
|
||||
// Example:
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Nickname *string // Optional field
|
||||
// }
|
||||
//
|
||||
// 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
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Use with ApSO to build a codec with optional field
|
||||
// personCodec := F.Pipe1(
|
||||
// codec.Struct[Person]("Person"),
|
||||
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
|
||||
// )
|
||||
//
|
||||
// // Encoding omits the field when absent
|
||||
// p1 := Person{Name: "Alice", Nickname: nil}
|
||||
// encoded := personCodec.Encode(p1) // No nickname in output
|
||||
//
|
||||
// See also:
|
||||
// - ApSO: Applicative sequencing with optional
|
||||
// - Lens: For fields that always exist
|
||||
Optional[S, A any] = optional.Optional[S, A]
|
||||
|
||||
// Semigroup represents an algebraic structure with an associative binary operation.
|
||||
//
|
||||
// A Semigroup[A] provides:
|
||||
// - Concat(A, A): Combines two values associatively
|
||||
//
|
||||
// Semigroup law:
|
||||
// - Associativity: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
|
||||
//
|
||||
// Unlike Monoid, Semigroup does not require an identity element (Empty).
|
||||
// This makes it more general but less powerful for certain operations.
|
||||
//
|
||||
// In the codec context, semigroups are used to:
|
||||
// - Combine validation errors
|
||||
// - Merge partial results
|
||||
// - Aggregate codec outputs
|
||||
//
|
||||
// Example semigroups:
|
||||
// - String concatenation (without empty string)
|
||||
// - Array concatenation (without empty array)
|
||||
// - Error accumulation
|
||||
//
|
||||
// Note: Every Monoid is also a Semigroup, but not every Semigroup is a Monoid.
|
||||
Semigroup[A any] = semigroup.Semigroup[A]
|
||||
|
||||
// Void represents a unit type with a single value.
|
||||
//
|
||||
// Void is used instead of struct{} to represent:
|
||||
// - Unit values in functional programming
|
||||
// - Placeholder types where no meaningful value is needed
|
||||
// - Return types for functions that produce no useful result
|
||||
//
|
||||
// The single value of type Void is VOID (function.VOID).
|
||||
//
|
||||
// Usage:
|
||||
// - Use function.Void (or F.Void) as the type
|
||||
// - Use function.VOID (or F.VOID) as the value
|
||||
//
|
||||
// Example:
|
||||
// unitCodec := codec.Empty[F.Void, F.Void, any](
|
||||
// lazy.Of(pair.MakePair(F.VOID, F.VOID)),
|
||||
// )
|
||||
//
|
||||
// Benefits over struct{}:
|
||||
// - More explicit intent (unit type vs empty struct)
|
||||
// - Consistent with functional programming conventions
|
||||
// - Better semantic meaning in type signatures
|
||||
//
|
||||
// See also:
|
||||
// - function.VOID: The single value of type Void
|
||||
// - Empty: Codec function that uses Void for unit types
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -170,12 +170,7 @@ func TestLet(t *testing.T) {
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, computed: 10}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 5, computed: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
@@ -218,12 +213,7 @@ func TestLet(t *testing.T) {
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 60, y: 10, z: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
// Convert to Validate
|
||||
validator := FromReaderResult[int, string](successRR)
|
||||
validator := FromReaderResult(successRR)
|
||||
|
||||
// Execute the validator
|
||||
validationResult := validator(42)(nil)
|
||||
@@ -53,7 +53,7 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
// Convert to Validate
|
||||
validator := FromReaderResult[string, int](parseIntRR)
|
||||
validator := FromReaderResult(parseIntRR)
|
||||
|
||||
// Execute with valid input
|
||||
validationResult := validator("123")(nil)
|
||||
@@ -74,7 +74,7 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
// Convert to Validate
|
||||
validator := FromReaderResult[string, User](createUserRR)
|
||||
validator := FromReaderResult(createUserRR)
|
||||
|
||||
// Execute the validator
|
||||
validationResult := validator("Alice")(nil)
|
||||
@@ -88,7 +88,7 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
return result.Of(input * 2)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, int](successRR)
|
||||
validator := FromReaderResult(successRR)
|
||||
validationResult := validator(21)(Context{})
|
||||
|
||||
assert.Equal(t, validation.Success(42), validationResult)
|
||||
@@ -99,7 +99,7 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
return result.Of(input + " processed")
|
||||
}
|
||||
|
||||
validator := FromReaderResult[string, string](successRR)
|
||||
validator := FromReaderResult(successRR)
|
||||
ctx := Context{
|
||||
{Key: "user", Type: "User"},
|
||||
{Key: "name", Type: "string"},
|
||||
@@ -122,7 +122,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
|
||||
}
|
||||
|
||||
// Convert to Validate
|
||||
validator := FromReaderResult[string, int](failureRR)
|
||||
validator := FromReaderResult(failureRR)
|
||||
|
||||
// Execute the validator
|
||||
validationResult := validator("invalid")(nil)
|
||||
@@ -147,7 +147,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
|
||||
return result.Left[string](originalErr)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, string](failureRR)
|
||||
validator := FromReaderResult(failureRR)
|
||||
validationResult := validator(42)(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(validationResult))
|
||||
@@ -166,7 +166,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
|
||||
return result.Left[int](errors.New("conversion failed"))
|
||||
}
|
||||
|
||||
validator := FromReaderResult[string, int](failureRR)
|
||||
validator := FromReaderResult(failureRR)
|
||||
ctx := Context{
|
||||
{Key: "user", Type: "User"},
|
||||
{Key: "age", Type: "int"},
|
||||
@@ -213,7 +213,7 @@ func TestFromReaderResult_Failure(t *testing.T) {
|
||||
return result.Left[int](tc.err)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[string, int](failureRR)
|
||||
validator := FromReaderResult(failureRR)
|
||||
validationResult := validator(tc.input)(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(validationResult))
|
||||
@@ -251,7 +251,7 @@ func TestFromReaderResult_Integration(t *testing.T) {
|
||||
|
||||
// Combine validators
|
||||
validator := F.Pipe1(
|
||||
FromReaderResult[string, int](parseIntRR),
|
||||
FromReaderResult(parseIntRR),
|
||||
Chain(validatePositive),
|
||||
)
|
||||
|
||||
@@ -273,8 +273,8 @@ func TestFromReaderResult_Integration(t *testing.T) {
|
||||
|
||||
// Convert and map to double the value
|
||||
validator := F.Pipe1(
|
||||
FromReaderResult[string, int](parseIntRR),
|
||||
Map[string, int, int](func(n int) int { return n * 2 }),
|
||||
FromReaderResult(parseIntRR),
|
||||
Map[string](func(n int) int { return n * 2 }),
|
||||
)
|
||||
|
||||
validationResult := validator("21")(nil)
|
||||
@@ -294,7 +294,7 @@ func TestFromReaderResult_Integration(t *testing.T) {
|
||||
Bind(func(p int) func(State) State {
|
||||
return func(s State) State { s.parsed = p; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return FromReaderResult[string, int](parseIntRR)
|
||||
return FromReaderResult(parseIntRR)
|
||||
}),
|
||||
Let[string](func(v bool) func(State) State {
|
||||
return func(s State) State { s.valid = v; return s }
|
||||
@@ -315,7 +315,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
|
||||
return result.Of(input)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, int](successRR)
|
||||
validator := FromReaderResult(successRR)
|
||||
validationResult := validator(42)(nil)
|
||||
|
||||
assert.True(t, either.IsRight(validationResult))
|
||||
@@ -326,7 +326,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
|
||||
return result.Of(input)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[string, string](identityRR)
|
||||
validator := FromReaderResult(identityRR)
|
||||
validationResult := validator("")(nil)
|
||||
|
||||
assert.Equal(t, validation.Success(""), validationResult)
|
||||
@@ -337,7 +337,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
|
||||
return result.Of(input)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, int](identityRR)
|
||||
validator := FromReaderResult(identityRR)
|
||||
validationResult := validator(0)(nil)
|
||||
|
||||
assert.Equal(t, validation.Success(0), validationResult)
|
||||
@@ -352,7 +352,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
|
||||
return result.Of(&Data{Value: input})
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, *Data](createDataRR)
|
||||
validator := FromReaderResult(createDataRR)
|
||||
validationResult := validator(42)(nil)
|
||||
|
||||
assert.True(t, either.IsRight(validationResult))
|
||||
@@ -372,7 +372,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
|
||||
return result.Of([]string{input, input})
|
||||
}
|
||||
|
||||
validator := FromReaderResult[string, []string](splitRR)
|
||||
validator := FromReaderResult(splitRR)
|
||||
validationResult := validator("test")(nil)
|
||||
|
||||
assert.Equal(t, validation.Success([]string{"test", "test"}), validationResult)
|
||||
@@ -383,7 +383,7 @@ func TestFromReaderResult_EdgeCases(t *testing.T) {
|
||||
return result.Of(map[string]int{input: len(input)})
|
||||
}
|
||||
|
||||
validator := FromReaderResult[string, map[string]int](createMapRR)
|
||||
validator := FromReaderResult(createMapRR)
|
||||
validationResult := validator("hello")(nil)
|
||||
|
||||
assert.Equal(t, validation.Success(map[string]int{"hello": 5}), validationResult)
|
||||
@@ -398,7 +398,7 @@ func TestFromReaderResult_TypeSafety(t *testing.T) {
|
||||
return result.Of(fmt.Sprintf("%d", input))
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, string](intToStringRR)
|
||||
validator := FromReaderResult(intToStringRR)
|
||||
|
||||
// This should compile and work correctly
|
||||
validationResult := validator(42)(nil)
|
||||
@@ -409,7 +409,7 @@ func TestFromReaderResult_TypeSafety(t *testing.T) {
|
||||
// This test verifies that the output type is preserved
|
||||
stringToIntRR := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
validator := FromReaderResult[string, int](stringToIntRR)
|
||||
validator := FromReaderResult(stringToIntRR)
|
||||
validationResult := validator("42")(nil)
|
||||
|
||||
// The result should be Validation[int]
|
||||
@@ -428,7 +428,7 @@ func TestFromReaderResult_TypeSafety(t *testing.T) {
|
||||
return Output{Result: val}, nil
|
||||
})
|
||||
|
||||
validator := FromReaderResult[Input, Output](transformRR)
|
||||
validator := FromReaderResult(transformRR)
|
||||
validationResult := validator(Input{Value: "42"})(nil)
|
||||
|
||||
assert.Equal(t, validation.Success(Output{Result: 42}), validationResult)
|
||||
@@ -441,7 +441,7 @@ func BenchmarkFromReaderResult_Success(b *testing.B) {
|
||||
return result.Of(input * 2)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, int](successRR)
|
||||
validator := FromReaderResult(successRR)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -455,7 +455,7 @@ func BenchmarkFromReaderResult_Failure(b *testing.B) {
|
||||
return result.Left[int](errors.New("error"))
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, int](failureRR)
|
||||
validator := FromReaderResult(failureRR)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -469,7 +469,7 @@ func BenchmarkFromReaderResult_WithContext(b *testing.B) {
|
||||
return result.Of(input * 2)
|
||||
}
|
||||
|
||||
validator := FromReaderResult[int, int](successRR)
|
||||
validator := FromReaderResult(successRR)
|
||||
ctx := Context{
|
||||
{Key: "user", Type: "User"},
|
||||
{Key: "age", Type: "int"},
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestMonadChainLeft(t *testing.T) {
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "validation failed" {
|
||||
return Of[string, int](0) // recover with default
|
||||
return Of[string](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
@@ -43,7 +43,7 @@ func TestMonadChainLeft(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successValidator := Of[string, int](42)
|
||||
successValidator := Of[string](42)
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
@@ -145,7 +145,7 @@ func TestMonadChainLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[Config, string] {
|
||||
return Of[Config, string]("default-value")
|
||||
return Of[Config]("default-value")
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(failingValidator, handler)
|
||||
@@ -194,7 +194,7 @@ func TestMonadChainLeft(t *testing.T) {
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
return Of[string](42)
|
||||
}
|
||||
|
||||
// MonadChainLeft - direct application
|
||||
@@ -229,7 +229,7 @@ func TestMonadChainLeft(t *testing.T) {
|
||||
// Check if we can recover
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "error1" {
|
||||
return Of[string, int](100) // recover
|
||||
return Of[string](100) // recover
|
||||
}
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
@@ -248,12 +248,12 @@ func TestMonadChainLeft(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("does not call handler on success", func(t *testing.T) {
|
||||
successValidator := Of[string, int](42)
|
||||
successValidator := Of[string](42)
|
||||
handlerCalled := false
|
||||
|
||||
handler := func(errs Errors) Validate[string, int] {
|
||||
handlerCalled = true
|
||||
return Of[string, int](0)
|
||||
return Of[string](0)
|
||||
}
|
||||
|
||||
validator := MonadChainLeft(successValidator, handler)
|
||||
@@ -267,9 +267,9 @@ func TestMonadChainLeft(t *testing.T) {
|
||||
// TestMonadAlt tests the MonadAlt function
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
t.Run("returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator1 := Of[string](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
return Of[string](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(validator1, validator2)("input")(nil)
|
||||
@@ -285,7 +285,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
return Of[string](42)
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)("input")(nil)
|
||||
@@ -328,11 +328,11 @@ func TestMonadAlt(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator1 := Of[string](42)
|
||||
evaluated := false
|
||||
validator2 := func() Validate[string, int] {
|
||||
evaluated = true
|
||||
return Of[string, int](100)
|
||||
return Of[string](100)
|
||||
}
|
||||
|
||||
result := MonadAlt(validator1, validator2)("input")(nil)
|
||||
@@ -349,7 +349,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, string] {
|
||||
return Of[string, string]("fallback")
|
||||
return Of[string]("fallback")
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)("input")(nil)
|
||||
@@ -374,7 +374,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
succeeding := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
return Of[string](42)
|
||||
}
|
||||
|
||||
// Chain: try failing1, then failing2, then succeeding
|
||||
@@ -395,7 +395,7 @@ func TestMonadAlt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[Config, string] {
|
||||
return Of[Config, string]("default")
|
||||
return Of[Config]("default")
|
||||
}
|
||||
|
||||
result := MonadAlt(failing, fallback)(Config{Port: 9999})(nil)
|
||||
@@ -458,9 +458,9 @@ func TestMonadAlt(t *testing.T) {
|
||||
// TestAlt tests the Alt function
|
||||
func TestAlt(t *testing.T) {
|
||||
t.Run("returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator1 := Of[string](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
return Of[string](100)
|
||||
}
|
||||
|
||||
withAlt := Alt(validator2)
|
||||
@@ -477,7 +477,7 @@ func TestAlt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
return Of[string](42)
|
||||
}
|
||||
|
||||
withAlt := Alt(fallback)
|
||||
@@ -522,11 +522,11 @@ func TestAlt(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator1 := Of[string](42)
|
||||
evaluated := false
|
||||
validator2 := func() Validate[string, int] {
|
||||
evaluated = true
|
||||
return Of[string, int](100)
|
||||
return Of[string](100)
|
||||
}
|
||||
|
||||
withAlt := Alt(validator2)
|
||||
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
succeeding := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
return Of[string](42)
|
||||
}
|
||||
|
||||
// Use F.Pipe to chain alternatives
|
||||
@@ -576,7 +576,7 @@ func TestAlt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
return Of[string](42)
|
||||
}
|
||||
|
||||
// Alt - curried for pipelines
|
||||
@@ -592,9 +592,9 @@ func TestAlt(t *testing.T) {
|
||||
// TestMonadAltAndAltEquivalence tests that MonadAlt and Alt are equivalent
|
||||
func TestMonadAltAndAltEquivalence(t *testing.T) {
|
||||
t.Run("both produce same results for success", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator1 := Of[string](42)
|
||||
validator2 := func() Validate[string, int] {
|
||||
return Of[string, int](100)
|
||||
return Of[string](100)
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(validator1, validator2)("input")(nil)
|
||||
@@ -612,7 +612,7 @@ func TestMonadAltAndAltEquivalence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
fallback := func() Validate[string, int] {
|
||||
return Of[string, int](42)
|
||||
return Of[string](42)
|
||||
}
|
||||
|
||||
resultMonadAlt := MonadAlt(failing, fallback)("input")(nil)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// TestAlternativeMonoid tests the AlternativeMonoid function
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
t.Run("with string monoid", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string, string](S.Monoid)
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
t.Run("empty returns validator that succeeds with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
@@ -25,8 +25,8 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("concat combines successful validators using monoid", func(t *testing.T) {
|
||||
validator1 := Of[string, string]("Hello")
|
||||
validator2 := Of[string, string](" World")
|
||||
validator1 := Of[string]("Hello")
|
||||
validator2 := Of[string](" World")
|
||||
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
@@ -42,7 +42,7 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
succeeding := Of[string, string]("fallback")
|
||||
succeeding := Of[string]("fallback")
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
@@ -85,7 +85,7 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("concat with empty preserves validator", func(t *testing.T) {
|
||||
validator := Of[string, string]("test")
|
||||
validator := Of[string]("test")
|
||||
empty := m.Empty()
|
||||
|
||||
result1 := m.Concat(validator, empty)("input")(nil)
|
||||
@@ -110,7 +110,7 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
m := AlternativeMonoid[string, int](intMonoid)
|
||||
m := AlternativeMonoid[string](intMonoid)
|
||||
|
||||
t.Run("empty returns validator with zero", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
@@ -124,8 +124,8 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
|
||||
validator1 := Of[string, int](10)
|
||||
validator2 := Of[string, int](32)
|
||||
validator1 := Of[string](10)
|
||||
validator2 := Of[string](32)
|
||||
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
@@ -145,7 +145,7 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
succeeding := Of[string, int](42)
|
||||
succeeding := Of[string](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
@@ -158,10 +158,10 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("multiple concat operations", func(t *testing.T) {
|
||||
validator1 := Of[string, int](1)
|
||||
validator2 := Of[string, int](2)
|
||||
validator3 := Of[string, int](3)
|
||||
validator4 := Of[string, int](4)
|
||||
validator1 := Of[string](1)
|
||||
validator2 := Of[string](2)
|
||||
validator3 := Of[string](3)
|
||||
validator4 := Of[string](4)
|
||||
|
||||
combined := m.Concat(m.Concat(m.Concat(validator1, validator2), validator3), validator4)
|
||||
result := combined("input")(nil)
|
||||
@@ -175,11 +175,11 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("satisfies monoid laws", func(t *testing.T) {
|
||||
m := AlternativeMonoid[string, string](S.Monoid)
|
||||
m := AlternativeMonoid[string](S.Monoid)
|
||||
|
||||
validator1 := Of[string, string]("a")
|
||||
validator2 := Of[string, string]("b")
|
||||
validator3 := Of[string, string]("c")
|
||||
validator1 := Of[string]("a")
|
||||
validator2 := Of[string]("b")
|
||||
validator3 := Of[string]("c")
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
result := m.Concat(m.Empty(), validator1)("input")(nil)
|
||||
@@ -222,7 +222,7 @@ func TestAlternativeMonoid(t *testing.T) {
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
t.Run("with default value as zero", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, int] {
|
||||
return Of[string, int](0)
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
t.Run("empty returns the provided zero validator", func(t *testing.T) {
|
||||
@@ -233,8 +233,8 @@ func TestAltMonoid(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("concat returns first validator when it succeeds", func(t *testing.T) {
|
||||
validator1 := Of[string, int](42)
|
||||
validator2 := Of[string, int](100)
|
||||
validator1 := Of[string](42)
|
||||
validator2 := Of[string](100)
|
||||
|
||||
combined := m.Concat(validator1, validator2)
|
||||
result := combined("input")(nil)
|
||||
@@ -250,7 +250,7 @@ func TestAltMonoid(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
succeeding := Of[string, int](42)
|
||||
succeeding := Of[string](42)
|
||||
|
||||
combined := m.Concat(failing, succeeding)
|
||||
result := combined("input")(nil)
|
||||
@@ -341,7 +341,7 @@ func TestAltMonoid(t *testing.T) {
|
||||
|
||||
t.Run("chaining multiple fallbacks", func(t *testing.T) {
|
||||
m := AltMonoid(func() Validate[string, string] {
|
||||
return Of[string, string]("default")
|
||||
return Of[string]("default")
|
||||
})
|
||||
|
||||
primary := func(input string) Reader[Context, Validation[string]] {
|
||||
@@ -358,7 +358,7 @@ func TestAltMonoid(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
tertiary := Of[string, string]("tertiary value")
|
||||
tertiary := Of[string]("tertiary value")
|
||||
|
||||
combined := m.Concat(m.Concat(primary, secondary), tertiary)
|
||||
result := combined("input")(nil)
|
||||
@@ -369,14 +369,14 @@ func TestAltMonoid(t *testing.T) {
|
||||
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
|
||||
// AltMonoid - first success wins
|
||||
altM := AltMonoid(func() Validate[string, int] {
|
||||
return Of[string, int](0)
|
||||
return Of[string](0)
|
||||
})
|
||||
|
||||
// AlternativeMonoid - combines successes
|
||||
altMonoid := AlternativeMonoid[string, int](N.MonoidSum[int]())
|
||||
altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
|
||||
|
||||
validator1 := Of[string, int](10)
|
||||
validator2 := Of[string, int](32)
|
||||
validator1 := Of[string](10)
|
||||
validator2 := Of[string](32)
|
||||
|
||||
// AltMonoid: returns first success (10)
|
||||
result1 := altM.Concat(validator1, validator2)("input")(nil)
|
||||
|
||||
@@ -160,6 +160,109 @@ func Of[I, A any](a A) Validate[I, A] {
|
||||
return reader.Of[I](decode.Of[Context](a))
|
||||
}
|
||||
|
||||
// OfLazy creates a Validate that defers the computation of a value until needed.
|
||||
//
|
||||
// This function lifts a lazy computation into the validation context. The computation
|
||||
// is deferred until the validator is actually executed, allowing for efficient handling
|
||||
// of expensive operations or values that may not always be needed.
|
||||
//
|
||||
// **IMPORTANT**: The lazy function MUST be pure (referentially transparent). It should
|
||||
// always return the same value when called and must not perform side effects. For
|
||||
// computations with side effects, use IO or IOEither types instead.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type (not used, but required for type consistency)
|
||||
// - A: The type of the value produced by the lazy computation
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: A lazy computation that produces a value of type A. This function is called
|
||||
// each time the validator is executed.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] that ignores its input and returns a successful validation containing
|
||||
// the lazily computed value.
|
||||
//
|
||||
// # Purity Requirements
|
||||
//
|
||||
// The lazy function MUST be pure:
|
||||
// - Always returns the same result for the same (lack of) input
|
||||
// - No side effects (no I/O, no mutation, no randomness)
|
||||
// - Deterministic and referentially transparent
|
||||
//
|
||||
// For side effects, use:
|
||||
// - IO types for effectful computations
|
||||
// - IOEither for effectful computations that may fail
|
||||
//
|
||||
// # Example: Deferring Expensive Computation
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Expensive computation deferred until needed
|
||||
// expensiveValue := validate.OfLazy[string, int](func() int {
|
||||
// // This computation only runs when the validator is executed
|
||||
// return computeExpensiveValue()
|
||||
// })
|
||||
//
|
||||
// result := expensiveValue("any input")(nil)
|
||||
// // result is validation.Success(computed value)
|
||||
//
|
||||
// # Example: Lazy Default Value
|
||||
//
|
||||
// // Provide a default value that's only computed if needed
|
||||
// withDefault := validate.OfLazy[Config, Config](func() Config {
|
||||
// return loadDefaultConfig()
|
||||
// })
|
||||
//
|
||||
// // Use in a validation pipeline
|
||||
// validator := F.Pipe1(
|
||||
// validateFromFile,
|
||||
// validate.Alt(func() validate.Validate[string, Config] {
|
||||
// return withDefault
|
||||
// }),
|
||||
// )
|
||||
// // Default config only loaded if file validation fails
|
||||
//
|
||||
// # Example: Composition with Other Validators
|
||||
//
|
||||
// // Combine lazy value with validation logic
|
||||
// lazyValidator := F.Pipe1(
|
||||
// validate.OfLazy[string, int](func() int { return 42 }),
|
||||
// validate.Chain(func(n int) validate.Validate[string, string] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if len(input) > n {
|
||||
// return validation.FailureWithMessage[string](input, "too long")(ctx)
|
||||
// }
|
||||
// return validation.Success(input)
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The lazy function is evaluated each time the validator is executed
|
||||
// - The input value I is ignored; the validator succeeds regardless of input
|
||||
// - The result is always wrapped in a successful validation
|
||||
// - This is useful for deferring expensive computations or providing lazy defaults
|
||||
// - The lazy function must be pure - no side effects allowed
|
||||
// - For side effects, use IO or IOEither types instead
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Of: For non-lazy values
|
||||
// - decode.OfLazy: The underlying decode operation
|
||||
// - reader.Of: The reader lifting operation
|
||||
func OfLazy[I, A any](fa Lazy[A]) Validate[I, A] {
|
||||
return reader.Of[I](decode.OfLazy[Context](fa))
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the successful result of a validation.
|
||||
//
|
||||
// This is the functor map operation for Validate. It transforms the success value
|
||||
|
||||
@@ -1274,3 +1274,139 @@ func TestOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOfLazy tests the OfLazy function
|
||||
func TestOfLazy(t *testing.T) {
|
||||
t.Run("evaluates lazy computation", func(t *testing.T) {
|
||||
// Create a validator with a lazy value
|
||||
validator := OfLazy[string, int](func() int {
|
||||
return 42
|
||||
})
|
||||
|
||||
result := validator("any input")(nil)
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("defers execution until called", func(t *testing.T) {
|
||||
executed := false
|
||||
validator := OfLazy[string, int](func() int {
|
||||
executed = true
|
||||
return 100
|
||||
})
|
||||
|
||||
// Lazy function not executed yet
|
||||
assert.False(t, executed)
|
||||
|
||||
// Execute the validator
|
||||
result := validator("input")(nil)
|
||||
|
||||
// Now it should be executed
|
||||
assert.True(t, executed)
|
||||
assert.Equal(t, validation.Success(100), result)
|
||||
})
|
||||
|
||||
t.Run("evaluates on each call", func(t *testing.T) {
|
||||
callCount := 0
|
||||
validator := OfLazy[string, int](func() int {
|
||||
callCount++
|
||||
return callCount
|
||||
})
|
||||
|
||||
// First call
|
||||
result1 := validator("input")(nil)
|
||||
assert.Equal(t, validation.Success(1), result1)
|
||||
|
||||
// Second call - evaluates again
|
||||
result2 := validator("input")(nil)
|
||||
assert.Equal(t, validation.Success(2), result2)
|
||||
|
||||
// Third call
|
||||
result3 := validator("input")(nil)
|
||||
assert.Equal(t, validation.Success(3), result3)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// String type
|
||||
stringValidator := OfLazy[int, string](func() string {
|
||||
return "hello"
|
||||
})
|
||||
result := stringValidator(42)(nil)
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
|
||||
// Struct type
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
configValidator := OfLazy[string, Config](func() Config {
|
||||
return Config{Host: "localhost", Port: 8080}
|
||||
})
|
||||
result2 := configValidator("input")(nil)
|
||||
assert.Equal(t, validation.Success(Config{Host: "localhost", Port: 8080}), result2)
|
||||
|
||||
// Slice type
|
||||
sliceValidator := OfLazy[string, []int](func() []int {
|
||||
return []int{1, 2, 3}
|
||||
})
|
||||
result3 := sliceValidator("input")(nil)
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result3)
|
||||
})
|
||||
|
||||
t.Run("composes with other validators", func(t *testing.T) {
|
||||
// Create a lazy validator that produces a number
|
||||
lazyValue := OfLazy[string, int](func() int {
|
||||
return 42
|
||||
})
|
||||
|
||||
// Map to transform the value
|
||||
validator := MonadMap(lazyValue, func(n int) int {
|
||||
return n * 2
|
||||
})
|
||||
|
||||
result := validator("any input")(nil)
|
||||
assert.Equal(t, validation.Success(84), result)
|
||||
})
|
||||
|
||||
t.Run("ignores input value", func(t *testing.T) {
|
||||
validator := OfLazy[string, int](func() int {
|
||||
return 999
|
||||
})
|
||||
|
||||
// Different inputs should produce the same result
|
||||
result1 := validator("input1")(nil)
|
||||
result2 := validator("input2")(nil)
|
||||
result3 := validator("")(nil)
|
||||
|
||||
assert.Equal(t, validation.Success(999), result1)
|
||||
assert.Equal(t, validation.Success(999), result2)
|
||||
assert.Equal(t, validation.Success(999), result3)
|
||||
})
|
||||
|
||||
t.Run("always wraps in success validation", func(t *testing.T) {
|
||||
validator := OfLazy[string, int](func() int {
|
||||
return 42
|
||||
})
|
||||
|
||||
result := validator("input")(nil)
|
||||
|
||||
// Verify it's a Right (success)
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Extract and verify the value
|
||||
value, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("works with context", func(t *testing.T) {
|
||||
validator := OfLazy[string, string](func() string {
|
||||
return "validated"
|
||||
})
|
||||
|
||||
ctx := validation.Context{
|
||||
{Key: "field", Type: "string"},
|
||||
}
|
||||
|
||||
result := validator("input")(ctx)
|
||||
assert.Equal(t, validation.Success("validated"), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -259,5 +259,42 @@ type (
|
||||
// result := LetL(lens, double)(Success(21)) // Success(42)
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lazy represents a lazily-evaluated value of type A.
|
||||
// This is an alias for lazy.Lazy[A], which defers computation until the value is needed.
|
||||
//
|
||||
// In the validation context, Lazy is used to defer expensive validation operations
|
||||
// or to break circular dependencies in validation logic.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lazyValidation := lazy.Of(func() Validation[int] {
|
||||
// // Expensive validation logic here
|
||||
// return Success(42)
|
||||
// })
|
||||
// // Validation is not executed until lazyValidation() is called
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// ErrorsProvider is an interface for types that can provide a collection of errors.
|
||||
// This interface allows validation errors to be extracted from various error types
|
||||
// in a uniform way, supporting error aggregation and reporting.
|
||||
//
|
||||
// Types implementing this interface can be unwrapped to access their underlying
|
||||
// error collection, enabling consistent error handling across different error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type MyErrors struct {
|
||||
// errs []error
|
||||
// }
|
||||
//
|
||||
// func (e *MyErrors) Errors() []error {
|
||||
// return e.errs
|
||||
// }
|
||||
//
|
||||
// // Usage
|
||||
// var provider ErrorsProvider = &MyErrors{errs: []error{...}}
|
||||
// allErrors := provider.Errors()
|
||||
ErrorsProvider interface {
|
||||
Errors() []error
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package validation
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
@@ -12,6 +13,11 @@ import (
|
||||
// Returns a generic error message indicating this is a validation error.
|
||||
// For detailed error information, use String() or Format() methods.
|
||||
|
||||
// toError converts the validation error to the error interface
|
||||
func toError(v *ValidationError) error {
|
||||
return v
|
||||
}
|
||||
|
||||
// Error implements the error interface for ValidationError.
|
||||
// Returns a generic error message.
|
||||
func (v *ValidationError) Error() string {
|
||||
@@ -34,44 +40,45 @@ func (v *ValidationError) String() string {
|
||||
// It includes the context path, message, and optionally the cause error.
|
||||
// Supports verbs: %s, %v, %+v (with additional details)
|
||||
func (v *ValidationError) Format(s fmt.State, verb rune) {
|
||||
// Build the context path
|
||||
path := ""
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path += "."
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path += entry.Key
|
||||
} else {
|
||||
path += entry.Type
|
||||
}
|
||||
}
|
||||
var result strings.Builder
|
||||
|
||||
// Start with the path if available
|
||||
result := ""
|
||||
if path != "" {
|
||||
result = fmt.Sprintf("at %s: ", path)
|
||||
// Build the context path
|
||||
if len(v.Context) > 0 {
|
||||
var path strings.Builder
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path.WriteString(".")
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path.WriteString(entry.Key)
|
||||
} else {
|
||||
path.WriteString(entry.Type)
|
||||
}
|
||||
}
|
||||
result.WriteString("at ")
|
||||
result.WriteString(path.String())
|
||||
result.WriteString(": ")
|
||||
}
|
||||
|
||||
// Add the message
|
||||
result += v.Messsage
|
||||
result.WriteString(v.Messsage)
|
||||
|
||||
// Add the cause if present
|
||||
if v.Cause != nil {
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
// Verbose format with detailed cause
|
||||
result += fmt.Sprintf("\n caused by: %+v", v.Cause)
|
||||
fmt.Fprintf(&result, "\n caused by: %+v", v.Cause)
|
||||
} else {
|
||||
result += fmt.Sprintf(" (caused by: %v)", v.Cause)
|
||||
fmt.Fprintf(&result, " (caused by: %v)", v.Cause)
|
||||
}
|
||||
}
|
||||
|
||||
// Add value information for verbose format
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
result += fmt.Sprintf("\n value: %#v", v.Value)
|
||||
fmt.Fprintf(&result, "\n value: %#v", v.Value)
|
||||
}
|
||||
|
||||
fmt.Fprint(s, result)
|
||||
fmt.Fprint(s, result.String())
|
||||
}
|
||||
|
||||
// LogValue implements the slog.LogValuer interface for ValidationError.
|
||||
@@ -94,18 +101,18 @@ func (v *ValidationError) LogValue() slog.Value {
|
||||
|
||||
// Add context path if available
|
||||
if len(v.Context) > 0 {
|
||||
path := ""
|
||||
var path strings.Builder
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path += "."
|
||||
path.WriteString(".")
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path += entry.Key
|
||||
path.WriteString(entry.Key)
|
||||
} else {
|
||||
path += entry.Type
|
||||
path.WriteString(entry.Type)
|
||||
}
|
||||
}
|
||||
attrs = append(attrs, slog.String("path", path))
|
||||
attrs = append(attrs, slog.String("path", path.String()))
|
||||
}
|
||||
|
||||
// Add cause if present
|
||||
@@ -119,13 +126,14 @@ func (v *ValidationError) LogValue() slog.Value {
|
||||
// Error implements the error interface for ValidationErrors.
|
||||
// Returns a generic error message indicating validation errors occurred.
|
||||
func (ve *validationErrors) Error() string {
|
||||
if len(ve.errors) == 0 {
|
||||
switch len(ve.errors) {
|
||||
case 0:
|
||||
return "ValidationErrors: no errors"
|
||||
}
|
||||
if len(ve.errors) == 1 {
|
||||
case 1:
|
||||
return "ValidationErrors: 1 error"
|
||||
default:
|
||||
return fmt.Sprintf("ValidationErrors: %d errors", len(ve.errors))
|
||||
}
|
||||
return fmt.Sprintf("ValidationErrors: %d errors", len(ve.errors))
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause error if present.
|
||||
@@ -134,6 +142,33 @@ func (ve *validationErrors) Unwrap() error {
|
||||
return ve.cause
|
||||
}
|
||||
|
||||
// Errors implements the ErrorsProvider interface for validationErrors.
|
||||
// It converts the internal collection of ValidationError pointers to a slice of error interfaces.
|
||||
// This method enables uniform error extraction from validation error collections.
|
||||
//
|
||||
// The returned slice contains the same errors as the internal errors field,
|
||||
// but typed as error interface values for compatibility with standard Go error handling.
|
||||
//
|
||||
// Returns:
|
||||
// - A slice of error interfaces, one for each ValidationError in the collection
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ve := &validationErrors{
|
||||
// errors: Errors{
|
||||
// &ValidationError{Messsage: "invalid email"},
|
||||
// &ValidationError{Messsage: "age must be positive"},
|
||||
// },
|
||||
// }
|
||||
// errs := ve.Errors()
|
||||
// // errs is []error with 2 elements, each implementing the error interface
|
||||
// for _, err := range errs {
|
||||
// fmt.Println(err.Error()) // "ValidationError"
|
||||
// }
|
||||
func (ve *validationErrors) Errors() []error {
|
||||
return A.MonadMap(ve.errors, toError)
|
||||
}
|
||||
|
||||
// String returns a simple string representation of all validation errors.
|
||||
// Each error is listed on a separate line with its index.
|
||||
func (ve *validationErrors) String() string {
|
||||
@@ -141,16 +176,17 @@ func (ve *validationErrors) String() string {
|
||||
return "ValidationErrors: no errors"
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("ValidationErrors (%d):\n", len(ve.errors))
|
||||
var result strings.Builder
|
||||
fmt.Fprintf(&result, "ValidationErrors (%d):\n", len(ve.errors))
|
||||
for i, err := range ve.errors {
|
||||
result += fmt.Sprintf(" [%d] %s\n", i, err.String())
|
||||
fmt.Fprintf(&result, " [%d] %s\n", i, err.String())
|
||||
}
|
||||
|
||||
if ve.cause != nil {
|
||||
result += fmt.Sprintf(" caused by: %v\n", ve.cause)
|
||||
fmt.Fprintf(&result, " caused by: %v\n", ve.cause)
|
||||
}
|
||||
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for custom formatting of ValidationErrors.
|
||||
|
||||
@@ -846,3 +846,142 @@ func TestLogValuerInterface(t *testing.T) {
|
||||
var _ slog.LogValuer = (*validationErrors)(nil)
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidationErrors_Errors tests the Errors() method implementation
|
||||
func TestValidationErrors_Errors(t *testing.T) {
|
||||
t.Run("returns empty slice for no errors", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
assert.Empty(t, errs)
|
||||
assert.NotNil(t, errs)
|
||||
})
|
||||
|
||||
t.Run("converts single ValidationError to error interface", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Value: "test", Messsage: "invalid value"},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, "ValidationError", errs[0].Error())
|
||||
})
|
||||
|
||||
t.Run("converts multiple ValidationErrors to error interfaces", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
&ValidationError{Value: "test3", Messsage: "error 3"},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 3)
|
||||
for _, err := range errs {
|
||||
assert.Equal(t, "ValidationError", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves error details in converted errors", func(t *testing.T) {
|
||||
originalErr := &ValidationError{
|
||||
Value: "abc",
|
||||
Context: []ContextEntry{{Key: "field"}},
|
||||
Messsage: "invalid format",
|
||||
Cause: errors.New("parse error"),
|
||||
}
|
||||
ve := &validationErrors{
|
||||
errors: Errors{originalErr},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 1)
|
||||
|
||||
// Verify the error can be type-asserted back to ValidationError
|
||||
validationErr, ok := errs[0].(*ValidationError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "abc", validationErr.Value)
|
||||
assert.Equal(t, "invalid format", validationErr.Messsage)
|
||||
assert.NotNil(t, validationErr.Cause)
|
||||
assert.Len(t, validationErr.Context, 1)
|
||||
})
|
||||
|
||||
t.Run("implements ErrorsProvider interface", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Messsage: "error 1"},
|
||||
&ValidationError{Messsage: "error 2"},
|
||||
},
|
||||
}
|
||||
|
||||
// Verify it implements ErrorsProvider
|
||||
var provider ErrorsProvider = ve
|
||||
errs := provider.Errors()
|
||||
assert.Len(t, errs, 2)
|
||||
})
|
||||
|
||||
t.Run("returned errors are usable with standard error handling", func(t *testing.T) {
|
||||
cause := errors.New("underlying error")
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "validation failed",
|
||||
Cause: cause,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 1)
|
||||
|
||||
// Test with errors.Is
|
||||
assert.True(t, errors.Is(errs[0], cause))
|
||||
|
||||
// Test with errors.As
|
||||
var validationErr *ValidationError
|
||||
assert.True(t, errors.As(errs[0], &validationErr))
|
||||
assert.Equal(t, "validation failed", validationErr.Messsage)
|
||||
})
|
||||
|
||||
t.Run("does not modify original errors slice", func(t *testing.T) {
|
||||
originalErrors := Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
}
|
||||
ve := &validationErrors{
|
||||
errors: originalErrors,
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 2)
|
||||
|
||||
// Original should be unchanged
|
||||
assert.Len(t, ve.errors, 2)
|
||||
assert.Equal(t, originalErrors, ve.errors)
|
||||
})
|
||||
|
||||
t.Run("each error in slice is independent", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 2)
|
||||
|
||||
// Verify each error is distinct
|
||||
err1, ok1 := errs[0].(*ValidationError)
|
||||
err2, ok2 := errs[1].(*ValidationError)
|
||||
require.True(t, ok1)
|
||||
require.True(t, ok2)
|
||||
assert.NotEqual(t, err1.Messsage, err2.Messsage)
|
||||
assert.NotEqual(t, err1.Value, err2.Value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1405,7 +1405,7 @@ func TestFromResult(t *testing.T) {
|
||||
t.Run("extract from successful result", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
|
||||
success := result.Of[int](42)
|
||||
success := result.Of(42)
|
||||
extracted := prism.GetOption(success)
|
||||
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
@@ -1435,7 +1435,7 @@ func TestFromResult(t *testing.T) {
|
||||
t.Run("works with string type", func(t *testing.T) {
|
||||
prism := FromResult[string]()
|
||||
|
||||
success := result.Of[string]("hello")
|
||||
success := result.Of("hello")
|
||||
extracted := prism.GetOption(success)
|
||||
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
@@ -1451,7 +1451,7 @@ func TestFromResult(t *testing.T) {
|
||||
prism := FromResult[Person]()
|
||||
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
success := result.Of[Person](person)
|
||||
success := result.Of(person)
|
||||
extracted := prism.GetOption(success)
|
||||
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
@@ -1465,9 +1465,9 @@ func TestFromResult(t *testing.T) {
|
||||
func TestFromResultWithSet(t *testing.T) {
|
||||
t.Run("set on successful result", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
setter := Set[result.Result[int], int](200)
|
||||
setter := Set[result.Result[int]](200)
|
||||
|
||||
success := result.Of[int](42)
|
||||
success := result.Of(42)
|
||||
updated := setter(prism)(success)
|
||||
|
||||
// Verify the value was updated
|
||||
@@ -1478,7 +1478,7 @@ func TestFromResultWithSet(t *testing.T) {
|
||||
|
||||
t.Run("set on error result leaves it unchanged", func(t *testing.T) {
|
||||
prism := FromResult[int]()
|
||||
setter := Set[result.Result[int], int](200)
|
||||
setter := Set[result.Result[int]](200)
|
||||
|
||||
failure := E.Left[int](errors.New("test error"))
|
||||
updated := setter(prism)(failure)
|
||||
@@ -1527,13 +1527,13 @@ func TestFromResultComposition(t *testing.T) {
|
||||
composed := Compose[result.Result[int]](positivePrism)(FromResult[int]())
|
||||
|
||||
// Test with positive number
|
||||
success := result.Of[int](42)
|
||||
success := result.Of(42)
|
||||
extracted := composed.GetOption(success)
|
||||
assert.True(t, O.IsSome(extracted))
|
||||
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
|
||||
|
||||
// Test with negative number
|
||||
negativeSuccess := result.Of[int](-5)
|
||||
negativeSuccess := result.Of(-5)
|
||||
extracted = composed.GetOption(negativeSuccess)
|
||||
assert.True(t, O.IsNone(extracted))
|
||||
|
||||
@@ -1705,7 +1705,7 @@ func TestParseJSONWithSet(t *testing.T) {
|
||||
originalJSON := []byte(`{"name":"Alice","age":30}`)
|
||||
newPerson := Person{Name: "Bob", Age: 25}
|
||||
|
||||
setter := Set[[]byte, Person](newPerson)
|
||||
setter := Set[[]byte](newPerson)
|
||||
updatedJSON := setter(prism)(originalJSON)
|
||||
|
||||
// Parse the updated JSON
|
||||
@@ -1722,7 +1722,7 @@ func TestParseJSONWithSet(t *testing.T) {
|
||||
invalidJSON := []byte(`{invalid}`)
|
||||
newPerson := Person{Name: "Charlie", Age: 35}
|
||||
|
||||
setter := Set[[]byte, Person](newPerson)
|
||||
setter := Set[[]byte](newPerson)
|
||||
result := setter(prism)(invalidJSON)
|
||||
|
||||
// Should return original unchanged since it couldn't be parsed
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user