mirror of
https://github.com/IBM/fp-go.git
synced 2026-04-22 20:33:06 +02:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b288003f93 | |||
| aed19f6edf | |||
| 45cc0a7fc1 | |||
| 21b517d388 | |||
| 0df62c0031 | |||
| 57318e2d1d | |||
| 2b937d3e93 | |||
| 747a1794e5 | |||
| c754cacf1f | |||
| d357b32847 | |||
| a3af003e74 | |||
| c81235827b | |||
| f35430cf18 | |||
| d3ffc71808 | |||
| 62844b7030 | |||
| 99a0ddd4b6 | |||
| 02acbae8f6 | |||
| eb27ecdc01 | |||
| e5eb7d343c | |||
| d5a3217251 | |||
| c5cbdaad68 | |||
| 5d0f27ad10 | |||
| 3a954e0d1f | |||
| cb2e0b23e8 | |||
| 8d5dc7ea1f | |||
| 69a11bc681 | |||
| a0910b8279 | |||
| 029d7be52d | |||
| c6d30bb642 | |||
| 1821f00fbe | |||
| f0ec0b2541 | |||
| ce3c7d9359 | |||
| 3ed354cc8c | |||
| 0932c8c464 | |||
| 475d09e987 | |||
| fd21bdeabf |
@@ -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
|
||||
|
||||
@@ -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+.
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
@@ -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).
|
||||
+66
-59
@@ -2,14 +2,30 @@
|
||||
|
||||
This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Documentation Standards](#documentation-standards)
|
||||
- [Go Doc Comments](#go-doc-comments)
|
||||
- [File Headers](#file-headers)
|
||||
- [Testing Standards](#testing-standards)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Test Coverage](#test-coverage)
|
||||
- [Example Test Pattern](#example-test-pattern)
|
||||
- [Code Style](#code-style)
|
||||
- [Functional Patterns](#functional-patterns)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Checklist for New Code](#checklist-for-new-code)
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Go Doc Comments
|
||||
|
||||
1. **Use Standard Go Doc Format**
|
||||
- Do NOT use markdown-style links like `[text](url)`
|
||||
- Do NOT use markdown-style headers like `# Section` or `## Subsection`
|
||||
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
|
||||
- Go's documentation system will automatically create links
|
||||
- Use plain text with blank lines to separate sections
|
||||
|
||||
2. **Structure**
|
||||
```go
|
||||
@@ -17,24 +33,20 @@ This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
//
|
||||
// Longer description explaining the purpose and behavior.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: Description of type parameter
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// Parameters:
|
||||
// - param: Description of parameter
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// Returns:
|
||||
// - ReturnType: Description of return value
|
||||
//
|
||||
// # Example Usage
|
||||
// Example:
|
||||
//
|
||||
// code example here
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// See Also:
|
||||
// - RelatedFunction: Brief description
|
||||
func FunctionName[T any](param T) ReturnType {
|
||||
```
|
||||
@@ -43,6 +55,7 @@ This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
- Use idiomatic Go patterns
|
||||
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
|
||||
- Show realistic, runnable examples
|
||||
- Indent code examples with spaces (not tabs) for proper godoc rendering
|
||||
|
||||
### File Headers
|
||||
|
||||
@@ -102,6 +115,50 @@ Always include the Apache 2.0 license header:
|
||||
- Use `result.Of` for success values
|
||||
- Use `result.Left` for error values
|
||||
|
||||
4. **Folding Either/Result Values in Tests**
|
||||
- Use `F.Pipe1(result, Fold(onLeft, onRight))` — avoid the `_ = Fold(...)(result)` discard pattern
|
||||
- Use `slices.Collect[T]` instead of a manual `for n := range seq { collected = append(...) }` loop
|
||||
- Use `t.Fatal` in the unexpected branch to combine the `IsLeft`/`IsRight` check with value extraction:
|
||||
```go
|
||||
// Good: single fold combines assertion and extraction
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
// Avoid: separate IsRight check + manual loop
|
||||
assert.True(t, IsRight(result))
|
||||
var collected []int
|
||||
_ = MonadFold(result,
|
||||
func(e error) []int { return nil },
|
||||
func(seq iter.Seq[int]) []int {
|
||||
for n := range seq { collected = append(collected, n) }
|
||||
return collected
|
||||
},
|
||||
)
|
||||
```
|
||||
- Use `F.Identity[error]` as the Left branch when extracting an error value:
|
||||
```go
|
||||
err := F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
```
|
||||
- Extract repeated fold patterns as local helper closures within the test function:
|
||||
```go
|
||||
collectInts := func(r Result[iter.Seq[int]]) []int {
|
||||
return F.Pipe1(r, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
5. **Other Test Style Details**
|
||||
- Use `for i := range 10` instead of `for i := 0; i < 10; i++`
|
||||
- Chain curried calls directly: `TraverseSeq(parse)(input)` — no need for an intermediate `traverseFn` variable
|
||||
- Use direct slice literals (`[]string{"a", "b"}`) rather than `A.From("a", "b")` in tests
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Include tests for:
|
||||
@@ -168,56 +225,6 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
- Check error context is preserved
|
||||
- Test error accumulation when applicable
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Converting Error-Based Functions
|
||||
|
||||
```go
|
||||
// Good: Use Eitherize1
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
// Avoid: Manual error handling
|
||||
parseIntRR := func(input string) result.Result[int] {
|
||||
val, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return result.Left[int](err)
|
||||
}
|
||||
return result.Of(val)
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Validation Results
|
||||
|
||||
```go
|
||||
// Good: Direct comparison
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Avoid: Verbose extraction (unless you need to verify specific fields)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
```
|
||||
|
||||
### Documentation Examples
|
||||
|
||||
```go
|
||||
// Good: Concise and idiomatic
|
||||
// parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
// validator := FromReaderResult[string, int](parseIntRR)
|
||||
|
||||
// Avoid: Verbose manual patterns
|
||||
// parseIntRR := func(input string) result.Result[int] {
|
||||
// val, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return result.Left[int](err)
|
||||
// }
|
||||
// return result.Of(val)
|
||||
// }
|
||||
```
|
||||
|
||||
## Checklist for New Code
|
||||
|
||||
- [ ] Apache 2.0 license header included
|
||||
|
||||
+725
-49
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
}
|
||||
+287
-3
@@ -198,11 +198,228 @@ func TestFilterMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFoldMap(t *testing.T) {
|
||||
src := From("a", "b", "c")
|
||||
t.Run("FoldMap with 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(empty)
|
||||
assert.Equal(t, 0, result, "FoldMap should return monoid empty for 0 items")
|
||||
})
|
||||
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
t.Run("FoldMap with 1 item", func(t *testing.T) {
|
||||
single := From(5)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(single)
|
||||
assert.Equal(t, 10, result, "FoldMap should map and return single item")
|
||||
})
|
||||
|
||||
assert.Equal(t, "ABC", fold(src))
|
||||
t.Run("FoldMap with 2 items", func(t *testing.T) {
|
||||
two := From(3, 4)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(two)
|
||||
assert.Equal(t, 14, result, "FoldMap should map and fold 2 items: (3*2) + (4*2) = 14")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with many items", func(t *testing.T) {
|
||||
many := From(1, 2, 3, 4, 5)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(many)
|
||||
assert.Equal(t, 30, result, "FoldMap should map and fold many items: (1*2) + (2*2) + (3*2) + (4*2) + (5*2) = 30")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - 0 items", func(t *testing.T) {
|
||||
empty := []string{}
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, "", result, "FoldMap should return empty string for 0 items")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - 1 item", func(t *testing.T) {
|
||||
single := From("a")
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(single)
|
||||
assert.Equal(t, "A", result, "FoldMap should map single string")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - 2 items", func(t *testing.T) {
|
||||
two := From("a", "b")
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(two)
|
||||
assert.Equal(t, "AB", result, "FoldMap should map and concatenate 2 strings")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - many items", func(t *testing.T) {
|
||||
many := From("a", "b", "c", "d", "e")
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(many)
|
||||
assert.Equal(t, "ABCDE", result, "FoldMap should map and concatenate many strings")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
t.Run("Fold with 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, 0, result, "Fold should return monoid empty for 0 items")
|
||||
})
|
||||
|
||||
t.Run("Fold with 1 item", func(t *testing.T) {
|
||||
single := From(42)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(single)
|
||||
assert.Equal(t, 42, result, "Fold should return single item")
|
||||
})
|
||||
|
||||
t.Run("Fold with 2 items", func(t *testing.T) {
|
||||
two := From(10, 20)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(two)
|
||||
assert.Equal(t, 30, result, "Fold should combine 2 items: 10 + 20 = 30")
|
||||
})
|
||||
|
||||
t.Run("Fold with many items", func(t *testing.T) {
|
||||
many := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(many)
|
||||
assert.Equal(t, 55, result, "Fold should combine many items: 1+2+3+4+5+6+7+8+9+10 = 55")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - 0 items", func(t *testing.T) {
|
||||
empty := []string{}
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, "", result, "Fold should return empty string for 0 items")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - 1 item", func(t *testing.T) {
|
||||
single := From("hello")
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(single)
|
||||
assert.Equal(t, "hello", result, "Fold should return single string")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - 2 items", func(t *testing.T) {
|
||||
two := From("hello", "world")
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(two)
|
||||
assert.Equal(t, "helloworld", result, "Fold should concatenate 2 strings")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - many items", func(t *testing.T) {
|
||||
many := From("a", "b", "c", "d", "e", "f")
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(many)
|
||||
assert.Equal(t, "abcdef", result, "Fold should concatenate many strings")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, 1, result, "Fold should return monoid empty (1) for product with 0 items")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - 1 item", func(t *testing.T) {
|
||||
single := From(7)
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(single)
|
||||
assert.Equal(t, 7, result, "Fold should return single item for product")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - 2 items", func(t *testing.T) {
|
||||
two := From(3, 4)
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(two)
|
||||
assert.Equal(t, 12, result, "Fold should multiply 2 items: 3 * 4 = 12")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - many items", func(t *testing.T) {
|
||||
many := From(2, 3, 4, 5)
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(many)
|
||||
assert.Equal(t, 120, result, "Fold should multiply many items: 2*3*4*5 = 120")
|
||||
})
|
||||
}
|
||||
func TestFoldMapWithIndex(t *testing.T) {
|
||||
t.Run("FoldMapWithIndex with 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
|
||||
result := foldMap(empty)
|
||||
assert.Equal(t, 0, result, "FoldMapWithIndex should return monoid empty for 0 items")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with 1 item", func(t *testing.T) {
|
||||
single := From(10)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
|
||||
result := foldMap(single)
|
||||
assert.Equal(t, 10, result, "FoldMapWithIndex should map with index: 0 + 10 = 10")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with 2 items", func(t *testing.T) {
|
||||
two := From(10, 20)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
|
||||
result := foldMap(two)
|
||||
assert.Equal(t, 31, result, "FoldMapWithIndex should map with indices: (0+10) + (1+20) = 31")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with many items", func(t *testing.T) {
|
||||
many := From(5, 10, 15, 20)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i * x })
|
||||
result := foldMap(many)
|
||||
assert.Equal(t, 100, result, "FoldMapWithIndex should map with indices: (0*5) + (1*10) + (2*15) + (3*20) = 100")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - 0 items", func(t *testing.T) {
|
||||
empty := []string{}
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := foldMap(empty)
|
||||
assert.Equal(t, "", result, "FoldMapWithIndex should return empty string for 0 items")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - 1 item", func(t *testing.T) {
|
||||
single := From("a")
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := foldMap(single)
|
||||
assert.Equal(t, "0:a", result, "FoldMapWithIndex should format single item with index")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - 2 items", func(t *testing.T) {
|
||||
two := From("a", "b")
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s,", i, s)
|
||||
})
|
||||
result := foldMap(two)
|
||||
assert.Equal(t, "0:a,1:b,", result, "FoldMapWithIndex should format 2 items with indices")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - many items", func(t *testing.T) {
|
||||
many := From("a", "b", "c", "d")
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("[%d]%s", i, s)
|
||||
})
|
||||
result := foldMap(many)
|
||||
assert.Equal(t, "[0]a[1]b[2]c[3]d", result, "FoldMapWithIndex should format many items with indices")
|
||||
})
|
||||
}
|
||||
|
||||
func ExampleFoldMap() {
|
||||
@@ -767,6 +984,25 @@ func TestExtendUseCases(t *testing.T) {
|
||||
|
||||
// TestConcat tests the Concat function
|
||||
func TestConcat(t *testing.T) {
|
||||
t.Run("Semantic: Concat(b)(a) produces [a... b...]", func(t *testing.T) {
|
||||
a := []int{1, 2, 3}
|
||||
b := []int{4, 5, 6}
|
||||
|
||||
// Concat(b)(a) should produce [a... b...]
|
||||
result := Concat(b)(a)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
|
||||
assert.Equal(t, expected, result, "Concat(b)(a) should produce [a... b...]")
|
||||
|
||||
// Verify order: a's elements come first, then b's elements
|
||||
assert.Equal(t, a[0], result[0], "First element should be from a")
|
||||
assert.Equal(t, a[1], result[1], "Second element should be from a")
|
||||
assert.Equal(t, a[2], result[2], "Third element should be from a")
|
||||
assert.Equal(t, b[0], result[3], "Fourth element should be from b")
|
||||
assert.Equal(t, b[1], result[4], "Fifth element should be from b")
|
||||
assert.Equal(t, b[2], result[5], "Sixth element should be from b")
|
||||
})
|
||||
|
||||
t.Run("Concat two non-empty arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
@@ -870,6 +1106,54 @@ func TestConcat(t *testing.T) {
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
t.Run("Explicit append semantic demonstration", func(t *testing.T) {
|
||||
// Given a base array
|
||||
base := []string{"A", "B", "C"}
|
||||
|
||||
// And a suffix to append
|
||||
suffix := []string{"D", "E", "F"}
|
||||
|
||||
// When we apply Concat(suffix) to base
|
||||
appendSuffix := Concat(suffix)
|
||||
result := appendSuffix(base)
|
||||
|
||||
// Then the result should be base followed by suffix
|
||||
expected := []string{"A", "B", "C", "D", "E", "F"}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// And the base should be unchanged
|
||||
assert.Equal(t, []string{"A", "B", "C"}, base)
|
||||
|
||||
// And the suffix should be unchanged
|
||||
assert.Equal(t, []string{"D", "E", "F"}, suffix)
|
||||
})
|
||||
|
||||
t.Run("Append semantic with different types", func(t *testing.T) {
|
||||
// Integers
|
||||
intResult := Concat([]int{4, 5})([]int{1, 2, 3})
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, intResult)
|
||||
|
||||
// Strings
|
||||
strResult := Concat([]string{"world"})([]string{"hello"})
|
||||
assert.Equal(t, []string{"hello", "world"}, strResult)
|
||||
|
||||
// Floats
|
||||
floatResult := Concat([]float64{3.3, 4.4})([]float64{1.1, 2.2})
|
||||
assert.Equal(t, []float64{1.1, 2.2, 3.3, 4.4}, floatResult)
|
||||
})
|
||||
|
||||
t.Run("Append semantic in pipeline", func(t *testing.T) {
|
||||
// Start with [1, 2, 3]
|
||||
// Append [4, 5] to get [1, 2, 3, 4, 5]
|
||||
// Append [6, 7] to get [1, 2, 3, 4, 5, 6, 7]
|
||||
result := F.Pipe2(
|
||||
[]int{1, 2, 3},
|
||||
Concat([]int{4, 5}),
|
||||
Concat([]int{6, 7}),
|
||||
)
|
||||
expected := []int{1, 2, 3, 4, 5, 6, 7}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatComposition tests Concat with other array operations
|
||||
|
||||
@@ -323,34 +323,49 @@ func Clone[AS ~[]A, A any](f func(A) A) func(as AS) AS {
|
||||
}
|
||||
|
||||
func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B {
|
||||
empty := m.Empty()
|
||||
concat := m.Concat
|
||||
return func(f func(A) B) func(AS) B {
|
||||
return func(as AS) B {
|
||||
return array.Reduce(as, func(cur B, a A) B {
|
||||
return concat(cur, f(a))
|
||||
}, empty)
|
||||
switch len(as) {
|
||||
case 0:
|
||||
return m.Empty()
|
||||
case 1:
|
||||
return f(as[0])
|
||||
case 2:
|
||||
return concat(f(as[0]), f(as[1]))
|
||||
default:
|
||||
return array.Reduce(as[1:], func(cur B, a A) B {
|
||||
return concat(cur, f(a))
|
||||
}, f(as[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FoldMapWithIndex[AS ~[]A, A, B any](m M.Monoid[B]) func(func(int, A) B) func(AS) B {
|
||||
empty := m.Empty()
|
||||
concat := m.Concat
|
||||
return func(f func(int, A) B) func(AS) B {
|
||||
return func(as AS) B {
|
||||
return array.ReduceWithIndex(as, func(idx int, cur B, a A) B {
|
||||
return concat(cur, f(idx, a))
|
||||
}, empty)
|
||||
}, m.Empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A {
|
||||
empty := m.Empty()
|
||||
concat := m.Concat
|
||||
return func(as AS) A {
|
||||
return array.Reduce(as, concat, empty)
|
||||
switch len(as) {
|
||||
case 0:
|
||||
return m.Empty()
|
||||
case 1:
|
||||
return as[0]
|
||||
case 2:
|
||||
return concat(as[0], as[1])
|
||||
default:
|
||||
return array.Reduce(as[1:], concat, as[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,3 +489,16 @@ func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
|
||||
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAt[GT ~[]T, T any](i int, v T) func(GT) O.Option[GT] {
|
||||
none := O.None[GT]()
|
||||
if i < 0 {
|
||||
return F.Constant1[GT](none)
|
||||
}
|
||||
return func(g GT) O.Option[GT] {
|
||||
if i >= len(g) {
|
||||
return none
|
||||
}
|
||||
return O.Of(array.UnsafeUpdateAt(g, i, v))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func MonadSequence[HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
m M.Monoid[HKTRA],
|
||||
ma []HKTA) HKTRA {
|
||||
return array.MonadSequence(fof, m.Empty(), m.Concat, ma)
|
||||
return array.MonadSequence(fof, m.Empty, m.Concat, ma)
|
||||
}
|
||||
|
||||
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
|
||||
@@ -67,7 +67,7 @@ func Sequence[HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
m M.Monoid[HKTRA],
|
||||
) func([]HKTA) HKTRA {
|
||||
return array.Sequence[[]HKTA](fof, m.Empty(), m.Concat)
|
||||
return array.Sequence[[]HKTA](fof, m.Empty, m.Concat)
|
||||
}
|
||||
|
||||
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.
|
||||
|
||||
@@ -13,28 +13,218 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package constraints defines a set of useful type constraints for generic programming in Go.
|
||||
|
||||
# Overview
|
||||
|
||||
This package provides type constraints that can be used with Go generics to restrict
|
||||
type parameters to specific categories of types. These constraints are similar to those
|
||||
in Go's standard constraints package but are defined here for consistency within the
|
||||
fp-go project.
|
||||
|
||||
# Type Constraints
|
||||
|
||||
Ordered - Types that support comparison operators:
|
||||
|
||||
type Ordered interface {
|
||||
Integer | Float | ~string
|
||||
}
|
||||
|
||||
Used for types that can be compared using <, <=, >, >= operators.
|
||||
|
||||
Integer - All integer types (signed and unsigned):
|
||||
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
||||
|
||||
Signed - Signed integer types:
|
||||
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
Unsigned - Unsigned integer types:
|
||||
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
Float - Floating-point types:
|
||||
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
Complex - Complex number types:
|
||||
|
||||
type Complex interface {
|
||||
~complex64 | ~complex128
|
||||
}
|
||||
|
||||
# Usage Examples
|
||||
|
||||
Using Ordered constraint for comparison:
|
||||
|
||||
import C "github.com/IBM/fp-go/v2/constraints"
|
||||
|
||||
func Min[T C.Ordered](a, b T) T {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
result := Min(5, 3) // 3
|
||||
result := Min(3.14, 2.71) // 2.71
|
||||
result := Min("apple", "banana") // "apple"
|
||||
|
||||
Using Integer constraint:
|
||||
|
||||
func Abs[T C.Integer](n T) T {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
result := Abs(-42) // 42
|
||||
result := Abs(uint(10)) // 10
|
||||
|
||||
Using Float constraint:
|
||||
|
||||
func Average[T C.Float](a, b T) T {
|
||||
return (a + b) / 2
|
||||
}
|
||||
|
||||
result := Average(3.14, 2.86) // 3.0
|
||||
|
||||
Using Complex constraint:
|
||||
|
||||
func Magnitude[T C.Complex](c T) float64 {
|
||||
r, i := real(c), imag(c)
|
||||
return math.Sqrt(r*r + i*i)
|
||||
}
|
||||
|
||||
c := complex(3, 4)
|
||||
result := Magnitude(c) // 5.0
|
||||
|
||||
# Combining Constraints
|
||||
|
||||
Constraints can be combined to create more specific type restrictions:
|
||||
|
||||
type Number interface {
|
||||
C.Integer | C.Float | C.Complex
|
||||
}
|
||||
|
||||
func Add[T Number](a, b T) T {
|
||||
return a + b
|
||||
}
|
||||
|
||||
# Tilde Operator
|
||||
|
||||
The ~ operator in type constraints means "underlying type". For example, ~int
|
||||
matches not only int but also any type whose underlying type is int:
|
||||
|
||||
type MyInt int
|
||||
|
||||
func Double[T C.Integer](n T) T {
|
||||
return n * 2
|
||||
}
|
||||
|
||||
var x MyInt = 5
|
||||
result := Double(x) // Works because MyInt's underlying type is int
|
||||
|
||||
# Related Packages
|
||||
|
||||
- number: Provides algebraic structures and utilities for numeric types
|
||||
- ord: Provides ordering operations using these constraints
|
||||
- eq: Provides equality operations for comparable types
|
||||
*/
|
||||
package constraints
|
||||
|
||||
// Ordered is a constraint that permits any ordered type: any type that supports
|
||||
// the operators < <= >= >. Ordered types include integers, floats, and strings.
|
||||
//
|
||||
// This constraint is commonly used for comparison operations, sorting, and
|
||||
// finding minimum/maximum values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Max[T Ordered](a, b T) T {
|
||||
// if a > b {
|
||||
// return a
|
||||
// }
|
||||
// return b
|
||||
// }
|
||||
type Ordered interface {
|
||||
Integer | Float | ~string
|
||||
}
|
||||
|
||||
// Signed is a constraint that permits any signed integer type.
|
||||
// This includes int, int8, int16, int32, and int64, as well as any
|
||||
// types whose underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Negate[T Signed](n T) T {
|
||||
// return -n
|
||||
// }
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
// Unsigned is a constraint that permits any unsigned integer type.
|
||||
// This includes uint, uint8, uint16, uint32, uint64, and uintptr, as well
|
||||
// as any types whose underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func IsEven[T Unsigned](n T) bool {
|
||||
// return n%2 == 0
|
||||
// }
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
// Integer is a constraint that permits any integer type, both signed and unsigned.
|
||||
// This is a union of the Signed and Unsigned constraints.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Abs[T Integer](n T) T {
|
||||
// if n < 0 {
|
||||
// return -n
|
||||
// }
|
||||
// return n
|
||||
// }
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
||||
|
||||
// Float is a constraint that permits any floating-point type.
|
||||
// This includes float32 and float64, as well as any types whose
|
||||
// underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Round[T Float](f T) T {
|
||||
// return T(math.Round(float64(f)))
|
||||
// }
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// Complex is a constraint that permits any complex numeric type.
|
||||
// This includes complex64 and complex128, as well as any types whose
|
||||
// underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Conjugate[T Complex](c T) T {
|
||||
// return complex(real(c), -imag(c))
|
||||
// }
|
||||
type Complex interface {
|
||||
~complex64 | ~complex128
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
)
|
||||
@@ -452,5 +452,3 @@ func BenchmarkWithResource(b *testing.B) {
|
||||
operation(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -652,9 +652,9 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// type key int
|
||||
// const userKey key = 0
|
||||
//
|
||||
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// addUser := readerio.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||
// return newCtx, func() {} // No-op cancel
|
||||
// return pair.MakePair(func() {}, newCtx) // No-op cancel
|
||||
// })
|
||||
//
|
||||
// getUser := readerio.FromReader(func(ctx context.Context) string {
|
||||
@@ -673,8 +673,9 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// Timeout Example:
|
||||
//
|
||||
// // Add a 5-second timeout to a specific operation
|
||||
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 5*time.Second)
|
||||
// withTimeout := readerio.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
|
||||
@@ -18,6 +18,10 @@ package readerioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/internal/witherable"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
@@ -49,3 +53,43 @@ import (
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return RIOR.FilterOrElse[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Filter[HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[HKTA, HKTA] {
|
||||
return witherable.Filter(
|
||||
Map,
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterArray[A any](p Predicate[A]) Operator[[]A, []A] {
|
||||
return Filter(array.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterIter[A any](p Predicate[A]) Operator[Seq[A], Seq[A]] {
|
||||
return Filter(iter.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMap[HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[HKTA, HKTB] {
|
||||
return witherable.FilterMap(
|
||||
Map,
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapArray[A, B any](p option.Kleisli[A, B]) Operator[[]A, []B] {
|
||||
return FilterMap(array.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapIter[A, B any](p option.Kleisli[A, B]) Operator[Seq[A], Seq[B]] {
|
||||
return FilterMap(iter.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
@@ -413,5 +413,3 @@ func isRight[A any](res Result[A]) bool {
|
||||
func isLeft[A any](res Result[A]) bool {
|
||||
return result.IsLeft(res)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -1028,9 +1028,9 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// type key int
|
||||
// const userKey key = 0
|
||||
//
|
||||
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// addUser := readerioresult.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||
// return newCtx, func() {} // No-op cancel
|
||||
// return pair.MakePair(func() {}, newCtx) // No-op cancel
|
||||
// })
|
||||
//
|
||||
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
|
||||
@@ -1049,8 +1049,9 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// Timeout Example:
|
||||
//
|
||||
// // Add a 5-second timeout to a specific operation
|
||||
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 5*time.Second)
|
||||
// withTimeout := readerioresult.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
|
||||
@@ -52,7 +52,7 @@ import (
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Trampoline[A, B]]) that:
|
||||
// - Takes the current state A
|
||||
// - Returns a ReaderIOResult that depends on [context.Context]
|
||||
// - Returns a ReaderIOResult that depends on context.Context
|
||||
// - Can fail with error (Left in the outer Either)
|
||||
// - Produces Trampoline[A, B] to control recursion flow (Right in the outer Either)
|
||||
//
|
||||
@@ -60,13 +60,13 @@ import (
|
||||
//
|
||||
// A Kleisli arrow (A => ReaderIOResult[B]) that:
|
||||
// - Takes an initial state A
|
||||
// - Returns a ReaderIOResult that requires [context.Context]
|
||||
// - Returns a ReaderIOResult that requires context.Context
|
||||
// - Can fail with error or context cancellation
|
||||
// - Produces the final result B after recursion completes
|
||||
//
|
||||
// # Context Cancellation
|
||||
//
|
||||
// Unlike the base [readerioresult.TailRec], this version automatically integrates
|
||||
// Unlike the base readerioresult.TailRec, this version automatically integrates
|
||||
// context cancellation checking:
|
||||
// - Each recursive iteration checks if the context is cancelled
|
||||
// - If cancelled, recursion terminates immediately with a cancellation error
|
||||
@@ -92,9 +92,9 @@ import (
|
||||
//
|
||||
// # Example: Cancellable Countdown
|
||||
//
|
||||
// countdownStep := func(n int) readerioresult.ReaderIOResult[tailrec.Trampoline[int, string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[int, string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[int, string]] {
|
||||
// countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
// return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
// return func() Either[Trampoline[int, string]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](tailrec.Land[int]("Done!"))
|
||||
// }
|
||||
@@ -105,7 +105,7 @@ import (
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// countdown := readerioresult.TailRec(countdownStep)
|
||||
// countdown := TailRec(countdownStep)
|
||||
//
|
||||
// // With cancellation
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
|
||||
@@ -119,9 +119,9 @@ import (
|
||||
// processed []string
|
||||
// }
|
||||
//
|
||||
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// processStep := func(state ProcessState) ReaderIOResult[Trampoline[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) IOEither[Trampoline[ProcessState, []string]] {
|
||||
// return func() Either[Trampoline[ProcessState, []string]] {
|
||||
// if len(state.files) == 0 {
|
||||
// return either.Right[error](tailrec.Land[ProcessState](state.processed))
|
||||
// }
|
||||
@@ -140,7 +140,7 @@ import (
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioresult.TailRec(processStep)
|
||||
// processFiles := TailRec(processStep)
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
//
|
||||
// // Can be cancelled at any point during processing
|
||||
@@ -158,7 +158,7 @@ import (
|
||||
// still respecting context cancellation:
|
||||
//
|
||||
// // Safe for very large inputs with cancellation support
|
||||
// largeCountdown := readerioresult.TailRec(countdownStep)
|
||||
// largeCountdown := TailRec(countdownStep)
|
||||
// ctx := t.Context()
|
||||
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
|
||||
//
|
||||
@@ -171,11 +171,11 @@ import (
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - [readerioresult.TailRec]: Base tail recursion without automatic context checking
|
||||
// - [WithContext]: Context cancellation wrapper used internally
|
||||
// - [Chain]: For sequencing ReaderIOResult computations
|
||||
// - [Ask]: For accessing the context
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
// - readerioresult.TailRec: Base tail recursion without automatic context checking
|
||||
// - WithContext: Context cancellation wrapper used internally
|
||||
// - Chain: For sequencing ReaderIOResult computations
|
||||
// - Ask: For accessing the context
|
||||
// - Left/Right: For creating error/success values
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
|
||||
@@ -30,6 +30,16 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// CustomError is a test error type
|
||||
type CustomError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *CustomError) Error() string {
|
||||
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
// Test basic countdown recursion
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
@@ -432,3 +442,237 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
func TestTailRec_MultipleErrorTypes(t *testing.T) {
|
||||
// Test that different error types are properly handled
|
||||
errorStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
if n == 5 {
|
||||
customErr := &CustomError{Code: 500, Message: "custom error"}
|
||||
return E.Left[Trampoline[int, string]](error(customErr))
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorRecursion := TailRec(errorStep)
|
||||
result := errorRecursion(10)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
customErr, ok := err.(*CustomError)
|
||||
require.True(t, ok, "Expected CustomError type")
|
||||
assert.Equal(t, 500, customErr.Code)
|
||||
assert.Equal(t, "custom error", customErr.Message)
|
||||
}
|
||||
|
||||
func TestTailRec_ContextCancelDuringBounce(t *testing.T) {
|
||||
// Test cancellation happens between bounces, not during computation
|
||||
var iterationCount int32
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
count := atomic.AddInt32(&iterationCount, 1)
|
||||
|
||||
// Cancel after 3 iterations
|
||||
if count == 3 {
|
||||
cancel()
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
result := slowRecursion(10)(ctx)()
|
||||
|
||||
// Should be cancelled after a few iterations
|
||||
assert.True(t, E.IsLeft(result))
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(2))
|
||||
assert.Less(t, iterations, int32(10))
|
||||
}
|
||||
|
||||
func TestTailRec_EmptyState(t *testing.T) {
|
||||
// Test with empty/zero-value state
|
||||
type EmptyState struct{}
|
||||
|
||||
emptyStep := func(state EmptyState) ReaderIOResult[Trampoline[EmptyState, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[EmptyState, int]] {
|
||||
return func() Either[Trampoline[EmptyState, int]] {
|
||||
return E.Right[error](tailrec.Land[EmptyState](42))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emptyRecursion := TailRec(emptyStep)
|
||||
result := emptyRecursion(EmptyState{})(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
}
|
||||
|
||||
func TestTailRec_PointerState(t *testing.T) {
|
||||
// Test with pointer state to ensure proper handling
|
||||
type Node struct {
|
||||
Value int
|
||||
Next *Node
|
||||
}
|
||||
|
||||
// Create a linked list: 1 -> 2 -> 3 -> nil
|
||||
list := &Node{Value: 1, Next: &Node{Value: 2, Next: &Node{Value: 3, Next: nil}}}
|
||||
|
||||
sumStep := func(node *Node) ReaderIOResult[Trampoline[*Node, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[*Node, int]] {
|
||||
return func() Either[Trampoline[*Node, int]] {
|
||||
if node == nil {
|
||||
return E.Right[error](tailrec.Land[*Node](0))
|
||||
}
|
||||
if node.Next == nil {
|
||||
return E.Right[error](tailrec.Land[*Node](node.Value))
|
||||
}
|
||||
// Accumulate value and continue
|
||||
node.Next.Value += node.Value
|
||||
return E.Right[error](tailrec.Bounce[int](node.Next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result := sumList(list)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](6), result) // 1 + 2 + 3 = 6
|
||||
}
|
||||
|
||||
func TestTailRec_ConcurrentCancellation(t *testing.T) {
|
||||
// Test that cancellation works correctly with concurrent operations
|
||||
var iterationCount int32
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Cancel from another goroutine after 50ms
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
result := slowRecursion(20)(ctx)()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Should complete quickly due to cancellation
|
||||
assert.Less(t, elapsed, 100*time.Millisecond)
|
||||
|
||||
// Should have executed some but not all iterations
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(0))
|
||||
assert.Less(t, iterations, int32(20))
|
||||
}
|
||||
|
||||
func TestTailRec_NestedContextValues(t *testing.T) {
|
||||
// Test that nested context values are preserved
|
||||
type contextKey string
|
||||
const (
|
||||
key1 contextKey = "key1"
|
||||
key2 contextKey = "key2"
|
||||
)
|
||||
|
||||
nestedStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
val1 := ctx.Value(key1)
|
||||
val2 := ctx.Value(key2)
|
||||
|
||||
require.NotNil(t, val1)
|
||||
require.NotNil(t, val2)
|
||||
assert.Equal(t, "value1", val1.(string))
|
||||
assert.Equal(t, "value2", val2.(string))
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nestedRecursion := TailRec(nestedStep)
|
||||
|
||||
ctx := context.WithValue(t.Context(), key1, "value1")
|
||||
ctx = context.WithValue(ctx, key2, "value2")
|
||||
|
||||
result := nestedRecursion(3)(ctx)()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
func BenchmarkTailRec_SimpleCountdown(b *testing.B) {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = countdown(1000)(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTailRec_WithCancellation(b *testing.B) {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = countdown(1000)(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
@@ -220,4 +221,10 @@ type (
|
||||
// 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]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/internal/witherable"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Filter[C, HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
|
||||
return witherable.Filter(
|
||||
Map[C],
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
|
||||
return Filter[C](array.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
|
||||
return Filter[C](iter.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMap[C, HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
|
||||
return witherable.FilterMap(
|
||||
Map[C],
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
|
||||
return FilterMap[C](array.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
|
||||
return FilterMap[C](iter.FilterMap[A, B])(p)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -196,6 +197,65 @@ 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.
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -834,7 +834,7 @@ func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endomorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadMapLeft(fa, f)
|
||||
}
|
||||
|
||||
@@ -843,7 +843,7 @@ func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error])
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
|
||||
func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] {
|
||||
return RRIOE.MapLeft[R, context.Context, A](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/traversal/result"
|
||||
@@ -146,9 +147,15 @@ type (
|
||||
// It's an alias for predicate.Predicate[A].
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Endmorphism represents a function from type A to type A.
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endmorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -229,9 +229,10 @@ func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
|
||||
// Example:
|
||||
//
|
||||
// // Add a timeout to a specific operation
|
||||
// withTimeout := statereaderioresult.Local[AppState, Data](
|
||||
// func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 60*time.Second)
|
||||
// withTimeout := statereaderioresult.Local[AppState, Data, context.Context](
|
||||
// func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// },
|
||||
// )
|
||||
// result := withTimeout(computation)
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# fp-go/v2 Reference for Claude Code
|
||||
|
||||
fp-go/v2 (`github.com/IBM/fp-go/v2`) is a typed functional programming library for Go 1.24+. It provides Option, Either/Result, IO, and Effect monads with data-last, curried APIs designed for pipeline composition via `Pipe` and `Flow`. The library follows Haskell/fp-ts conventions adapted for Go generics with explicit arity-numbered functions (e.g., `Pipe3`, `Flow2`). Two module families exist: **standard** (struct-based monads, full FP toolkit) and **idiomatic** (Go-native `(value, error)` tuples, zero-alloc, 2-32x faster).
|
||||
|
||||
## Import Conventions
|
||||
|
||||
| Alias | Package |
|
||||
|-------|---------|
|
||||
| `F` | `github.com/IBM/fp-go/v2/function` |
|
||||
| `O` | `github.com/IBM/fp-go/v2/option` |
|
||||
| `E` | `github.com/IBM/fp-go/v2/either` |
|
||||
| `R` | `github.com/IBM/fp-go/v2/result` |
|
||||
| `A` | `github.com/IBM/fp-go/v2/array` |
|
||||
| `IO` | `github.com/IBM/fp-go/v2/io` |
|
||||
| `IOR` | `github.com/IBM/fp-go/v2/ioresult` |
|
||||
| `IOE` | `github.com/IBM/fp-go/v2/ioeither` |
|
||||
| `RIO` | `github.com/IBM/fp-go/v2/context/readerioresult` |
|
||||
| `EFF` | `github.com/IBM/fp-go/v2/effect` |
|
||||
| `P` | `github.com/IBM/fp-go/v2/pair` |
|
||||
| `T` | `github.com/IBM/fp-go/v2/tuple` |
|
||||
| `N` | `github.com/IBM/fp-go/v2/number` |
|
||||
| `S` | `github.com/IBM/fp-go/v2/string` |
|
||||
| `B` | `github.com/IBM/fp-go/v2/boolean` |
|
||||
| `L` | `github.com/IBM/fp-go/v2/optics/lens` |
|
||||
| `PR` | `github.com/IBM/fp-go/v2/optics/prism` |
|
||||
|
||||
**Idiomatic variants** (tuple-based, zero-alloc):
|
||||
|
||||
| Alias | Package |
|
||||
|-------|---------|
|
||||
| `IR` | `github.com/IBM/fp-go/v2/idiomatic/result` |
|
||||
| `IO_` | `github.com/IBM/fp-go/v2/idiomatic/option` |
|
||||
| `IIR` | `github.com/IBM/fp-go/v2/idiomatic/ioresult` |
|
||||
| `IRR` | `github.com/IBM/fp-go/v2/idiomatic/context/readerresult` |
|
||||
| `IRO` | `github.com/IBM/fp-go/v2/idiomatic/readerioresult` |
|
||||
|
||||
## Monad Selection
|
||||
|
||||
- **Pure value** -- use the value directly, no wrapper needed
|
||||
- **May be absent** -- `Option[A]` (struct-based) or `(A, bool)` (idiomatic)
|
||||
- **Can fail with `error`** -- `Result[A]` = `Either[error, A]`
|
||||
- Need custom error type E -- use `Either[E, A]` instead
|
||||
- **Lazy + can fail** -- `IOResult[A]` = `func() Either[error, A]`
|
||||
- Idiomatic: `func() (A, error)`
|
||||
- **Needs `context.Context` + lazy + can fail** -- `ReaderIOResult[A]` via `context/readerioresult`
|
||||
- Type: `func(context.Context) func() Either[error, A]`
|
||||
- Idiomatic: `func(context.Context) (A, error)` via `idiomatic/context/readerresult`
|
||||
- **Typed DI + context + lazy + can fail** -- `Effect[C, A]` via `effect` package
|
||||
- Type: `func(C) func(context.Context) func() Either[error, A]`
|
||||
- C is your dependency/config struct; context.Context is handled internally
|
||||
- **Performance-critical** -- prefer `idiomatic/` variants throughout
|
||||
|
||||
## Standard vs Idiomatic
|
||||
|
||||
| Aspect | Standard | Idiomatic |
|
||||
|--------|----------|-----------|
|
||||
| Representation | `Either[error, A]` struct | `(A, error)` tuple |
|
||||
| Performance | Baseline | 2-32x faster, zero allocs |
|
||||
| Custom error types | `Either[E, A]` for any E | error only |
|
||||
| Do-notation | Full support | Full support |
|
||||
| FP toolkit | Complete | Complete |
|
||||
| Go interop | Requires `Unwrap`/`Eitherize` | Native `(val, err)` |
|
||||
|
||||
**Rule of thumb**: Use idiomatic for production code and hot paths. Use standard when you need custom error types (`Either[E, A]`) or when composing with packages that use the standard types.
|
||||
|
||||
## Core Types
|
||||
|
||||
```go
|
||||
// function package
|
||||
type Void = struct{}
|
||||
var VOID Void = struct{}{}
|
||||
|
||||
// option
|
||||
type Option[A any] struct { /* Some/None */ }
|
||||
|
||||
// either
|
||||
type Either[E, A any] struct { /* Left/Right */ }
|
||||
|
||||
// result (specialized Either)
|
||||
type Result[A any] = Either[error, A]
|
||||
|
||||
// io
|
||||
type IO[A any] = func() A
|
||||
|
||||
// ioresult
|
||||
type IOResult[A any] = IO[Result[A]] // = func() Either[error, A]
|
||||
|
||||
// context/readerioresult
|
||||
type ReaderIOResult[A any] = func(context.Context) func() Either[error, A]
|
||||
|
||||
// effect
|
||||
type Effect[C, A any] = func(C) func(context.Context) func() Either[error, A]
|
||||
type Kleisli[C, A, B any] = func(A) Effect[C, B]
|
||||
|
||||
// idiomatic equivalents
|
||||
type IOResult[A any] = func() (A, error)
|
||||
type ReaderResult[A any] = func(context.Context) (A, error)
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Data-last**: Configuration/behavior params come first, data comes last. This enables partial application and pipeline composition.
|
||||
|
||||
2. **Type parameter ordering**: Non-inferrable type params come first. Example: `Ap[B, E, A]` -- B cannot be inferred, so it leads. `Map[A, B]` -- both usually inferred.
|
||||
|
||||
3. **Composition direction**:
|
||||
- `F.Flow1/2/3/.../N` -- left-to-right (use this for pipelines)
|
||||
- `Compose` -- right-to-left (mathematical convention; avoid in pipelines)
|
||||
|
||||
4. **Pipe vs Flow**:
|
||||
- `F.Pipe3(value, f1, f2, f3)` -- apply data to a pipeline immediately
|
||||
- `F.Flow3(f1, f2, f3)` -- create a reusable pipeline (returns a function)
|
||||
|
||||
5. **Arity-numbered functions**: `Pipe1` through `Pipe20`, `Flow1` through `Flow20`. Choose the number matching your operation count.
|
||||
|
||||
6. **Naming conventions**:
|
||||
- `Chain` = flatMap/bind (`A -> F[B]`, flattens)
|
||||
- `Map` = fmap (`A -> B`, lifts into context)
|
||||
- `Ap` = applicative apply (apply wrapped function to wrapped value)
|
||||
- `ChainFirst` / `Tap` = execute for side effects, keep original value
|
||||
- `ChainEitherK` = lift pure `func(A) Either[E, B]` into monadic chain
|
||||
- `Of` = pure/return (lift value into monad)
|
||||
- `Fold` = catamorphism (handle both cases)
|
||||
- `Left` / `Right` = Either constructors
|
||||
- `Some` / `None` = Option constructors
|
||||
|
||||
7. **Prefer `result` over `either`** unless you need a custom error type E. `Result[A]` = `Either[error, A]`.
|
||||
|
||||
8. **Wrapping Go functions**:
|
||||
- `result.Eitherize1(fn)` wraps `func(X) (Y, error)` into `func(X) Result[Y]`
|
||||
- `result.Eitherize2(fn)` wraps `func(X, Y) (Z, error)` into `func(X, Y) Result[Z]`
|
||||
- Variants up to `Eitherize15`
|
||||
|
||||
9. **Use `function.Void` / `function.VOID`** instead of `struct{}` / `struct{}{}`.
|
||||
|
||||
10. **Go 1.24+ required** (generic type aliases).
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pipeline with Pipe
|
||||
```go
|
||||
result := F.Pipe3(
|
||||
inputValue,
|
||||
R.Map(transform),
|
||||
R.Chain(validate),
|
||||
R.Fold(onError, onSuccess),
|
||||
)
|
||||
```
|
||||
|
||||
### Reusable pipeline with Flow
|
||||
```go
|
||||
pipeline := F.Flow3(
|
||||
R.Map(normalize),
|
||||
R.Chain(validate),
|
||||
R.Map(format),
|
||||
)
|
||||
output := pipeline(R.Of(input))
|
||||
```
|
||||
|
||||
### Wrapping Go error functions
|
||||
```go
|
||||
safeParseInt := R.Eitherize1(strconv.Atoi)
|
||||
// safeParseInt: func(string) Result[int]
|
||||
result := safeParseInt("42") // Right(42)
|
||||
```
|
||||
|
||||
### Effect with DI
|
||||
```go
|
||||
type Deps struct { DB *sql.DB }
|
||||
|
||||
fetchUser := EFF.Eitherize(func(deps Deps, ctx context.Context) (*User, error) {
|
||||
return deps.DB.QueryRowContext(ctx, "SELECT ...").Scan(...)
|
||||
})
|
||||
// fetchUser: Effect[Deps, *User]
|
||||
|
||||
// Execute:
|
||||
val, err := EFF.RunSync(EFF.Provide[*User](myDeps)(fetchUser))(ctx)
|
||||
```
|
||||
|
||||
### Effect composition
|
||||
```go
|
||||
pipeline := F.Pipe1(
|
||||
fetchUser,
|
||||
EFF.Map[Deps](func(u *User) string { return u.Name }),
|
||||
)
|
||||
```
|
||||
|
||||
### Do-notation (building up state)
|
||||
```go
|
||||
type State struct { X int; Y string }
|
||||
|
||||
result := F.Pipe3(
|
||||
R.Do(State{}),
|
||||
R.Bind(
|
||||
func(x int) func(State) State {
|
||||
return func(s State) State { s.X = x; return s }
|
||||
},
|
||||
func(s State) Result[int] { return R.Of(42) },
|
||||
),
|
||||
R.Let(
|
||||
func(y string) func(State) State {
|
||||
return func(s State) State { s.Y = y; return s }
|
||||
},
|
||||
func(s State) string { return fmt.Sprintf("val=%d", s.X) },
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Optics (Lens)
|
||||
```go
|
||||
type Person struct { Name string; Age int }
|
||||
|
||||
nameLens := L.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
name := nameLens.Get(person) // get
|
||||
updated := nameLens.Set("Bob")(person) // set (returns new Person)
|
||||
modified := L.Modify(strings.ToUpper)(nameLens)(person) // modify
|
||||
```
|
||||
|
||||
### Option handling
|
||||
```go
|
||||
result := F.Pipe3(
|
||||
O.Some(42),
|
||||
O.Map(func(x int) int { return x * 2 }),
|
||||
O.GetOrElse(F.Constant(0)),
|
||||
)
|
||||
```
|
||||
|
||||
### Idiomatic IOResult
|
||||
```go
|
||||
readFile := func() ([]byte, error) { return os.ReadFile("config.json") }
|
||||
// This IS an idiomatic IOResult[[]byte] -- just a func() ([]byte, error)
|
||||
|
||||
parsed := IIR.Map(parseConfig)(readFile)
|
||||
config, err := parsed()
|
||||
```
|
||||
|
||||
### ReaderIOResult (context-dependent IO)
|
||||
```go
|
||||
// Eitherize1 wraps func(context.Context, T0) (R, error) -> func(T0) ReaderIOResult[R]
|
||||
fetchURL := RIO.Eitherize1(func(ctx context.Context, url string) ([]byte, error) {
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil { return nil, err }
|
||||
defer resp.Body.Close()
|
||||
return iolib.ReadAll(resp.Body)
|
||||
})
|
||||
// fetchURL: func(string) ReaderIOResult[[]byte]
|
||||
result := fetchURL("https://example.com")(ctx)() // execute
|
||||
```
|
||||
|
||||
## Deeper Documentation
|
||||
|
||||
- `fp-go-cookbook.md` -- migration recipes and "how do I X in fp-go?"
|
||||
- `fp-go-core-patterns.md` -- core types, operations, and composition details
|
||||
- `fp-go-mastery.md` -- advanced FP techniques, architecture, and Effect system
|
||||
- `fp-go-full-reference.md` -- complete API inventory across all packages
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,106 @@ 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)
|
||||
}
|
||||
|
||||
// Ask returns an Effect that produces the context C as its success value.
|
||||
// This is the fundamental operation of the reader/environment monad,
|
||||
// allowing effects to access their own context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type (also the produced value type)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, C]: An effect that succeeds with its own context value
|
||||
//
|
||||
//go:inline
|
||||
func Ask[C any]() Effect[C, C] {
|
||||
return readerreaderioresult.Ask[C]()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -618,3 +618,379 @@ 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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsk(t *testing.T) {
|
||||
t.Run("returns context as value", func(t *testing.T) {
|
||||
ctx := "my-context"
|
||||
result, err := runEffect(Ask[string](), ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ctx, result)
|
||||
})
|
||||
|
||||
t.Run("works with struct context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
cfg := Config{Host: "localhost", Port: 8080}
|
||||
result, err := runEffect(Ask[Config](), cfg)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cfg, result)
|
||||
})
|
||||
|
||||
t.Run("can be chained with Map to extract a field", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
hostEffect := Map[Config](func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})(Ask[Config]())
|
||||
|
||||
result, err := runEffect(hostEffect, Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "example.com", result)
|
||||
})
|
||||
|
||||
t.Run("can be chained with Chain to produce a derived effect", func(t *testing.T) {
|
||||
type Config struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
derived := Chain(func(cfg Config) Effect[Config, string] {
|
||||
if cfg.APIKey == "" {
|
||||
return Fail[Config, string](assert.AnError)
|
||||
}
|
||||
return Of[Config]("authenticated: " + cfg.APIKey)
|
||||
})(Ask[Config]())
|
||||
|
||||
// Valid key
|
||||
result, err := runEffect(derived, Config{APIKey: "secret"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "authenticated: secret", result)
|
||||
|
||||
// Empty key
|
||||
_, err = runEffect(derived, Config{APIKey: ""})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("is idempotent - multiple calls return same context", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "shared"}
|
||||
|
||||
r1, err1 := runEffect(Ask[TestContext](), ctx)
|
||||
r2, err2 := runEffect(Ask[TestContext](), ctx)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, r1, r2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,6 +204,102 @@ func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
|
||||
return readerreaderioresult.ChainFirst(f)
|
||||
}
|
||||
|
||||
// ChainFirstThunkK chains an effect with a function that returns a Thunk,
|
||||
// but discards the result and returns the original value.
|
||||
// This is useful for performing side effects (like logging or IO operations) that don't
|
||||
// need the effect's context, without changing the value flowing through the computation.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The value type (preserved)
|
||||
// - B: The type produced by the Thunk (discarded)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns Thunk[B] for side effects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, A]: A function that executes the Thunk but preserves the original value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// logToFile := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
// return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
// return func() result.Result[any] {
|
||||
// // Perform IO operation that doesn't need effect context
|
||||
// fmt.Printf("Logging: %d\n", n)
|
||||
// return result.Of[any](nil)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// logged := effect.ChainFirstThunkK[MyContext](logToFile)(eff)
|
||||
// // Prints "Logging: 42" but still produces 42
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ChainThunkK: Chains with a Thunk and uses its result
|
||||
// - TapThunkK: Alias for ChainFirstThunkK
|
||||
// - ChainFirstIOK: Similar but for IO operations
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
ChainFirst[C, A, B],
|
||||
FromThunk[C, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TapThunkK is an alias for ChainFirstThunkK.
|
||||
// It chains an effect with a function that returns a Thunk for side effects,
|
||||
// but preserves the original value. This is useful for logging, debugging, or
|
||||
// performing IO operations that don't need the effect's context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The value type (preserved)
|
||||
// - B: The type produced by the Thunk (discarded)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns Thunk[B] for side effects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, A]: A function that executes the Thunk but preserves the original value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// performSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
// return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
// return func() result.Result[any] {
|
||||
// // Perform context-independent IO operation
|
||||
// log.Printf("Processing value: %d", n)
|
||||
// return result.Of[any](nil)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// tapped := effect.TapThunkK[MyContext](performSideEffect)(eff)
|
||||
// // Logs "Processing value: 42" but still produces 42
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ChainFirstThunkK: The underlying implementation
|
||||
// - TapIOK: Similar but for IO operations
|
||||
// - Tap: Similar but for full effects
|
||||
//
|
||||
//go:inline
|
||||
func TapThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, A] {
|
||||
return ChainFirstThunkK[C](f)
|
||||
}
|
||||
|
||||
// ChainIOK chains an effect with a function that returns an IO action.
|
||||
// This is useful for integrating IO-based computations (synchronous side effects)
|
||||
// into effect chains. The IO action is automatically lifted into the Effect context.
|
||||
@@ -612,3 +708,52 @@ func ChainReaderIOK[C, A, B any](f readerio.Kleisli[C, A, B]) Operator[C, A, B]
|
||||
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
|
||||
// Asks creates an Effect that projects a value from the context using a Reader function.
|
||||
// This is useful for extracting specific fields or computing derived values from the context.
|
||||
// It's essentially a lifted version of the Reader pattern into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type
|
||||
// - A: The type of the projected value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - r: A Reader function that extracts or computes a value from the context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that succeeds with the projected value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Extract a specific field
|
||||
// getHost := effect.Asks[Config](func(cfg Config) string {
|
||||
// return cfg.Host
|
||||
// })
|
||||
//
|
||||
// // Compute a derived value
|
||||
// getURL := effect.Asks[Config](func(cfg Config) string {
|
||||
// return fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)
|
||||
// })
|
||||
//
|
||||
// result, err := runEffect(getHost, Config{Host: "localhost", Port: 8080})
|
||||
// // result == "localhost", err == nil
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// See Also:
|
||||
//
|
||||
// - Ask: Returns the entire context as the value
|
||||
// - Map: Transforms the value after extraction
|
||||
//
|
||||
//go:inline
|
||||
func Asks[C, A any](r Reader[C, A]) Effect[C, A] {
|
||||
return readerreaderioresult.Asks(r)
|
||||
}
|
||||
|
||||
@@ -677,3 +677,992 @@ func TestChainThunkK_Integration(t *testing.T) {
|
||||
assert.Equal(t, result.Of("Value: 100"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_Success(t *testing.T) {
|
||||
t.Run("executes thunk but preserves original value", func(t *testing.T) {
|
||||
sideEffectExecuted := false
|
||||
|
||||
sideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
sideEffectExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](sideEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.True(t, sideEffectExecuted)
|
||||
})
|
||||
|
||||
t.Run("chains multiple side effects", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("log: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
ChainFirstThunkK[TestConfig](logValue),
|
||||
ChainFirstThunkK[TestConfig](logValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "log: 10", log[0])
|
||||
assert.Equal(t, "log: 10", log[1])
|
||||
})
|
||||
|
||||
t.Run("side effect can access runtime context", func(t *testing.T) {
|
||||
var capturedCtx context.Context
|
||||
|
||||
captureContext := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
capturedCtx = ctx
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](captureContext),
|
||||
)
|
||||
outcome := computation(testConfig)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, ctx, capturedCtx)
|
||||
})
|
||||
|
||||
t.Run("side effect result is discarded", func(t *testing.T) {
|
||||
returnDifferentValue := func(n int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) io.IO[result.Result[string]] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of("different value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](returnDifferentValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_Failure(t *testing.T) {
|
||||
t.Run("propagates error from previous effect", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("previous error")
|
||||
sideEffectExecuted := false
|
||||
|
||||
sideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
sideEffectExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
ChainFirstThunkK[TestConfig](sideEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, sideEffectExecuted)
|
||||
})
|
||||
|
||||
t.Run("propagates error from thunk side effect", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("side effect error")
|
||||
|
||||
failingSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
return result.Left[any](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](failingSideEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("stops execution on first error", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("first error")
|
||||
secondEffectExecuted := false
|
||||
|
||||
failingEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
return result.Left[any](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
secondEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
secondEffectExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](failingEffect),
|
||||
ChainFirstThunkK[TestConfig](secondEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, secondEffectExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero value", func(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
countCalls := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
callCount++
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](0),
|
||||
ChainFirstThunkK[TestConfig](countCalls),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
var capturedValue string
|
||||
|
||||
captureValue := func(s string) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
capturedValue = s
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](""),
|
||||
ChainFirstThunkK[TestConfig](captureValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(""), outcome)
|
||||
assert.Equal(t, "", capturedValue)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer", func(t *testing.T) {
|
||||
var capturedPtr *int
|
||||
|
||||
capturePtr := func(ptr *int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
capturedPtr = ptr
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig]((*int)(nil)),
|
||||
ChainFirstThunkK[TestConfig](capturePtr),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of((*int)(nil)), outcome)
|
||||
assert.Nil(t, capturedPtr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_Integration(t *testing.T) {
|
||||
t.Run("composes with Map and Chain", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("value: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe3(
|
||||
Of[TestConfig](5),
|
||||
Map[TestConfig](func(x int) int { return x * 2 }),
|
||||
ChainFirstThunkK[TestConfig](logValue),
|
||||
Map[TestConfig](func(x int) int { return x + 3 }),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(13), outcome) // (5 * 2) + 3
|
||||
assert.Equal(t, 1, len(log))
|
||||
assert.Equal(t, "value: 10", log[0])
|
||||
})
|
||||
|
||||
t.Run("composes with ChainThunkK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("side-effect: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformValue := func(n int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) io.IO[result.Result[string]] {
|
||||
return func() result.Result[string] {
|
||||
log = append(log, fmt.Sprintf("transform: %d", n))
|
||||
return result.Of(fmt.Sprintf("Result: %d", n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](logSideEffect),
|
||||
ChainThunkK[TestConfig](transformValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of("Result: 42"), outcome)
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "side-effect: 42", log[0])
|
||||
assert.Equal(t, "transform: 42", log[1])
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderK and ChainReaderIOK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
addMultiplier := func(n int) reader.Reader[TestConfig, int] {
|
||||
return func(cfg TestConfig) int {
|
||||
return n + cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
logReaderIO := func(n int) readerio.ReaderIO[TestConfig, int] {
|
||||
return func(cfg TestConfig) io.IO[int] {
|
||||
return func() int {
|
||||
log = append(log, fmt.Sprintf("reader-io: %d", n))
|
||||
return n * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logThunk := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("thunk: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe3(
|
||||
Of[TestConfig](5),
|
||||
ChainReaderK(addMultiplier),
|
||||
ChainReaderIOK(logReaderIO),
|
||||
ChainFirstThunkK[TestConfig](logThunk),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(16), outcome) // (5 + 3) * 2
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "reader-io: 8", log[0])
|
||||
assert.Equal(t, "thunk: 16", log[1])
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_Success(t *testing.T) {
|
||||
t.Run("is alias for ChainFirstThunkK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("tapped: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](logValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 1, len(log))
|
||||
assert.Equal(t, "tapped: 42", log[0])
|
||||
})
|
||||
|
||||
t.Run("useful for logging without changing value", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logStep := func(step string) func(int) readerioresult.ReaderIOResult[any] {
|
||||
return func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("%s: %d", step, n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe4(
|
||||
Of[TestConfig](10),
|
||||
TapThunkK[TestConfig](logStep("start")),
|
||||
Map[TestConfig](func(x int) int { return x * 2 }),
|
||||
TapThunkK[TestConfig](logStep("after-map")),
|
||||
Map[TestConfig](func(x int) int { return x + 5 }),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(25), outcome) // (10 * 2) + 5
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "start: 10", log[0])
|
||||
assert.Equal(t, "after-map: 20", log[1])
|
||||
})
|
||||
|
||||
t.Run("can perform IO operations", func(t *testing.T) {
|
||||
var ioExecuted bool
|
||||
|
||||
performIO := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
// Simulate IO operation
|
||||
ioExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](performIO),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_Failure(t *testing.T) {
|
||||
t.Run("propagates error from previous effect", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("previous error")
|
||||
tapExecuted := false
|
||||
|
||||
tapValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
tapExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
TapThunkK[TestConfig](tapValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, tapExecuted)
|
||||
})
|
||||
|
||||
t.Run("propagates error from tap operation", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("tap error")
|
||||
|
||||
failingTap := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
return result.Left[any](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](failingTap),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_EdgeCases(t *testing.T) {
|
||||
t.Run("handles multiple taps in sequence", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
tap1 := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, "tap1")
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tap2 := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, "tap2")
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tap3 := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, "tap3")
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe3(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](tap1),
|
||||
TapThunkK[TestConfig](tap2),
|
||||
TapThunkK[TestConfig](tap3),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, []string{"tap1", "tap2", "tap3"}, log)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_Integration(t *testing.T) {
|
||||
t.Run("real-world logging scenario", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logStart := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("Starting computation with: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logIntermediate := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("Intermediate result: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logFinal := func(s string) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("Final result: %s", s))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe5(
|
||||
Of[TestConfig](10),
|
||||
TapThunkK[TestConfig](logStart),
|
||||
Map[TestConfig](func(x int) int { return x * 3 }),
|
||||
TapThunkK[TestConfig](logIntermediate),
|
||||
Map[TestConfig](func(x int) string { return fmt.Sprintf("Value: %d", x) }),
|
||||
TapThunkK[TestConfig](logFinal),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of("Value: 30"), outcome)
|
||||
assert.Equal(t, 3, len(log))
|
||||
assert.Equal(t, "Starting computation with: 10", log[0])
|
||||
assert.Equal(t, "Intermediate result: 30", log[1])
|
||||
assert.Equal(t, "Final result: Value: 30", log[2])
|
||||
})
|
||||
|
||||
t.Run("composes with FromThunk", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
thunk := func(ctx context.Context) io.IO[result.Result[int]] {
|
||||
return func() result.Result[int] {
|
||||
return result.Of(100)
|
||||
}
|
||||
}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("value: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
FromThunk[TestConfig](thunk),
|
||||
TapThunkK[TestConfig](logValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(100), outcome)
|
||||
assert.Equal(t, 1, len(log))
|
||||
assert.Equal(t, "value: 100", log[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Success(t *testing.T) {
|
||||
t.Run("extracts a field from context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
getHost := Asks(func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getHost, Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "localhost", result)
|
||||
})
|
||||
|
||||
t.Run("extracts multiple fields and computes derived value", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
getURL := Asks(func(cfg Config) string {
|
||||
return fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)
|
||||
})
|
||||
|
||||
result, err := runEffect(getURL, Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "http://example.com:443", result)
|
||||
})
|
||||
|
||||
t.Run("extracts numeric field", func(t *testing.T) {
|
||||
getPort := Asks(func(cfg TestConfig) int {
|
||||
return cfg.Multiplier
|
||||
})
|
||||
|
||||
result, err := runEffect(getPort, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, result)
|
||||
})
|
||||
|
||||
t.Run("computes value from context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
getArea := Asks(func(cfg Config) int {
|
||||
return cfg.Width * cfg.Height
|
||||
})
|
||||
|
||||
result, err := runEffect(getArea, Config{Width: 10, Height: 20})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, result)
|
||||
})
|
||||
|
||||
t.Run("transforms string field", func(t *testing.T) {
|
||||
getUpperPrefix := Asks(func(cfg TestConfig) string {
|
||||
return fmt.Sprintf("[%s]", cfg.Prefix)
|
||||
})
|
||||
|
||||
result, err := runEffect(getUpperPrefix, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[LOG]", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
getValue := Asks(func(cfg Config) int {
|
||||
return cfg.Value
|
||||
})
|
||||
|
||||
result, err := runEffect(getValue, Config{Value: 0})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
getName := Asks(func(cfg Config) string {
|
||||
return cfg.Name
|
||||
})
|
||||
|
||||
result, err := runEffect(getName, Config{Name: ""})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer fields", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Data *string
|
||||
}
|
||||
|
||||
hasData := Asks(func(cfg Config) bool {
|
||||
return cfg.Data != nil
|
||||
})
|
||||
|
||||
result, err := runEffect(hasData, Config{Data: nil})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("handles complex nested structures", func(t *testing.T) {
|
||||
type Database struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
type Config struct {
|
||||
DB Database
|
||||
}
|
||||
|
||||
getDBHost := Asks(func(cfg Config) string {
|
||||
return cfg.DB.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getDBHost, Config{
|
||||
DB: Database{Host: "db.example.com", Port: 5432},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "db.example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg Config) int {
|
||||
return cfg.Value
|
||||
}),
|
||||
Map[Config](func(x int) int { return x * 2 }),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Value: 21})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg Config) int {
|
||||
return cfg.Multiplier
|
||||
}),
|
||||
Chain(func(mult int) Effect[Config, int] {
|
||||
return Of[Config](mult * 10)
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Multiplier: 5})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 50, result)
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderK", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg TestConfig) int {
|
||||
return cfg.Multiplier
|
||||
}),
|
||||
ChainReaderK(func(mult int) reader.Reader[TestConfig, int] {
|
||||
return func(cfg TestConfig) int {
|
||||
return mult + len(cfg.Prefix)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, result) // 3 + len("LOG")
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderIOK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg TestConfig) string {
|
||||
return cfg.Prefix
|
||||
}),
|
||||
ChainReaderIOK(func(prefix string) readerio.ReaderIO[TestConfig, string] {
|
||||
return func(cfg TestConfig) io.IO[string] {
|
||||
return func() string {
|
||||
log = append(log, "executed")
|
||||
return fmt.Sprintf("%s:%d", prefix, cfg.Multiplier)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG:3", result)
|
||||
assert.Equal(t, 1, len(log))
|
||||
})
|
||||
|
||||
t.Run("multiple Asks in sequence", func(t *testing.T) {
|
||||
type Config struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Asks(func(cfg Config) string {
|
||||
return cfg.First
|
||||
}),
|
||||
Chain(func(_ string) Effect[Config, string] {
|
||||
return Asks(func(cfg Config) string {
|
||||
return cfg.Second
|
||||
})
|
||||
}),
|
||||
Map[Config](func(s string) string {
|
||||
return "Result: " + s
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{First: "A", Second: "B"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Result: B", result)
|
||||
})
|
||||
|
||||
t.Run("Asks combined with Ask", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Ask[Config](),
|
||||
Chain(func(cfg Config) Effect[Config, int] {
|
||||
return Asks(func(c Config) int {
|
||||
return c.Value * 2
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Value: 15})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Comparison(t *testing.T) {
|
||||
t.Run("Asks vs Ask with Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
// Using Asks
|
||||
asksVersion := Asks(func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
|
||||
// Using Ask + Map
|
||||
askMapVersion := F.Pipe1(
|
||||
Ask[Config](),
|
||||
Map[Config](func(cfg Config) int {
|
||||
return cfg.Port
|
||||
}),
|
||||
)
|
||||
|
||||
cfg := Config{Port: 8080}
|
||||
|
||||
result1, err1 := runEffect(asksVersion, cfg)
|
||||
result2, err2 := runEffect(askMapVersion, cfg)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, 8080, result1)
|
||||
})
|
||||
|
||||
t.Run("Asks is more concise than Ask + Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Asks is more direct for field extraction
|
||||
getHost := Asks(func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getHost, Config{Host: "api.example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "api.example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_RealWorldScenarios(t *testing.T) {
|
||||
t.Run("extract database connection string", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Database string
|
||||
User string
|
||||
}
|
||||
|
||||
getConnectionString := Asks(func(cfg DatabaseConfig) string {
|
||||
return fmt.Sprintf("postgres://%s@%s:%d/%s",
|
||||
cfg.User, cfg.Host, cfg.Port, cfg.Database)
|
||||
})
|
||||
|
||||
result, err := runEffect(getConnectionString, DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Database: "myapp",
|
||||
User: "admin",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://admin@localhost:5432/myapp", result)
|
||||
})
|
||||
|
||||
t.Run("compute API endpoint from config", func(t *testing.T) {
|
||||
type APIConfig struct {
|
||||
Protocol string
|
||||
Host string
|
||||
Port int
|
||||
BasePath string
|
||||
}
|
||||
|
||||
getEndpoint := Asks(func(cfg APIConfig) string {
|
||||
return fmt.Sprintf("%s://%s:%d%s",
|
||||
cfg.Protocol, cfg.Host, cfg.Port, cfg.BasePath)
|
||||
})
|
||||
|
||||
result, err := runEffect(getEndpoint, APIConfig{
|
||||
Protocol: "https",
|
||||
Host: "api.example.com",
|
||||
Port: 443,
|
||||
BasePath: "/v1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://api.example.com:443/v1", result)
|
||||
})
|
||||
|
||||
t.Run("validate configuration", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Timeout int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
isValid := Asks(func(cfg Config) bool {
|
||||
return cfg.Timeout > 0 && cfg.MaxRetries >= 0
|
||||
})
|
||||
|
||||
// Valid config
|
||||
result1, err1 := runEffect(isValid, Config{Timeout: 30, MaxRetries: 3})
|
||||
assert.NoError(t, err1)
|
||||
assert.True(t, result1)
|
||||
|
||||
// Invalid config
|
||||
result2, err2 := runEffect(isValid, Config{Timeout: 0, MaxRetries: 3})
|
||||
assert.NoError(t, err2)
|
||||
assert.False(t, result2)
|
||||
})
|
||||
|
||||
t.Run("extract feature flags", func(t *testing.T) {
|
||||
type FeatureFlags struct {
|
||||
EnableNewUI bool
|
||||
EnableBetaAPI bool
|
||||
EnableAnalytics bool
|
||||
}
|
||||
|
||||
hasNewUI := Asks[FeatureFlags](func(flags FeatureFlags) bool {
|
||||
return flags.EnableNewUI
|
||||
})
|
||||
|
||||
result, err := runEffect(hasNewUI, FeatureFlags{
|
||||
EnableNewUI: true,
|
||||
EnableBetaAPI: false,
|
||||
EnableAnalytics: true,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Filter lifts a filtering operation on a higher-kinded type into an Effect operator.
|
||||
// This is a generic function that works with any filterable data structure by taking
|
||||
// a filter function and returning an operator that can be used in effect chains.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - HKTA: The higher-kinded type being filtered (e.g., []A, Seq[A])
|
||||
// - A: The element type being filtered
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - filter: A function that takes a predicate and returns an endomorphism on HKTA
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Predicate[A]) Operator[C, HKTA, HKTA]: A function that takes a predicate
|
||||
// and returns an operator that filters effects containing HKTA values
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import A "github.com/IBM/fp-go/v2/array"
|
||||
//
|
||||
// // Create a custom filter operator for arrays
|
||||
// filterOp := Filter[MyContext](A.Filter[int])
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
|
||||
// filterOp(isEven),
|
||||
// Map[MyContext](func(arr []int) int { return len(arr) }),
|
||||
// )
|
||||
// // Result: Effect that produces 2 (count of even numbers)
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterArray: Specialized version for array filtering
|
||||
// - FilterIter: Specialized version for iterator filtering
|
||||
// - FilterMap: For filtering and mapping simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func Filter[C, HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
|
||||
return readerreaderioresult.Filter[C](filter)
|
||||
}
|
||||
|
||||
// FilterArray creates an operator that filters array elements within an Effect based on a predicate.
|
||||
// Elements that satisfy the predicate are kept, while others are removed.
|
||||
// This is a specialized version of Filter for arrays.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The element type in the array
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A predicate function that tests each element
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, []A, []A]: An operator that filters array elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// filterPositive := FilterArray[MyContext](isPositive)
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]int{-2, -1, 0, 1, 2, 3}),
|
||||
// filterPositive,
|
||||
// )
|
||||
// // Result: Effect that produces []int{1, 2, 3}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Filter: Generic version for any filterable type
|
||||
// - FilterIter: For filtering iterators
|
||||
// - FilterMapArray: For filtering and mapping arrays simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
|
||||
return readerreaderioresult.FilterArray[C](p)
|
||||
}
|
||||
|
||||
// FilterIter creates an operator that filters iterator elements within an Effect based on a predicate.
|
||||
// Elements that satisfy the predicate are kept in the resulting iterator, while others are removed.
|
||||
// This is a specialized version of Filter for iterators (Seq).
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The element type in the iterator
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A predicate function that tests each element
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, Seq[A], Seq[A]]: An operator that filters iterator elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// filterEven := FilterIter[MyContext](isEven)
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext](slices.Values([]int{1, 2, 3, 4, 5, 6})),
|
||||
// filterEven,
|
||||
// )
|
||||
// // Result: Effect that produces an iterator over [2, 4, 6]
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Filter: Generic version for any filterable type
|
||||
// - FilterArray: For filtering arrays
|
||||
// - FilterMapIter: For filtering and mapping iterators simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
|
||||
return readerreaderioresult.FilterIter[C](p)
|
||||
}
|
||||
|
||||
// FilterMap lifts a filter-map operation on a higher-kinded type into an Effect operator.
|
||||
// This combines filtering and mapping in a single operation: elements are transformed
|
||||
// using a function that returns Option, and only Some values are kept in the result.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - HKTA: The input higher-kinded type (e.g., []A, Seq[A])
|
||||
// - HKTB: The output higher-kinded type (e.g., []B, Seq[B])
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - filter: A function that takes an option.Kleisli and returns a transformation from HKTA to HKTB
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB]: A function that takes a Kleisli arrow
|
||||
// and returns an operator that filter-maps effects
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import A "github.com/IBM/fp-go/v2/array"
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Parse and filter positive integers
|
||||
// parsePositive := func(s string) O.Option[int] {
|
||||
// var n int
|
||||
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n > 0 {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// filterMapOp := FilterMap[MyContext](A.FilterMap[string, int])
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]string{"1", "-2", "3", "invalid", "5"}),
|
||||
// filterMapOp(parsePositive),
|
||||
// )
|
||||
// // Result: Effect that produces []int{1, 3, 5}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMapArray: Specialized version for arrays
|
||||
// - FilterMapIter: Specialized version for iterators
|
||||
// - Filter: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMap[C, HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
|
||||
return readerreaderioresult.FilterMap[C](filter)
|
||||
}
|
||||
|
||||
// FilterMapArray creates an operator that filters and maps array elements within an Effect.
|
||||
// Each element is transformed using a function that returns Option[B]. Elements that
|
||||
// produce Some(b) are kept in the result array, while None values are filtered out.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, []A, []B]: An operator that filter-maps array elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Double even numbers, filter out odd numbers
|
||||
// doubleEven := func(n int) O.Option[int] {
|
||||
// if n%2 == 0 {
|
||||
// return O.Some(n * 2)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
|
||||
// FilterMapArray[MyContext](doubleEven),
|
||||
// )
|
||||
// // Result: Effect that produces []int{4, 8}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMap: Generic version for any filterable type
|
||||
// - FilterMapIter: For filter-mapping iterators
|
||||
// - FilterArray: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
|
||||
return readerreaderioresult.FilterMapArray[C](p)
|
||||
}
|
||||
|
||||
// FilterMapIter creates an operator that filters and maps iterator elements within an Effect.
|
||||
// Each element is transformed using a function that returns Option[B]. Elements that
|
||||
// produce Some(b) are kept in the resulting iterator, while None values are filtered out.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, Seq[A], Seq[B]]: An operator that filter-maps iterator elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Parse strings to integers, keeping only valid ones
|
||||
// parseInt := func(s string) O.Option[int] {
|
||||
// var n int
|
||||
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext](slices.Values([]string{"1", "2", "invalid", "3"})),
|
||||
// FilterMapIter[MyContext](parseInt),
|
||||
// )
|
||||
// // Result: Effect that produces an iterator over [1, 2, 3]
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMap: Generic version for any filterable type
|
||||
// - FilterMapArray: For filter-mapping arrays
|
||||
// - FilterIter: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
|
||||
return readerreaderioresult.FilterMapIter[C](p)
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type FilterTestConfig struct {
|
||||
MaxValue int
|
||||
MinValue int
|
||||
}
|
||||
|
||||
// Helper to collect iterator results from an effect
|
||||
func collectSeqEffect[C, A any](eff Effect[C, Seq[A]], cfg C) []A {
|
||||
result, err := runEffect(eff, cfg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return slices.Collect(result)
|
||||
}
|
||||
|
||||
func TestFilterArray_Success(t *testing.T) {
|
||||
t.Run("filters array keeping matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig]([]int{1, -2, 3, -4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 3, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty array when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
isNegative := N.LessThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isNegative)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("returns all elements when all match", func(t *testing.T) {
|
||||
// Arrange
|
||||
alwaysTrue := func(n int) bool { return true }
|
||||
filterOp := FilterArray[FilterTestConfig](alwaysTrue)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterIter_Success(t *testing.T) {
|
||||
t.Run("filters iterator keeping matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterOp := FilterIter[FilterTestConfig](isEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5, 6}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{2, 4, 6}, collected)
|
||||
})
|
||||
|
||||
t.Run("returns empty iterator when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
isNegative := N.LessThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isNegative)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterArray_WithContext(t *testing.T) {
|
||||
t.Run("uses context for filtering", func(t *testing.T) {
|
||||
// Arrange
|
||||
cfg := FilterTestConfig{MaxValue: 100, MinValue: 0}
|
||||
inRange := func(n int) bool { return n >= cfg.MinValue && n <= cfg.MaxValue }
|
||||
filterOp := FilterArray[FilterTestConfig](inRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-10, 50, 150, 75})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), cfg)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{50, 75}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterArray_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig]([]int{})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, []int](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterIter_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isPositive)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, Seq[int]](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_GenericFilter(t *testing.T) {
|
||||
t.Run("works with custom filter function", func(t *testing.T) {
|
||||
// Arrange
|
||||
customFilter := func(p Predicate[int]) Endomorphism[[]int] {
|
||||
return A.Filter(p)
|
||||
}
|
||||
filterOp := Filter[FilterTestConfig](customFilter)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(isEven)(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_Success(t *testing.T) {
|
||||
t.Run("filters and maps array elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
parsePositive := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("positive:%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](parsePositive)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"positive:2", "positive:4", "positive:5"}, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
neverMatch := func(n int) O.Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](neverMatch)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("maps all elements when all match", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapIter_Success(t *testing.T) {
|
||||
t.Run("filters and maps iterator elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](doubleEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{4, 8}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_TypeConversion(t *testing.T) {
|
||||
t.Run("converts int to string", func(t *testing.T) {
|
||||
// Arrange
|
||||
intToString := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](intToString)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"2", "4"}, result)
|
||||
})
|
||||
|
||||
t.Run("converts string to int", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseEven := func(s string) O.Option[int] {
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n%2 == 0 {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](parseEven)
|
||||
input := Succeed[FilterTestConfig]([]string{"1", "2", "3", "4", "invalid"})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig]([]int{})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, []int](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapIter_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMap_GenericFilterMap(t *testing.T) {
|
||||
t.Run("works with custom filterMap function", func(t *testing.T) {
|
||||
// Arrange
|
||||
customFilterMap := func(f O.Kleisli[int, string]) Reader[[]int, []string] {
|
||||
return A.FilterMap(f)
|
||||
}
|
||||
filterMapOp := FilterMap[FilterTestConfig](customFilterMap)
|
||||
intToString := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(intToString)(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"2", "4"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Composition(t *testing.T) {
|
||||
t.Run("chains multiple filters", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterPositive := FilterArray[FilterTestConfig](isPositive)
|
||||
filterEven := FilterArray[FilterTestConfig](isEven)
|
||||
input := Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterEven(filterPositive(input)), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("chains filter and filterMap", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](doubleEven)
|
||||
input := Succeed[FilterTestConfig]([]int{-2, 1, 2, 3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(filterOp(input)), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{4, 8}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_WithComplexTypes(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("filters structs", func(t *testing.T) {
|
||||
// Arrange
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
filterOp := FilterArray[FilterTestConfig](isAdult)
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Bob", Age: 16},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
input := Succeed[FilterTestConfig](users)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
expected := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("filterMaps structs to different type", func(t *testing.T) {
|
||||
// Arrange
|
||||
extractAdultName := func(u User) O.Option[string] {
|
||||
if u.Age >= 18 {
|
||||
return O.Some(u.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](extractAdultName)
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Bob", Age: 16},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
input := Succeed[FilterTestConfig](users)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"Alice", "Charlie"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_BoundaryConditions(t *testing.T) {
|
||||
t.Run("filters with boundary predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
inRange := func(n int) bool { return n >= 0 && n <= 100 }
|
||||
filterOp := FilterArray[FilterTestConfig](inRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 50, 100}, result)
|
||||
})
|
||||
|
||||
t.Run("filterMap with boundary conditions", func(t *testing.T) {
|
||||
// Arrange
|
||||
clampToRange := func(n int) O.Option[int] {
|
||||
if n >= 0 && n <= 100 {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](clampToRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 50, 100}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_WithIterators(t *testing.T) {
|
||||
t.Run("filters large iterator efficiently", func(t *testing.T) {
|
||||
// Arrange
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterOp := FilterIter[FilterTestConfig](isEven)
|
||||
|
||||
// Create iterator for range 0-99
|
||||
makeSeq := func(yield func(int) bool) {
|
||||
for i := range 100 {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
input := Succeed[FilterTestConfig](Seq[int](makeSeq))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, 50, len(collected))
|
||||
assert.Equal(t, 0, collected[0])
|
||||
assert.Equal(t, 98, collected[49])
|
||||
})
|
||||
|
||||
t.Run("filterMap with iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
squareEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](squareEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{4, 16}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_ErrorPropagation(t *testing.T) {
|
||||
t.Run("filter propagates Left through chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
originalErr := errors.New("original error")
|
||||
|
||||
// Create an effect that fails
|
||||
failedEffect := F.Pipe1(
|
||||
Succeed[FilterTestConfig]([]int{1, 2, 3}),
|
||||
Chain(func([]int) Effect[FilterTestConfig, []int] {
|
||||
return Fail[FilterTestConfig, []int](originalErr)
|
||||
}),
|
||||
)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(failedEffect), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
})
|
||||
|
||||
t.Run("filterMap propagates Left through chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
originalErr := errors.New("original error")
|
||||
|
||||
// Create an effect that fails
|
||||
failedEffect := F.Pipe1(
|
||||
Succeed[FilterTestConfig]([]int{1, 2, 3}),
|
||||
Chain(func([]int) Effect[FilterTestConfig, []int] {
|
||||
return Fail[FilterTestConfig, []int](originalErr)
|
||||
}),
|
||||
)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterMapOp(failedEffect), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Integration(t *testing.T) {
|
||||
t.Run("complex filtering pipeline", func(t *testing.T) {
|
||||
// Arrange: Filter positive numbers, then double evens, then filter > 5
|
||||
isPositive := N.MoreThan(0)
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
isGreaterThan5 := N.MoreThan(5)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6}),
|
||||
FilterArray[FilterTestConfig](isPositive),
|
||||
FilterMapArray[FilterTestConfig](doubleEven),
|
||||
FilterArray[FilterTestConfig](isGreaterThan5),
|
||||
)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(pipeline, FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
// Positive: [1,2,3,4,5,6] -> DoubleEven: [4,8,12] -> >5: [8,12]
|
||||
assert.Equal(t, []int{8, 12}, result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of an Effect.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the Effect (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// Promap is particularly useful for adapting effects to work with different context types
|
||||
// while simultaneously transforming their output values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - E: The original context type expected by the Effect
|
||||
// - A: The original success type produced by the Effect
|
||||
// - D: The new input context type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: Function to transform the input context from D to E (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Kleisli arrow that takes an Effect[E, A] and returns a function from D to B
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// APIKey string
|
||||
// }
|
||||
//
|
||||
// type DBConfig struct {
|
||||
// URL string
|
||||
// }
|
||||
//
|
||||
// // Effect that uses DBConfig and returns an int
|
||||
// getUserCount := func(cfg DBConfig) effect.Effect[context.Context, int] {
|
||||
// return effect.Succeed[context.Context](42)
|
||||
// }
|
||||
//
|
||||
// // Transform AppConfig to DBConfig
|
||||
// extractDBConfig := func(app AppConfig) DBConfig {
|
||||
// return DBConfig{URL: app.DatabaseURL}
|
||||
// }
|
||||
//
|
||||
// // Transform int to string
|
||||
// formatCount := func(count int) string {
|
||||
// return fmt.Sprintf("Users: %d", count)
|
||||
// }
|
||||
//
|
||||
// // Adapt the effect to work with AppConfig and return string
|
||||
// adapted := effect.Promap(extractDBConfig, formatCount)(getUserCount)
|
||||
// result := adapted(AppConfig{DatabaseURL: "localhost:5432", APIKey: "secret"})
|
||||
//
|
||||
//go:inline
|
||||
func Promap[E, A, D, B any](f Reader[D, E], g Reader[A, B]) Kleisli[D, Effect[E, A], B] {
|
||||
return F.Flow2(
|
||||
Local[A](f),
|
||||
Map[D](g),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for profunctor tests
|
||||
type AppConfig struct {
|
||||
DatabaseURL string
|
||||
APIKey string
|
||||
Port int
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
// Effect that uses DBConfig and returns an int
|
||||
getUserCount := Succeed[DBConfig](42)
|
||||
|
||||
// Transform AppConfig to DBConfig
|
||||
extractDBConfig := func(app AppConfig) DBConfig {
|
||||
return DBConfig{URL: app.DatabaseURL}
|
||||
}
|
||||
|
||||
// Transform int to string
|
||||
formatCount := func(count int) string {
|
||||
return fmt.Sprintf("Users: %d", count)
|
||||
}
|
||||
|
||||
// Adapt the effect to work with AppConfig and return string
|
||||
adapted := Promap(extractDBConfig, formatCount)(getUserCount)
|
||||
result := adapted(AppConfig{
|
||||
DatabaseURL: "localhost:5432",
|
||||
APIKey: "secret",
|
||||
Port: 8080,
|
||||
})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Users: 42"), result)
|
||||
})
|
||||
|
||||
t.Run("identity transformations", func(t *testing.T) {
|
||||
// Effect that returns a value
|
||||
getValue := Succeed[DBConfig](100)
|
||||
|
||||
// Identity transformations
|
||||
identity := func(x DBConfig) DBConfig { return x }
|
||||
identityInt := func(x int) int { return x }
|
||||
|
||||
// Apply identity transformations
|
||||
adapted := Promap(identity, identityInt)(getValue)
|
||||
result := adapted(DBConfig{URL: "localhost"})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapComposition tests that Promap composes correctly
|
||||
func TestPromapComposition(t *testing.T) {
|
||||
t.Run("compose multiple transformations", func(t *testing.T) {
|
||||
// Effect that uses ServerConfig and returns the port
|
||||
getPort := Map[ServerConfig](func(cfg ServerConfig) int {
|
||||
return cfg.Port
|
||||
})(Ask[ServerConfig]())
|
||||
|
||||
// First transformation: AppConfig -> ServerConfig
|
||||
extractServerConfig := func(app AppConfig) ServerConfig {
|
||||
return ServerConfig{Host: "localhost", Port: app.Port}
|
||||
}
|
||||
|
||||
// Second transformation: int -> string
|
||||
formatPort := func(port int) string {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(extractServerConfig, formatPort)(getPort)
|
||||
result := adapted(AppConfig{
|
||||
DatabaseURL: "db.example.com",
|
||||
APIKey: "key123",
|
||||
Port: 9000,
|
||||
})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(":9000"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithErrors tests Promap with effects that can fail
|
||||
func TestPromapWithErrors(t *testing.T) {
|
||||
t.Run("propagates errors correctly", func(t *testing.T) {
|
||||
// Effect that fails
|
||||
failingEffect := Fail[DBConfig, int](fmt.Errorf("database connection failed"))
|
||||
|
||||
// Transformations
|
||||
extractDBConfig := func(app AppConfig) DBConfig {
|
||||
return DBConfig{URL: app.DatabaseURL}
|
||||
}
|
||||
formatCount := func(count int) string {
|
||||
return fmt.Sprintf("Count: %d", count)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(extractDBConfig, formatCount)(failingEffect)
|
||||
result := adapted(AppConfig{DatabaseURL: "localhost"})(context.Background())()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
err := R.MonadFold(result,
|
||||
func(e error) error { return e },
|
||||
func(string) error { return nil },
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "database connection failed")
|
||||
})
|
||||
|
||||
t.Run("output transformation not applied on error", func(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
// Effect that fails
|
||||
failingEffect := Fail[DBConfig, int](fmt.Errorf("error"))
|
||||
|
||||
// Transformation that counts calls
|
||||
countingTransform := func(x int) string {
|
||||
callCount++
|
||||
return strconv.Itoa(x)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(
|
||||
func(app AppConfig) DBConfig { return DBConfig{URL: app.DatabaseURL} },
|
||||
countingTransform,
|
||||
)(failingEffect)
|
||||
result := adapted(AppConfig{DatabaseURL: "localhost"})(context.Background())()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
assert.Equal(t, 0, callCount, "output transformation should not be called on error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithComplexTypes tests Promap with more complex type transformations
|
||||
func TestPromapWithComplexTypes(t *testing.T) {
|
||||
t.Run("transform struct to different struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
type UserDTO struct {
|
||||
UserID int
|
||||
FullName string
|
||||
}
|
||||
|
||||
// Effect that uses User and returns a string
|
||||
getUserInfo := Map[User](func(user User) string {
|
||||
return fmt.Sprintf("User %s (ID: %d)", user.Name, user.ID)
|
||||
})(Ask[User]())
|
||||
|
||||
// Transform UserDTO to User
|
||||
dtoToUser := func(dto UserDTO) User {
|
||||
return User{ID: dto.UserID, Name: dto.FullName}
|
||||
}
|
||||
|
||||
// Transform string to uppercase
|
||||
toUpper := func(s string) string {
|
||||
return fmt.Sprintf("INFO: %s", s)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(dtoToUser, toUpper)(getUserInfo)
|
||||
result := adapted(UserDTO{UserID: 123, FullName: "Alice"})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("INFO: User Alice (ID: 123)"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapChaining tests chaining multiple Promap operations
|
||||
func TestPromapChaining(t *testing.T) {
|
||||
t.Run("chain multiple Promap operations", func(t *testing.T) {
|
||||
// Base effect that doubles the input
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
// First Promap: string -> int, int -> string
|
||||
step1 := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(baseEffect)
|
||||
|
||||
// Second Promap: float64 -> string, string -> float64
|
||||
step2 := Promap(
|
||||
func(f float64) string {
|
||||
return fmt.Sprintf("%.0f", f)
|
||||
},
|
||||
func(s string) float64 {
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
},
|
||||
)(step1)
|
||||
|
||||
result := step2(21.0)(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(42.0), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapEdgeCases tests edge cases
|
||||
func TestPromapEdgeCases(t *testing.T) {
|
||||
t.Run("zero values", func(t *testing.T) {
|
||||
effect := Map[int](func(x int) int {
|
||||
return x
|
||||
})(Ask[int]())
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int { return 0 },
|
||||
func(x int) string { return "" },
|
||||
)(effect)
|
||||
|
||||
result := adapted("anything")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(""), result)
|
||||
})
|
||||
|
||||
t.Run("nil context handling", func(t *testing.T) {
|
||||
effect := Succeed[int]("success")
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int { return 42 },
|
||||
func(s string) string { return s + "!" },
|
||||
)(effect)
|
||||
|
||||
// Using background context instead of nil
|
||||
result := adapted("test")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("success!"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapIntegration tests integration with other effect operations
|
||||
func TestPromapIntegration(t *testing.T) {
|
||||
t.Run("Promap with Map", func(t *testing.T) {
|
||||
// Base effect that adds 10
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x + 10
|
||||
})(Ask[int]())
|
||||
|
||||
// Apply Promap
|
||||
promapped := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
func(x int) int { return x * 2 },
|
||||
)(baseEffect)
|
||||
|
||||
// Apply Map on top
|
||||
mapped := Map[string](func(x int) string {
|
||||
return fmt.Sprintf("Result: %d", x)
|
||||
})(promapped)
|
||||
|
||||
result := mapped("5")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Result: 30"), result)
|
||||
})
|
||||
|
||||
t.Run("Promap with Chain", func(t *testing.T) {
|
||||
// Base effect
|
||||
baseEffect := Ask[int]()
|
||||
|
||||
// Apply Promap
|
||||
promapped := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
func(x int) int { return x * 2 },
|
||||
)(baseEffect)
|
||||
|
||||
// Chain with another effect
|
||||
chained := Chain(func(x int) Effect[string, string] {
|
||||
return Succeed[string](fmt.Sprintf("Value: %d", x))
|
||||
})(promapped)
|
||||
|
||||
result := chained("10")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Value: 20"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkPromap benchmarks the Promap operation
|
||||
func BenchmarkPromap(b *testing.B) {
|
||||
effect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(effect)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = adapted("42")(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPromapChained benchmarks chained Promap operations
|
||||
func BenchmarkPromapChained(b *testing.B) {
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
step1 := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(baseEffect)
|
||||
|
||||
step2 := Promap(
|
||||
func(f float64) string {
|
||||
return fmt.Sprintf("%.0f", f)
|
||||
},
|
||||
func(s string) float64 {
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
},
|
||||
)(step1)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = step2(21.0)(ctx)()
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,11 @@ import (
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
@@ -89,4 +91,14 @@ type (
|
||||
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
|
||||
// It's used for lifting operations over effects.
|
||||
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
|
||||
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
)
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RA "github.com/IBM/fp-go/v2/internal/array"
|
||||
)
|
||||
@@ -178,3 +181,92 @@ func CompactArrayG[A1 ~[]Either[E, A], A2 ~[]A, E, A any](fa A1) A2 {
|
||||
func CompactArray[E, A any](fa []Either[E, A]) []A {
|
||||
return CompactArrayG[[]Either[E, A], []A](fa)
|
||||
}
|
||||
|
||||
// TraverseSeq transforms an iterator by applying a function that returns an Either to each element.
|
||||
// If any element produces a Left, the entire result is that Left (short-circuits).
|
||||
// Otherwise, returns Right containing an iterator of all Right values.
|
||||
//
|
||||
// The function eagerly evaluates all elements in the input iterator to detect any Left values,
|
||||
// then returns an iterator over the collected Right values. This is necessary because Either
|
||||
// represents computations that can fail, and we need to know if any element failed before
|
||||
// producing the result iterator.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - E: The error type for Left values
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that transforms each element into an Either
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that takes an iterator of A and returns Either containing an iterator of B
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// parse := func(s string) either.Either[error, int] {
|
||||
// v, err := strconv.Atoi(s)
|
||||
// return either.FromError(v, err)
|
||||
// }
|
||||
// input := slices.Values([]string{"1", "2", "3"})
|
||||
// result := either.TraverseSeq(parse)(input)
|
||||
// // result is Right(iterator over [1, 2, 3])
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - TraverseArray: For slice-based traversal
|
||||
// - SequenceSeq: For sequencing iterators of Either values
|
||||
func TraverseSeq[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, iter.Seq[A], iter.Seq[B]] {
|
||||
return func(ga iter.Seq[A]) Either[E, iter.Seq[B]] {
|
||||
var bs []B
|
||||
for a := range ga {
|
||||
b := f(a)
|
||||
if b.isLeft {
|
||||
return Left[iter.Seq[B]](b.l)
|
||||
}
|
||||
bs = append(bs, b.r)
|
||||
}
|
||||
return Of[E](slices.Values(bs))
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSeq converts an iterator of Either into an Either of iterator.
|
||||
// If any element is Left, returns that Left (short-circuits).
|
||||
// Otherwise, returns Right containing an iterator of all the Right values.
|
||||
//
|
||||
// This function eagerly evaluates all Either values in the input iterator to detect
|
||||
// any Left values, then returns an iterator over the collected Right values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - E: The error type for Left values
|
||||
// - A: The value type for Right values
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - ma: An iterator of Either values
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Either containing an iterator of Right values, or the first Left encountered
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// eithers := slices.Values([]either.Either[error, int]{
|
||||
// either.Right[error](1),
|
||||
// either.Right[error](2),
|
||||
// either.Right[error](3),
|
||||
// })
|
||||
// result := either.SequenceSeq(eithers)
|
||||
// // result is Right(iterator over [1, 2, 3])
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - SequenceArray: For slice-based sequencing
|
||||
// - TraverseSeq: For transforming and sequencing in one step
|
||||
func SequenceSeq[E, A any](ma iter.Seq[Either[E, A]]) Either[E, iter.Seq[A]] {
|
||||
return TraverseSeq(F.Identity[Either[E, A]])(ma)
|
||||
}
|
||||
|
||||
+247
-11
@@ -1,27 +1,28 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
TST "github.com/IBM/fp-go/v2/internal/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompactArray(t *testing.T) {
|
||||
ar := A.From(
|
||||
ar := []Either[string, string]{
|
||||
Of[string]("ok"),
|
||||
Left[string]("err"),
|
||||
Of[string]("ok"),
|
||||
)
|
||||
|
||||
res := CompactArray(ar)
|
||||
assert.Equal(t, 2, len(res))
|
||||
}
|
||||
assert.Equal(t, 2, len(CompactArray(ar)))
|
||||
}
|
||||
|
||||
func TestSequenceArray(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayTest(
|
||||
FromStrictEquals[error, bool](),
|
||||
Pointed[error, string](),
|
||||
@@ -29,14 +30,12 @@ func TestSequenceArray(t *testing.T) {
|
||||
Functor[error, []string, bool](),
|
||||
SequenceArray[error, string],
|
||||
)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for i := range 10 {
|
||||
t.Run(fmt.Sprintf("TestSequenceArray %d", i), s(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceArrayError(t *testing.T) {
|
||||
|
||||
s := TST.SequenceArrayErrorTest(
|
||||
FromStrictEquals[error, bool](),
|
||||
Left[string, error],
|
||||
@@ -46,6 +45,243 @@ func TestSequenceArrayError(t *testing.T) {
|
||||
Functor[error, []string, bool](),
|
||||
SequenceArray[error, string],
|
||||
)
|
||||
// run across four bits
|
||||
s(4)(t)
|
||||
}
|
||||
|
||||
func TestTraverseSeq_Success(t *testing.T) {
|
||||
parse := func(s string) Either[error, int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
|
||||
collectInts := func(result Either[error, iter.Seq[int]]) []int {
|
||||
return F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("transforms all elements successfully", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
|
||||
assert.Equal(t, []int{1, 2, 3}, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{}))
|
||||
assert.Empty(t, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"42"}))
|
||||
assert.Equal(t, []int{42}, collectInts(result))
|
||||
})
|
||||
|
||||
t.Run("preserves order of elements", func(t *testing.T) {
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"10", "20", "30", "40", "50"}))
|
||||
assert.Equal(t, []int{10, 20, 30, 40, 50}, collectInts(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseSeq_Failure(t *testing.T) {
|
||||
parse := func(s string) Either[error, int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
|
||||
extractErr := func(result Either[error, iter.Seq[int]]) error {
|
||||
return F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("short-circuits on first Left", func(t *testing.T) {
|
||||
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "invalid", "3"})))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid syntax")
|
||||
})
|
||||
|
||||
t.Run("returns first error when multiple failures exist", func(t *testing.T) {
|
||||
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "bad1", "bad2"})))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bad1")
|
||||
})
|
||||
|
||||
t.Run("handles custom error types", func(t *testing.T) {
|
||||
customErr := errors.New("custom validation error")
|
||||
validate := func(n int) Either[error, int] {
|
||||
if n == 2 {
|
||||
return Left[int](customErr)
|
||||
}
|
||||
return Right[error](n * 10)
|
||||
}
|
||||
err := extractErr(TraverseSeq(validate)(slices.Values([]int{1, 2, 3})))
|
||||
assert.Equal(t, customErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseSeq_EdgeCases(t *testing.T) {
|
||||
t.Run("handles complex transformations", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
transform := func(id int) Either[error, User] {
|
||||
return Right[error](User{ID: id, Name: fmt.Sprintf("User%d", id)})
|
||||
}
|
||||
|
||||
result := TraverseSeq(transform)(slices.Values([]int{1, 2, 3}))
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []User { t.Fatal(e); return nil },
|
||||
slices.Collect[User],
|
||||
))
|
||||
|
||||
assert.Equal(t, []User{
|
||||
{ID: 1, Name: "User1"},
|
||||
{ID: 2, Name: "User2"},
|
||||
{ID: 3, Name: "User3"},
|
||||
}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with identity transformation", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, int]{
|
||||
Right[error](1),
|
||||
Right[error](2),
|
||||
Right[error](3),
|
||||
})
|
||||
|
||||
result := TraverseSeq(F.Identity[Either[error, int]])(input)
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Success(t *testing.T) {
|
||||
collectInts := func(result Either[error, iter.Seq[int]]) []int {
|
||||
return F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("sequences multiple Right values", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Right[error](2), Right[error](3)})
|
||||
assert.Equal(t, []int{1, 2, 3}, collectInts(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, string]{})
|
||||
result := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []string { t.Fatal(e); return nil },
|
||||
slices.Collect[string],
|
||||
))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("works with single Right value", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, string]{Right[error]("hello")})
|
||||
result := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []string { t.Fatal(e); return nil },
|
||||
slices.Collect[string],
|
||||
))
|
||||
assert.Equal(t, []string{"hello"}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order of results", func(t *testing.T) {
|
||||
input := slices.Values([]Either[error, int]{
|
||||
Right[error](5), Right[error](4), Right[error](3), Right[error](2), Right[error](1),
|
||||
})
|
||||
assert.Equal(t, []int{5, 4, 3, 2, 1}, collectInts(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Item struct {
|
||||
Value int
|
||||
Label string
|
||||
}
|
||||
|
||||
input := slices.Values([]Either[error, Item]{
|
||||
Right[error](Item{Value: 1, Label: "first"}),
|
||||
Right[error](Item{Value: 2, Label: "second"}),
|
||||
Right[error](Item{Value: 3, Label: "third"}),
|
||||
})
|
||||
|
||||
collected := F.Pipe1(SequenceSeq(input), Fold(
|
||||
func(e error) []Item { t.Fatal(e); return nil },
|
||||
slices.Collect[Item],
|
||||
))
|
||||
|
||||
assert.Equal(t, []Item{
|
||||
{Value: 1, Label: "first"},
|
||||
{Value: 2, Label: "second"},
|
||||
{Value: 3, Label: "third"},
|
||||
}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Failure(t *testing.T) {
|
||||
extractErr := func(result Either[error, iter.Seq[int]]) error {
|
||||
return F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
}
|
||||
|
||||
t.Run("short-circuits on first Left", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Left[int](testErr), Right[error](3)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("returns first error when multiple Left values exist", func(t *testing.T) {
|
||||
err1 := errors.New("error 1")
|
||||
err2 := errors.New("error 2")
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Left[int](err1), Left[int](err2)})
|
||||
assert.Equal(t, err1, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("handles Left at the beginning", func(t *testing.T) {
|
||||
testErr := errors.New("first error")
|
||||
input := slices.Values([]Either[error, int]{Left[int](testErr), Right[error](2), Right[error](3)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
|
||||
t.Run("handles Left at the end", func(t *testing.T) {
|
||||
testErr := errors.New("last error")
|
||||
input := slices.Values([]Either[error, int]{Right[error](1), Right[error](2), Left[int](testErr)})
|
||||
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceSeq_Integration(t *testing.T) {
|
||||
t.Run("integrates with TraverseSeq", func(t *testing.T) {
|
||||
parse := func(s string) Either[error, int] {
|
||||
v, err := strconv.Atoi(s)
|
||||
return TryCatchError(v, err)
|
||||
}
|
||||
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
|
||||
assert.True(t, IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("SequenceSeq is equivalent to TraverseSeq with Identity", func(t *testing.T) {
|
||||
mkInput := func() []Either[error, int] {
|
||||
return []Either[error, int]{Right[error](10), Right[error](20), Right[error](30)}
|
||||
}
|
||||
|
||||
collected1 := F.Pipe1(SequenceSeq(slices.Values(mkInput())), Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
collected2 := F.Pipe1(TraverseSeq(F.Identity[Either[error, int]])(slices.Values(mkInput())), Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
assert.Equal(t, collected1, collected2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+98
-6
@@ -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]())
|
||||
}
|
||||
|
||||
+12
-10
@@ -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]]
|
||||
)
|
||||
|
||||
@@ -236,6 +236,7 @@ func Pipe4[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, T
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow4[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, T0, T1, T2, T3, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(T0) T4 {
|
||||
//go:inline
|
||||
return func(t0 T0) T4 {
|
||||
return Pipe4(t0, f1, f2, f3, f4)
|
||||
}
|
||||
@@ -302,6 +303,7 @@ func Pipe5[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow5[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, T0, T1, T2, T3, T4, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(T0) T5 {
|
||||
//go:inline
|
||||
return func(t0 T0) T5 {
|
||||
return Pipe5(t0, f1, f2, f3, f4, f5)
|
||||
}
|
||||
@@ -370,6 +372,7 @@ func Pipe6[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow6[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, T0, T1, T2, T3, T4, T5, T6 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6) func(T0) T6 {
|
||||
//go:inline
|
||||
return func(t0 T0) T6 {
|
||||
return Pipe6(t0, f1, f2, f3, f4, f5, f6)
|
||||
}
|
||||
@@ -440,6 +443,7 @@ func Pipe7[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow7[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, T0, T1, T2, T3, T4, T5, T6, T7 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7) func(T0) T7 {
|
||||
//go:inline
|
||||
return func(t0 T0) T7 {
|
||||
return Pipe7(t0, f1, f2, f3, f4, f5, f6, f7)
|
||||
}
|
||||
@@ -512,6 +516,7 @@ func Pipe8[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow8[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, T0, T1, T2, T3, T4, T5, T6, T7, T8 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8) func(T0) T8 {
|
||||
//go:inline
|
||||
return func(t0 T0) T8 {
|
||||
return Pipe8(t0, f1, f2, f3, f4, f5, f6, f7, f8)
|
||||
}
|
||||
@@ -586,6 +591,7 @@ func Pipe9[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow9[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9) func(T0) T9 {
|
||||
//go:inline
|
||||
return func(t0 T0) T9 {
|
||||
return Pipe9(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9)
|
||||
}
|
||||
@@ -662,6 +668,7 @@ func Pipe10[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow10[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(T0) T10 {
|
||||
//go:inline
|
||||
return func(t0 T0) T10 {
|
||||
return Pipe10(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10)
|
||||
}
|
||||
@@ -740,6 +747,7 @@ func Pipe11[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow11[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11) func(T0) T11 {
|
||||
//go:inline
|
||||
return func(t0 T0) T11 {
|
||||
return Pipe11(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11)
|
||||
}
|
||||
@@ -820,6 +828,7 @@ func Pipe12[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow12[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12) func(T0) T12 {
|
||||
//go:inline
|
||||
return func(t0 T0) T12 {
|
||||
return Pipe12(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12)
|
||||
}
|
||||
@@ -902,6 +911,7 @@ func Pipe13[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow13[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13) func(T0) T13 {
|
||||
//go:inline
|
||||
return func(t0 T0) T13 {
|
||||
return Pipe13(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13)
|
||||
}
|
||||
@@ -986,6 +996,7 @@ func Pipe14[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow14[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14) func(T0) T14 {
|
||||
//go:inline
|
||||
return func(t0 T0) T14 {
|
||||
return Pipe14(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14)
|
||||
}
|
||||
@@ -1072,6 +1083,7 @@ func Pipe15[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow15[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15) func(T0) T15 {
|
||||
//go:inline
|
||||
return func(t0 T0) T15 {
|
||||
return Pipe15(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15)
|
||||
}
|
||||
@@ -1160,6 +1172,7 @@ func Pipe16[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow16[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16) func(T0) T16 {
|
||||
//go:inline
|
||||
return func(t0 T0) T16 {
|
||||
return Pipe16(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16)
|
||||
}
|
||||
@@ -1250,6 +1263,7 @@ func Pipe17[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow17[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17) func(T0) T17 {
|
||||
//go:inline
|
||||
return func(t0 T0) T17 {
|
||||
return Pipe17(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17)
|
||||
}
|
||||
@@ -1342,6 +1356,7 @@ func Pipe18[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow18[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18) func(T0) T18 {
|
||||
//go:inline
|
||||
return func(t0 T0) T18 {
|
||||
return Pipe18(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18)
|
||||
}
|
||||
@@ -1436,6 +1451,7 @@ func Pipe19[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow19[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, F19 ~func(T18) T19, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18, f19 F19) func(T0) T19 {
|
||||
//go:inline
|
||||
return func(t0 T0) T19 {
|
||||
return Pipe19(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19)
|
||||
}
|
||||
@@ -1532,6 +1548,7 @@ func Pipe20[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow20[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, F19 ~func(T18) T19, F20 ~func(T19) T20, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18, f19 F19, f20 F20) func(T0) T20 {
|
||||
//go:inline
|
||||
return func(t0 T0) T20 {
|
||||
return Pipe20(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.24
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v3 v3.7.0
|
||||
github.com/urfave/cli/v3 v3.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -4,10 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
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=
|
||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||
github.com/urfave/cli/v3 v3.8.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=
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
package array
|
||||
|
||||
import "slices"
|
||||
|
||||
func Of[GA ~[]A, A any](a A) GA {
|
||||
return GA{a}
|
||||
}
|
||||
@@ -197,3 +199,9 @@ func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
}
|
||||
return ras
|
||||
}
|
||||
|
||||
func UnsafeUpdateAt[GT ~[]T, T any](as GT, i int, v T) GT {
|
||||
c := slices.Clone(as)
|
||||
c[i] = v
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import (
|
||||
// - Multiple elements: recursively divides and conquers
|
||||
func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
empty HKTRB,
|
||||
empty func() HKTRB,
|
||||
concat func(HKTRB, HKTRB) HKTRB,
|
||||
fbs []HKTB,
|
||||
start, end int,
|
||||
@@ -54,7 +54,7 @@ func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
|
||||
switch end - start {
|
||||
case 0:
|
||||
return empty
|
||||
return empty()
|
||||
case 1:
|
||||
return fof(fbs[start])
|
||||
default:
|
||||
@@ -254,7 +254,7 @@ HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
func MonadSequence[GA ~[]HKTA, HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
empty HKTRA,
|
||||
empty func() HKTRA,
|
||||
concat func(HKTRA, HKTRA) HKTRA,
|
||||
|
||||
ta GA) HKTRA {
|
||||
@@ -263,7 +263,7 @@ func MonadSequence[GA ~[]HKTA, HKTA, HKTRA any](
|
||||
|
||||
func Sequence[GA ~[]HKTA, HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
empty HKTRA,
|
||||
empty func() HKTRA,
|
||||
concat func(HKTRA, HKTRA) HKTRA,
|
||||
) func(GA) HKTRA {
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package filterable
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Separated[A, B any] = pair.Pair[A, B]
|
||||
|
||||
FilterType[A, HKTA any] = func(func(A) bool) func(HKTA) HKTA
|
||||
|
||||
FilterMapType[A, B, HKTA, HKTB any] = func(func(A) Option[B]) func(HKTA) HKTB
|
||||
)
|
||||
@@ -73,7 +73,7 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
|
||||
|
||||
fof := F.Bind2nd(fmap_b, Of[GB])
|
||||
|
||||
empty := fof_gb(Empty[GB]())
|
||||
empty := F.Nullary2(Empty[GB], fof_gb)
|
||||
|
||||
cb := F.Curry2(Concat[GB])
|
||||
concat_gb := F.Bind2nd(fmap_gb, cb)
|
||||
@@ -180,7 +180,7 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
|
||||
// convert to an array
|
||||
hktb := ToArray[GA, []HKTA](ta)
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty, m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
|
||||
@@ -223,7 +223,7 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
|
||||
// convert to an array
|
||||
hktb := MonadMapToArrayWithIndex[GA, []HKTB](ta, f)
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty, m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package witherable
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/filterable"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
func Filter[A, HKT_G_A, HKT_F_HKT_G_A any](
|
||||
fmap functor.MapType[HKT_G_A, HKT_G_A, HKT_F_HKT_G_A, HKT_F_HKT_G_A],
|
||||
ffilter filterable.FilterType[A, HKT_G_A],
|
||||
) func(func(A) bool) func(HKT_F_HKT_G_A) HKT_F_HKT_G_A {
|
||||
return function.Flow2(
|
||||
ffilter,
|
||||
fmap,
|
||||
)
|
||||
}
|
||||
|
||||
func FilterMap[A, B, HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B any](
|
||||
fmap functor.MapType[HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B],
|
||||
ffilter filterable.FilterMapType[A, B, HKT_G_A, HKT_G_B],
|
||||
) func(func(A) Option[B]) func(HKT_F_HKT_G_A) HKT_F_HKT_G_B {
|
||||
return function.Flow2(
|
||||
ffilter,
|
||||
fmap,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package witherable
|
||||
@@ -0,0 +1,7 @@
|
||||
package witherable
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
)
|
||||
|
||||
// AsyncBuf converts a synchronous sequence into an asynchronous buffered sequence.
|
||||
// It spawns a goroutine to consume the input sequence and sends values through
|
||||
// a buffered channel, allowing concurrent production and consumption of elements.
|
||||
//
|
||||
// The function provides backpressure control through the buffer size and properly
|
||||
// handles early termination when the consumer stops iterating. This is useful for
|
||||
// decoupling producers and consumers, enabling pipeline parallelism, or when you
|
||||
// need to process sequences concurrently.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type of elements in the sequence
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - input: The source sequence to be consumed asynchronously
|
||||
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
|
||||
// synchronization between producer and consumer.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq[T]: A new sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - Spawns a goroutine that consumes the input sequence
|
||||
// - Elements are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// the producer goroutine is signaled to stop via a done channel
|
||||
// - Both the producer goroutine and the done channel are properly cleaned up
|
||||
// - The channel is closed when the input sequence is exhausted or early termination occurs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create an async sequence with a buffer of 10
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// async := AsyncBuf(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for v := range async {
|
||||
// fmt.Println(v) // Prints: 1, 2, 3, 4, 5
|
||||
// }
|
||||
//
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// async := AsyncBuf(seq, 5)
|
||||
//
|
||||
// // Stop after 3 elements - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range async {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 3 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Example with Unbuffered Channel
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// seq := From(1, 2, 3)
|
||||
// async := AsyncBuf(seq, 0)
|
||||
//
|
||||
// // Producer and consumer are synchronized
|
||||
// for v := range async {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - From: Creates a sequence from values
|
||||
// - Map: Transforms sequence elements
|
||||
// - Filter: Filters sequence elements
|
||||
func AsyncBuf[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
return MergeBuf(A.Of(input), bufSize)
|
||||
}
|
||||
|
||||
// Async converts a synchronous sequence into an asynchronous sequence using a default buffer size.
|
||||
// This is a convenience wrapper around AsyncBuf that uses a default buffer size of 8.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - input: The source sequence to be consumed asynchronously
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns a goroutine that consumes the input sequence
|
||||
// - Elements are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - The channel is closed when the input sequence is exhausted
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// async := Async(seq)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for v := range async {
|
||||
// fmt.Println(v) // Prints: 1, 2, 3, 4, 5
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - AsyncBuf: Async with custom buffer size
|
||||
// - Async2: Asynchronous sequence for key-value sequences
|
||||
// - Merge: Merges multiple sequences concurrently
|
||||
func Async[T any](input Seq[T]) Seq[T] {
|
||||
return AsyncBuf(input, defaultBufferSize)
|
||||
}
|
||||
|
||||
// Async2Buf converts a synchronous key-value sequence into an asynchronous buffered sequence.
|
||||
// It spawns a goroutine to consume the input sequence and sends key-value pairs through
|
||||
// a buffered channel, allowing concurrent production and consumption of elements.
|
||||
//
|
||||
// This function is the Seq2 variant of Async, providing the same asynchronous behavior
|
||||
// for key-value sequences. It internally converts the Seq2 to a sequence of Pairs,
|
||||
// applies Async, and converts back to Seq2.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - K: The type of keys in the sequence
|
||||
// - V: The type of values in the sequence
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - input: The source key-value sequence to be consumed asynchronously
|
||||
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
|
||||
// synchronization between producer and consumer.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq2[K, V]: A new key-value sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - Spawns a goroutine that consumes the input key-value sequence
|
||||
// - Key-value pairs are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// the producer goroutine is signaled to stop via a done channel
|
||||
// - Both the producer goroutine and the done channel are properly cleaned up
|
||||
// - The channel is closed when the input sequence is exhausted or early termination occurs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create an async key-value sequence with a buffer of 10
|
||||
// seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// async := Async2Buf(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // 1: a
|
||||
// // 2: b
|
||||
// // 3: c
|
||||
//
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
// async := Async2Buf(seq, 5)
|
||||
//
|
||||
// // Stop after 2 pairs - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// count++
|
||||
// if count >= 2 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Async: Asynchronous sequence for single-value sequences
|
||||
// - ToSeqPair: Converts Seq2 to Seq of Pairs
|
||||
// - FromSeqPair: Converts Seq of Pairs to Seq2
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
func Async2Buf[K, V any](input Seq2[K, V], bufSize int) Seq2[K, V] {
|
||||
return FromSeqPair(AsyncBuf(ToSeqPair(input), bufSize))
|
||||
}
|
||||
|
||||
// Async2 converts a synchronous key-value sequence into an asynchronous sequence using a default buffer size.
|
||||
// This is a convenience wrapper around Async2Buf that uses a default buffer size of 8.
|
||||
// It's the Seq2 variant of Async, providing the same asynchronous behavior for key-value sequences.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - K: The type of keys in the sequence
|
||||
// - V: The type of values in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - input: The source key-value sequence to be consumed asynchronously
|
||||
//
|
||||
// Returns:
|
||||
// - Seq2[K, V]: A new key-value sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns a goroutine that consumes the input key-value sequence
|
||||
// - Key-value pairs are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - The channel is closed when the input sequence is exhausted
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// async := Async2(seq)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // 1: a
|
||||
// // 2: b
|
||||
// // 3: c
|
||||
//
|
||||
// See Also:
|
||||
// - Async2Buf: Async2 with custom buffer size
|
||||
// - Async: Asynchronous sequence for single-value sequences
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
func Async2[K, V any](input Seq2[K, V]) Seq2[K, V] {
|
||||
return Async2Buf(input, defaultBufferSize)
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAsync_Success tests basic Async functionality
|
||||
func TestAsync_Success(t *testing.T) {
|
||||
t.Run("converts sequence to async with buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves element order", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "d", "e")
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []string{"a", "b", "c", "d", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
async := AsyncBuf(seq, 1)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with large sequence", func(t *testing.T) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
async := AsyncBuf(seq, 20)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, data, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_BufferSizes tests different buffer sizes
|
||||
func TestAsync_BufferSizes(t *testing.T) {
|
||||
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 0)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("small buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 2)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("large buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 100)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("negative buffer size treated as 0", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, -5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size equals sequence length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size larger than sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_Empty tests Async with empty sequences
|
||||
func TestAsync_Empty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
async := AsyncBuf(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty with zero buffer", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := AsyncBuf(seq, 0)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_EarlyTermination tests that Async properly handles early termination
|
||||
func TestAsync_EarlyTermination(t *testing.T) {
|
||||
t.Run("stops producer when consumer breaks", func(t *testing.T) {
|
||||
var producerCount atomic.Int32
|
||||
|
||||
// Create a sequence that tracks how many elements were produced
|
||||
seq := func(yield func(int) bool) {
|
||||
for i := range 100 {
|
||||
producerCount.Add(1)
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
// Consume only 5 elements
|
||||
count := 0
|
||||
for range async {
|
||||
count++
|
||||
if count >= 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Give goroutine time to clean up
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Producer should have stopped shortly after consumer stopped
|
||||
// It may produce a few extra due to buffering, but not all 100
|
||||
produced := producerCount.Load()
|
||||
assert.LessOrEqual(t, produced, int32(20), "producer should stop after consumer breaks")
|
||||
assert.GreaterOrEqual(t, produced, int32(5), "producer should produce at least what was consumed")
|
||||
})
|
||||
|
||||
t.Run("handles yield returning false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
collected = append(collected, v)
|
||||
if v == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("early termination with unbuffered channel", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 0)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
collected = append(collected, v)
|
||||
if v == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_WithComplexTypes tests Async with complex data types
|
||||
func TestAsync_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("works with pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []*Person{p1, p2, p3}, result)
|
||||
})
|
||||
|
||||
t.Run("works with slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6})
|
||||
async := AsyncBuf(seq, 2)
|
||||
result := toSlice(async)
|
||||
expected := [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("works with maps", func(t *testing.T) {
|
||||
m1 := map[string]int{"a": 1}
|
||||
m2 := map[string]int{"b": 2}
|
||||
m3 := map[string]int{"c": 3}
|
||||
seq := From(m1, m2, m3)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []map[string]int{m1, m2, m3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_WithChainedOperations tests Async with other sequence operations
|
||||
func TestAsync_WithChainedOperations(t *testing.T) {
|
||||
t.Run("async after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
async := AsyncBuf(mapped, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("map after async", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("async after filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
async := AsyncBuf(filtered, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after async", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("async after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
async := AsyncBuf(chained, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple async operations", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async1 := AsyncBuf(seq, 3)
|
||||
async2 := AsyncBuf(async1, 2)
|
||||
result := toSlice(async2)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_Concurrency tests concurrent behavior
|
||||
func TestAsync_Concurrency(t *testing.T) {
|
||||
t.Run("allows concurrent production and consumption", func(t *testing.T) {
|
||||
// Create a slow producer
|
||||
seq := func(yield func(int) bool) {
|
||||
for i := range 5 {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
result := toSlice(async)
|
||||
|
||||
// Verify all elements are produced correctly
|
||||
assert.Equal(t, []int{0, 1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("handles concurrent consumption safely", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
// Consume with some processing time
|
||||
var sum atomic.Int32
|
||||
for v := range async {
|
||||
sum.Add(int32(v))
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(55), sum.Load())
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_EdgeCases tests edge cases
|
||||
func TestAsync_EdgeCases(t *testing.T) {
|
||||
t.Run("very large buffer size", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 1000000)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size of 1", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 1)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("works with replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{42, 42, 42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with makeBy", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkAsync(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_LargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_SmallBuffer(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 1)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_LargeBuffer(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_Unbuffered(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 0)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_WithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
for range mapped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_WithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 5)
|
||||
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
for range filtered {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleAsync() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleAsync_unbuffered() {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 0)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleAsync_earlyTermination() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
count++
|
||||
if count >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleAsync_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
doubled := MonadMap(async, N.Mul(2))
|
||||
|
||||
for v := range doubled {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8 10
|
||||
}
|
||||
|
||||
func ExampleAsync_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
evens := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
|
||||
for v := range evens {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8 10
|
||||
}
|
||||
|
||||
// TestAsync2_Success tests basic Async2 functionality
|
||||
func TestAsync2_Success(t *testing.T) {
|
||||
t.Run("converts Seq2 to async with buffer", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 10)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("preserves key-value pairs order", func(t *testing.T) {
|
||||
seq := MonadZip(From("x", "y", "z"), From(10, 20, 30))
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
keys := []string{}
|
||||
values := []int{}
|
||||
for k, v := range async {
|
||||
keys = append(keys, k)
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"x", "y", "z"}, keys)
|
||||
assert.Equal(t, []int{10, 20, 30}, values)
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
seq := Of2("key", 42)
|
||||
async := Async2Buf(seq, 1)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with large Seq2", func(t *testing.T) {
|
||||
keys := make([]int, 100)
|
||||
values := make([]string, 100)
|
||||
for i := range keys {
|
||||
keys[i] = i
|
||||
values[i] = fmt.Sprintf("val%d", i)
|
||||
}
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
async := Async2Buf(seq, 20)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, 100, len(result))
|
||||
for i := range 100 {
|
||||
assert.Equal(t, fmt.Sprintf("val%d", i), result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_BufferSizes tests different buffer sizes
|
||||
func TestAsync2_BufferSizes(t *testing.T) {
|
||||
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 0)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("negative buffer size treated as 0", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, -5)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("large buffer", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 100)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_Empty tests Async2 with empty sequences
|
||||
func TestAsync2_Empty(t *testing.T) {
|
||||
t.Run("empty Seq2", func(t *testing.T) {
|
||||
seq := MonadZip(Empty[int](), Empty[string]())
|
||||
async := Async2Buf(seq, 5)
|
||||
result := toMap(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_EarlyTermination tests that Async2 properly handles early termination
|
||||
func TestAsync2_EarlyTermination(t *testing.T) {
|
||||
t.Run("stops producer when consumer breaks", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for range async {
|
||||
count++
|
||||
if count >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_WithChainedOperations tests Async2 with other operations
|
||||
func TestAsync2_WithChainedOperations(t *testing.T) {
|
||||
t.Run("async2 after map", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From(10, 20, 30))
|
||||
mapped := MonadMapWithKey(seq, func(k, v int) int { return k + v })
|
||||
async := Async2Buf(mapped, 5)
|
||||
result := toMap(async)
|
||||
expected := map[int]int{1: 11, 2: 22, 3: 33}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_Success tests basic ToSeqPair functionality
|
||||
func TestToSeqPair_Success(t *testing.T) {
|
||||
t.Run("converts Seq2 to Seq of Pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, "a", pair.Tail(result[0]))
|
||||
assert.Equal(t, 2, pair.Head(result[1]))
|
||||
assert.Equal(t, "b", pair.Tail(result[1]))
|
||||
assert.Equal(t, 3, pair.Head(result[2]))
|
||||
assert.Equal(t, "c", pair.Tail(result[2]))
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
seq2 := MonadZip(From("x", "y", "z"), From(10, 20, 30))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
for i, p := range result {
|
||||
expectedKey := string(rune('x' + i))
|
||||
expectedVal := (i + 1) * 10
|
||||
assert.Equal(t, expectedKey, pair.Head(p))
|
||||
assert.Equal(t, expectedVal, pair.Tail(p))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
seq2 := Of2("key", 42)
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "key", pair.Head(result[0]))
|
||||
assert.Equal(t, 42, pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_Empty tests ToSeqPair with empty sequences
|
||||
func TestToSeqPair_Empty(t *testing.T) {
|
||||
t.Run("empty Seq2 produces empty Seq", func(t *testing.T) {
|
||||
seq2 := MonadZip(Empty[int](), Empty[string]())
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_WithComplexTypes tests ToSeqPair with complex types
|
||||
func TestToSeqPair_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with struct values", func(t *testing.T) {
|
||||
seq2 := MonadZip(
|
||||
From(1, 2, 3),
|
||||
From(Person{"Alice", 30}, Person{"Bob", 25}, Person{"Charlie", 35}),
|
||||
)
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, Person{"Alice", 30}, pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_Success tests basic FromSeqPair functionality
|
||||
func TestFromSeqPair_Success(t *testing.T) {
|
||||
t.Run("converts Seq of Pairs to Seq2", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair("x", 10),
|
||||
pair.MakePair("y", 20),
|
||||
pair.MakePair("z", 30),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
|
||||
keys := []string{}
|
||||
values := []int{}
|
||||
for k, v := range seq2 {
|
||||
keys = append(keys, k)
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"x", "y", "z"}, keys)
|
||||
assert.Equal(t, []int{10, 20, 30}, values)
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
pairs := From(pair.MakePair("key", 42))
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_Empty tests FromSeqPair with empty sequences
|
||||
func TestFromSeqPair_Empty(t *testing.T) {
|
||||
t.Run("empty Seq produces empty Seq2", func(t *testing.T) {
|
||||
pairs := Empty[Pair[int, string]]()
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_WithComplexTypes tests FromSeqPair with complex types
|
||||
func TestFromSeqPair_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with struct values", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, Person{"Alice", 30}),
|
||||
pair.MakePair(2, Person{"Bob", 25}),
|
||||
pair.MakePair(3, Person{"Charlie", 35}),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
expected := map[int]Person{
|
||||
1: {"Alice", 30},
|
||||
2: {"Bob", 25},
|
||||
3: {"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRoundTrip tests that ToSeqPair and FromSeqPair are inverses
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
t.Run("ToSeqPair then FromSeqPair", func(t *testing.T) {
|
||||
original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(original)
|
||||
restored := FromSeqPair(pairs)
|
||||
result := toMap(restored)
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("FromSeqPair then ToSeqPair", func(t *testing.T) {
|
||||
original := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(original)
|
||||
restored := ToSeqPair(seq2)
|
||||
result := toSlice(restored)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, "a", pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for Async2
|
||||
func BenchmarkAsync2(b *testing.B) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async2Buf(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync2_LargeSequence(b *testing.B) {
|
||||
keys := make([]int, 1000)
|
||||
values := make([]string, 1000)
|
||||
for i := range keys {
|
||||
keys[i] = i
|
||||
values[i] = fmt.Sprintf("val%d", i)
|
||||
}
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async2Buf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests for FromSeqPair
|
||||
func BenchmarkFromSeqPair(b *testing.B) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
pair.MakePair(4, "d"),
|
||||
pair.MakePair(5, "e"),
|
||||
)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
seq2 := FromSeqPair(pairs)
|
||||
for range seq2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRoundTrip(b *testing.B) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
pairs := ToSeqPair(seq)
|
||||
restored := FromSeqPair(pairs)
|
||||
for range restored {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for Async2
|
||||
func ExampleAsync2() {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 10)
|
||||
|
||||
for k, v := range async {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
// 3: c
|
||||
}
|
||||
|
||||
func ExampleAsync2_earlyTermination() {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for k, v := range async {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
count++
|
||||
if count >= 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
}
|
||||
|
||||
// Example tests for FromSeqPair
|
||||
func ExampleFromSeqPair() {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
|
||||
for k, v := range seq2 {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
// 3: c
|
||||
}
|
||||
@@ -34,6 +34,13 @@ import (
|
||||
// 3. Filtering to keep only pairs where the boolean (tail) is true
|
||||
// 4. Extracting the original values (head) from the filtered pairs
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Data: --1--2--3--4--5-->
|
||||
// Selectors: --T--F--T--F--T-->
|
||||
// Compress
|
||||
// Output: --1-----3-----5-->
|
||||
//
|
||||
// RxJS Equivalent: Similar to combining [zip] with [filter] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
@@ -21,6 +21,12 @@ package iter
|
||||
// all elements repeatedly. When the end of the input sequence is reached, it starts over
|
||||
// from the beginning, continuing this pattern forever.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3|
|
||||
// Cycle
|
||||
// Output: --1--2--3--1--2--3--1--2--3--> (infinite)
|
||||
//
|
||||
// RxJS Equivalent: [repeat] - https://rxjs.dev/api/operators/repeat
|
||||
//
|
||||
// WARNING: This creates an INFINITE sequence for non-empty inputs. It must be used with
|
||||
|
||||
@@ -23,6 +23,16 @@ import "github.com/IBM/fp-go/v2/option"
|
||||
// contains at least one element, it returns Some(element). If the iterator is empty,
|
||||
// it returns None. The function consumes only the first element of the iterator.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// First
|
||||
// Output: --Some(1)|
|
||||
//
|
||||
// Input: --|
|
||||
// First
|
||||
// Output: --None|
|
||||
//
|
||||
// RxJS Equivalent: [first] - https://rxjs.dev/api/operators/first
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
@@ -82,6 +82,12 @@ func Of2[K, A any](k K, a A) Seq2[K, A] {
|
||||
// MonadMap transforms each element in a sequence using the provided function.
|
||||
// This is the monadic version that takes the sequence as the first parameter.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3-->
|
||||
// Map(x => x * 2)
|
||||
// Output: --2--4--6-->
|
||||
//
|
||||
// RxJS Equivalent: [map] - https://rxjs.dev/api/operators/map
|
||||
//
|
||||
// Example:
|
||||
@@ -186,6 +192,12 @@ func MapWithKey[K, A, B any](f func(K, A) B) Operator2[K, A, B] {
|
||||
|
||||
// MonadFilter returns a sequence containing only elements that satisfy the predicate.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// Filter(x => x % 2 == 0)
|
||||
// Output: -----2-----4----->
|
||||
//
|
||||
// RxJS Equivalent: [filter] - https://rxjs.dev/api/operators/filter
|
||||
//
|
||||
// Example:
|
||||
@@ -293,6 +305,12 @@ func FilterWithKey[K, A any](pred func(K, A) bool) Operator2[K, A, A] {
|
||||
// MonadFilterMap applies a function that returns an Option to each element,
|
||||
// keeping only the Some values and unwrapping them.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// FilterMap(x => x % 2 == 0 ? Some(x * 10) : None)
|
||||
// Output: -----20----40---->
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
@@ -430,6 +448,12 @@ func FilterMapWithKey[K, A, B any](f func(K, A) Option[B]) Operator2[K, A, B] {
|
||||
// MonadChain applies a function that returns a sequence to each element and flattens the results.
|
||||
// This is the monadic bind operation (flatMap).
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1-----2-----3---->
|
||||
// Chain(x => [x, x*10])
|
||||
// Output: --1-10--2-20--3-30->
|
||||
//
|
||||
// RxJS Equivalent: [mergeMap/flatMap] - https://rxjs.dev/api/operators/mergeMap
|
||||
//
|
||||
// Example:
|
||||
@@ -471,8 +495,34 @@ func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// ConcatMap is an alias for Chain that emphasizes sequential concatenation.
|
||||
// It maps each element to a sequence and concatenates the results in order.
|
||||
//
|
||||
// Unlike concurrent operations, ConcatMap preserves the order of elements:
|
||||
// it fully processes each input element (yielding all elements from f(a))
|
||||
// before moving to the next input element.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// result := ConcatMap(func(x int) Seq[int] {
|
||||
// return From(x, x*10)
|
||||
// })(seq)
|
||||
// // yields: 1, 10, 2, 20, 3, 30 (order preserved)
|
||||
//
|
||||
//go:inline
|
||||
func ConcatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// Flatten flattens a sequence of sequences into a single sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --[1,2]--[3,4]--[5]-->
|
||||
// Flatten
|
||||
// Output: --1-2----3-4----5---->
|
||||
//
|
||||
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
|
||||
//
|
||||
// Example:
|
||||
@@ -486,9 +536,22 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
return MonadChain(mma, F.Identity[Seq[A]])
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ConcatAll[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
return Flatten(mma)
|
||||
}
|
||||
|
||||
// MonadAp applies a sequence of functions to a sequence of values.
|
||||
// This is the applicative apply operation.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Functions: --(*2)---(+10)-->
|
||||
// Values: --5------3------>
|
||||
// Ap
|
||||
// Output: --10-6---15-13-->
|
||||
// (each function applied to each value)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(N.Mul(2), N.Add(10))
|
||||
@@ -577,6 +640,13 @@ func Replicate[A any](n int, a A) Seq[A] {
|
||||
// MonadReduce reduces a sequence to a single value by applying a function to each element
|
||||
// and an accumulator, starting with an initial value.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--|
|
||||
// Reduce((acc, x) => acc + x, 0)
|
||||
// Output: ------------------15|
|
||||
// (emits final result only)
|
||||
//
|
||||
// RxJS Equivalent: [reduce] - https://rxjs.dev/api/operators/reduce
|
||||
//
|
||||
// Example:
|
||||
@@ -811,6 +881,13 @@ func FoldMapWithKey[K, A, B any](m M.Monoid[B]) func(func(K, A) B) func(Seq2[K,
|
||||
// MonadFlap applies a fixed value to a sequence of functions.
|
||||
// This is the dual of MonadAp.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Functions: --(*2)---(+10)-->
|
||||
// Value: 5 (fixed)
|
||||
// Flap
|
||||
// Output: --10-----15----->
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(N.Mul(2), N.Add(10))
|
||||
@@ -832,6 +909,12 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
|
||||
// Prepend returns a function that adds an element to the beginning of a sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: -----2--3--4-->
|
||||
// Prepend(1)
|
||||
// Output: --1--2--3--4-->
|
||||
//
|
||||
// RxJS Equivalent: [startWith] - https://rxjs.dev/api/operators/startWith
|
||||
//
|
||||
// Example:
|
||||
@@ -847,6 +930,12 @@ func Prepend[A any](head A) Operator[A, A] {
|
||||
|
||||
// Append returns a function that adds an element to the end of a sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3-----|
|
||||
// Append(4)
|
||||
// Output: --1--2--3--4--|
|
||||
//
|
||||
// RxJS Equivalent: [endWith] - https://rxjs.dev/api/operators/endWith
|
||||
//
|
||||
// Example:
|
||||
@@ -863,6 +952,14 @@ func Append[A any](tail A) Operator[A, A] {
|
||||
// MonadZip combines two sequences into a sequence of pairs.
|
||||
// The resulting sequence stops when either input sequence is exhausted.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// SeqA: --1--2--3---->
|
||||
// SeqB: --a--b------->
|
||||
// Zip
|
||||
// Output: --(1,a)-(2,b)|
|
||||
// (stops when shorter sequence ends)
|
||||
//
|
||||
// RxJS Equivalent: [zip] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Example:
|
||||
@@ -1002,3 +1099,138 @@ func ToSeqPair[A, B any](as Seq2[A, B]) Seq[Pair[A, B]] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromSeqPair converts a sequence of Pairs into a key-value sequence.
|
||||
//
|
||||
// This function transforms a Seq[Pair[A, B]] (which yields Pair objects when iterated)
|
||||
// into a Seq2[A, B] (which yields key-value pairs as separate arguments). This is the
|
||||
// inverse operation of ToSeqPair and is useful when you need to convert from working
|
||||
// with pairs as first-class values back to the key-value iteration pattern.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the first element (key) in each pair
|
||||
// - B: The type of the second element (value) in each pair
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - as: A Seq that yields Pair objects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq2[A, B]: A key-value sequence that yields the unpacked pairs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create a sequence of pairs
|
||||
// pairs := From(
|
||||
// pair.MakePair("a", 1),
|
||||
// pair.MakePair("b", 2),
|
||||
// pair.MakePair("c", 3),
|
||||
// )
|
||||
// seq2 := FromSeqPair(pairs)
|
||||
//
|
||||
// // Iterate as key-value pairs
|
||||
// for k, v := range seq2 {
|
||||
// fmt.Printf("%s: %d\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // a: 1
|
||||
// // b: 2
|
||||
// // c: 3
|
||||
//
|
||||
// # Example with Map
|
||||
//
|
||||
// pairs := From(
|
||||
// pair.MakePair(1, 10),
|
||||
// pair.MakePair(2, 20),
|
||||
// pair.MakePair(3, 30),
|
||||
// )
|
||||
// seq2 := FromSeqPair(pairs)
|
||||
//
|
||||
// // Use with Seq2 operations
|
||||
// mapped := MonadMapWithKey(seq2, func(k, v int) int {
|
||||
// return k + v
|
||||
// })
|
||||
// // yields: 11, 22, 33
|
||||
//
|
||||
// # Example - Round-trip conversion
|
||||
//
|
||||
// original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// pairs := ToSeqPair(original)
|
||||
// restored := FromSeqPair(pairs)
|
||||
// // restored is equivalent to original
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ToSeqPair: Converts Seq2 to Seq of Pairs (inverse operation)
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
// - pair.MakePair: Creates a Pair from two values
|
||||
// - pair.Unpack: Unpacks a Pair into two values
|
||||
func FromSeqPair[A, B any](as Seq[Pair[A, B]]) Seq2[A, B] {
|
||||
return func(yield func(A, B) bool) {
|
||||
for p := range as {
|
||||
if !yield(pair.Unpack(p)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip returns an operator that skips the first n elements of a sequence.
|
||||
//
|
||||
// This function creates a transformation that discards the first n elements from
|
||||
// the source sequence and yields all remaining elements. If n is less than or equal
|
||||
// to 0, all elements are yielded. If n is greater than or equal to the sequence length,
|
||||
// an empty sequence is returned.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// The first n elements are consumed and discarded, then subsequent elements are yielded.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--6--7--8-->
|
||||
// Skip(3)
|
||||
// Output: -----------4--5--6--7--8-->
|
||||
//
|
||||
// RxJS Equivalent: [skip] - https://rxjs.dev/api/operators/skip
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - count: The number of elements to skip from the beginning of the sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by skipping the first count elements
|
||||
//
|
||||
// Example - Skip first 3 elements:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := Skip[int](3)(seq)
|
||||
// // yields: 4, 5
|
||||
//
|
||||
// Example - Skip more than available:
|
||||
//
|
||||
// seq := From(1, 2)
|
||||
// result := Skip[int](5)(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Skip zero or negative:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// result := Skip[int](0)(seq)
|
||||
// // yields: 1, 2, 3 (all elements)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// result := F.Pipe2(
|
||||
// seq,
|
||||
// Skip[int](3),
|
||||
// MonadFilter(seq, func(x int) bool { return x%2 == 0 }),
|
||||
// )
|
||||
// // yields: 4, 6, 8, 10 (skip first 3, then filter evens)
|
||||
func Skip[U any](count int) Operator[U, U] {
|
||||
return FilterWithIndex(func(idx int, _ U) bool { return idx >= count })
|
||||
}
|
||||
|
||||
@@ -612,3 +612,440 @@ func TestMapToArrayIdentity(t *testing.T) {
|
||||
result := mapper(seq)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
}
|
||||
|
||||
// TestSkip tests basic Skip functionality
|
||||
func TestSkip(t *testing.T) {
|
||||
t.Run("skips first n elements from sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](3)(seq))
|
||||
assert.Equal(t, []int{4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skips first element", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
result := toSlice(Skip[int](1)(seq))
|
||||
assert.Equal(t, []int{20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all elements when n equals length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Skip[int](3)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips all elements when n exceeds length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Skip[int](10)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "d", "e")
|
||||
result := toSlice(Skip[string](2)(seq))
|
||||
assert.Equal(t, []string{"c", "d", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("skips from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Skip[int](1)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips from large sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(Skip[int](7)(seq))
|
||||
assert.Equal(t, []int{8, 9, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipZeroOrNegative tests Skip with zero or negative values
|
||||
func TestSkipZeroOrNegative(t *testing.T) {
|
||||
t.Run("returns all elements when n is zero", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("returns all elements when n is negative", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](-1)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("returns all elements when n is large negative", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
result := toSlice(Skip[string](-100)(seq))
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipEmpty tests Skip with empty sequences
|
||||
func TestSkipEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Skip[int](5)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty from empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
result := toSlice(Skip[string](3)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when skipping zero from empty", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithComplexTypes tests Skip with complex data types
|
||||
func TestSkipWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("skips structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(Skip[Person](2)(seq))
|
||||
expected := []Person{
|
||||
{"Charlie", 35},
|
||||
{"David", 28},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("skips pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
result := toSlice(Skip[*Person](1)(seq))
|
||||
assert.Equal(t, []*Person{p2, p3}, result)
|
||||
})
|
||||
|
||||
t.Run("skips slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6}, []int{7, 8})
|
||||
result := toSlice(Skip[[]int](2)(seq))
|
||||
expected := [][]int{{5, 6}, {7, 8}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithChainedOperations tests Skip with other sequence operations
|
||||
func TestSkipWithChainedOperations(t *testing.T) {
|
||||
t.Run("skip after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(Skip[int](2)(mapped))
|
||||
assert.Equal(t, []int{6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skip after filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(Skip[int](2)(filtered))
|
||||
assert.Equal(t, []int{6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("map after skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := Skip[int](2)(seq)
|
||||
result := toSlice(MonadMap(skipped, N.Mul(10)))
|
||||
assert.Equal(t, []int{30, 40, 50}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
skipped := Skip[int](2)(seq)
|
||||
result := toSlice(MonadFilter(skipped, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("skip after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
result := toSlice(Skip[int](3)(chained))
|
||||
assert.Equal(t, []int{20, 3, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple skips", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped1 := Skip[int](2)(seq)
|
||||
skipped2 := Skip[int](3)(skipped1)
|
||||
result := toSlice(skipped2)
|
||||
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skip and take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped := Skip[int](3)(seq)
|
||||
taken := Take[int](3)(skipped)
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{4, 5, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("take and skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken := Take[int](7)(seq)
|
||||
skipped := Skip[int](2)(taken)
|
||||
result := toSlice(skipped)
|
||||
assert.Equal(t, []int{3, 4, 5, 6, 7}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithReplicate tests Skip with Replicate
|
||||
func TestSkipWithReplicate(t *testing.T) {
|
||||
t.Run("skips from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 42)
|
||||
result := toSlice(Skip[int](7)(seq))
|
||||
assert.Equal(t, []int{42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all from short replicate", func(t *testing.T) {
|
||||
seq := Replicate(2, "hello")
|
||||
result := toSlice(Skip[string](5)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips zero from replicate", func(t *testing.T) {
|
||||
seq := Replicate(3, 100)
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Equal(t, []int{100, 100, 100}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithMakeBy tests Skip with MakeBy
|
||||
func TestSkipWithMakeBy(t *testing.T) {
|
||||
t.Run("skips from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(10, func(i int) int { return i * i })
|
||||
result := toSlice(Skip[int](5)(seq))
|
||||
assert.Equal(t, []int{25, 36, 49, 64, 81}, result)
|
||||
})
|
||||
|
||||
t.Run("skips more than generated", func(t *testing.T) {
|
||||
seq := MakeBy(3, func(i int) int { return i + 1 })
|
||||
result := toSlice(Skip[int](10)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithPrependAppend tests Skip with Prepend and Append
|
||||
func TestSkipWithPrependAppend(t *testing.T) {
|
||||
t.Run("skip from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(Skip[int](2)(prepended))
|
||||
assert.Equal(t, []int{3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skip from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Skip[int](2)(appended))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("skip includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Skip[int](3)(appended))
|
||||
assert.Equal(t, []int{4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithFlatten tests Skip with Flatten
|
||||
func TestSkipWithFlatten(t *testing.T) {
|
||||
t.Run("skips from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5, 6))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(Skip[int](3)(flattened))
|
||||
assert.Equal(t, []int{4, 5, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("skips from flattened with empty inner sequences", func(t *testing.T) {
|
||||
nested := From(From(1, 2), Empty[int](), From(3, 4))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(Skip[int](2)(flattened))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipDoesNotConsumeSkippedElements tests that Skip is efficient
|
||||
func TestSkipDoesNotConsumeSkippedElements(t *testing.T) {
|
||||
t.Run("processes all elements including skipped", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})
|
||||
|
||||
skipped := Skip[int](7)(seq)
|
||||
|
||||
result := []int{}
|
||||
for v := range skipped {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{16, 18, 20}, result)
|
||||
// Skip still needs to iterate through skipped elements to count them
|
||||
assert.Equal(t, 10, callCount, "should process all elements")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipEdgeCases tests edge cases
|
||||
func TestSkipEdgeCases(t *testing.T) {
|
||||
t.Run("skip 0 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("skip 1 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Skip[int](1)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skip large number from small sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
result := toSlice(Skip[int](1000000)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skip with very large n", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Skip[int](int(^uint(0) >> 1))(seq)) // max int
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skip all but one", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](4)(seq))
|
||||
assert.Equal(t, []int{5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for Skip
|
||||
func BenchmarkSkip(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := Skip[int](5)(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := Skip[int](900)(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
skipped := Skip[int](5)(mapped)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
skipped := Skip[int](2)(filtered)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleSkip() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := Skip[int](3)(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 4 5
|
||||
}
|
||||
|
||||
func ExampleSkip_moreThanAvailable() {
|
||||
seq := From(1, 2, 3)
|
||||
skipped := Skip[int](10)(seq)
|
||||
|
||||
count := 0
|
||||
for range skipped {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleSkip_zero() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := Skip[int](0)(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleSkip_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
skipped := Skip[int](2)(evens)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 6 8 10
|
||||
}
|
||||
|
||||
func ExampleSkip_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
skipped := Skip[int](2)(doubled)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 6 8 10
|
||||
}
|
||||
|
||||
func ExampleSkip_chained() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := F.Pipe3(
|
||||
seq,
|
||||
Skip[int](3),
|
||||
Filter(func(x int) bool { return x%2 == 0 }),
|
||||
toSlice[int],
|
||||
)
|
||||
|
||||
fmt.Println(result)
|
||||
// Output: [4 6 8 10]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,16 @@ import (
|
||||
// sequence. If the iterator contains at least one element, it returns Some(element).
|
||||
// If the iterator is empty, it returns None.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--|
|
||||
// Last
|
||||
// Output: -----------------Some(5)|
|
||||
//
|
||||
// Input: --|
|
||||
// Last
|
||||
// Output: --None|
|
||||
//
|
||||
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBufferSize = 8
|
||||
)
|
||||
|
||||
// MergeBuf merges multiple sequences concurrently into a single sequence.
|
||||
// It spawns a goroutine for each input sequence and merges their elements through
|
||||
// a buffered channel, allowing concurrent production from all sources. The output
|
||||
// order is non-deterministic and depends on the timing of concurrent producers.
|
||||
//
|
||||
// This function is useful for combining results from multiple concurrent operations,
|
||||
// processing data from multiple sources in parallel, or implementing fan-in patterns
|
||||
// where multiple producers feed into a single consumer.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - iterables: A slice of sequences to merge. If empty, returns an empty sequence.
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
// A buffer of 0 creates an unbuffered channel requiring synchronization.
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from all input sequences in non-deterministic order
|
||||
//
|
||||
// Behavior:
|
||||
// - Spawns one goroutine per input sequence to produce elements concurrently
|
||||
// - Elements from different sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// all producer goroutines are signaled to stop and cleaned up
|
||||
// - The output channel is closed when all input sequences are exhausted
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // MergeBuf three sequences concurrently
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// seq3 := From(7, 8, 9)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2, seq3}, 10)
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// seq1 := From(1, 2, 3, 4, 5)
|
||||
// seq2 := From(6, 7, 8, 9, 10)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2}, 5)
|
||||
//
|
||||
// // Stop after 3 elements - all producer goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 3 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Unbuffered Channel:
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2}, 0)
|
||||
//
|
||||
// // Producers and consumer are synchronized
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
// - From: Creates a sequence from values
|
||||
// - MonadChain: Sequentially chains sequences (deterministic order)
|
||||
func MergeBuf[T any](iterables []Seq[T], bufSize int) Seq[T] {
|
||||
return F.Pipe2(
|
||||
iterables,
|
||||
slices.Values,
|
||||
MergeAll[T](bufSize),
|
||||
)
|
||||
}
|
||||
|
||||
// Merge merges multiple sequences concurrently into a single sequence using a default buffer size.
|
||||
// This is a convenience wrapper around MergeBuf that uses a default buffer size of 8.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - iterables: A slice of sequences to merge. If empty, returns an empty sequence.
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from all input sequences in non-deterministic order
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns one goroutine per input sequence to produce elements concurrently
|
||||
// - Elements from different sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// seq3 := From(7, 8, 9)
|
||||
// merged := Merge([]Seq[int]{seq1, seq2, seq3})
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - MergeBuf: Merge with custom buffer size
|
||||
// - MergeAll: Merges a sequence of sequences
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func Merge[T any](iterables []Seq[T]) Seq[T] {
|
||||
return MergeBuf(iterables, defaultBufferSize)
|
||||
}
|
||||
|
||||
// MergeMonoid creates a Monoid for merging sequences concurrently.
|
||||
// The monoid combines two sequences by merging them concurrently with the specified
|
||||
// buffer size, and uses an empty sequence as the identity element.
|
||||
//
|
||||
// A Monoid is an algebraic structure with an associative binary operation (concat)
|
||||
// and an identity element (empty). For sequences, the concat operation merges two
|
||||
// sequences concurrently, and the identity is an empty sequence.
|
||||
//
|
||||
// This is useful for functional composition patterns where you need to combine
|
||||
// multiple sequences using monoid operations like Reduce, FoldMap, or when working
|
||||
// with monadic operations that require a monoid instance.
|
||||
//
|
||||
// Marble Diagram (Concurrent Merging):
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Merge: --1-4-2-5-3-6--|
|
||||
// (non-deterministic order)
|
||||
//
|
||||
// Marble Diagram (vs ConcatMonoid):
|
||||
//
|
||||
// MergeMonoid (concurrent):
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1-4-2-5-3-6--|
|
||||
// (elements interleaved)
|
||||
//
|
||||
// ConcatMonoid (sequential):
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1--2--3--4--5--6--|
|
||||
// (deterministic order)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - bufSize: The buffer size for the internal channel used during merging.
|
||||
// This buffer size will be used for all merge operations performed by the monoid.
|
||||
// Negative values are treated as 0 (unbuffered).
|
||||
//
|
||||
// Returns:
|
||||
// - Monoid[Seq[T]]: A monoid instance with:
|
||||
// - Concat: Merges two sequences concurrently using Merge
|
||||
// - Empty: Returns an empty sequence
|
||||
//
|
||||
// Properties:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(a, b), c) = concat(a, concat(b, c))
|
||||
// Note: Due to concurrent execution, element order may vary between equivalent expressions
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a monoid for merging integer sequences
|
||||
// monoid := MergeMonoid[int](10)
|
||||
//
|
||||
// // Use with Reduce to merge multiple sequences
|
||||
// sequences := []Seq[int]{
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// }
|
||||
// merged := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty)
|
||||
// // merged contains all elements from all sequences (order non-deterministic)
|
||||
//
|
||||
// Example with Empty Identity:
|
||||
//
|
||||
// monoid := MergeMonoid[int](5)
|
||||
// seq := From(1, 2, 3)
|
||||
//
|
||||
// // Merging with empty is identity
|
||||
// result1 := monoid.Concat(monoid.Empty, seq) // same as seq
|
||||
// result2 := monoid.Concat(seq, monoid.Empty) // same as seq
|
||||
//
|
||||
// Example with FoldMap:
|
||||
//
|
||||
// // Convert each number to a sequence and merge all results
|
||||
// monoid := MergeMonoid[int](10)
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, monoid)
|
||||
// // result contains: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
//
|
||||
// See Also:
|
||||
// - Merge: The underlying merge function
|
||||
// - MergeAll: Merges multiple sequences at once
|
||||
// - Empty: Creates an empty sequence
|
||||
func MergeMonoid[T any](bufSize int) M.Monoid[Seq[T]] {
|
||||
return M.MakeMonoid(
|
||||
func(l, r Seq[T]) Seq[T] {
|
||||
return MergeBuf(A.From(l, r), bufSize)
|
||||
},
|
||||
Empty[T](),
|
||||
)
|
||||
}
|
||||
|
||||
// MergeAll creates an operator that flattens and merges a sequence of sequences concurrently.
|
||||
// It takes a sequence of sequences (Seq[Seq[T]]) and produces a single flat sequence (Seq[T])
|
||||
// by spawning a goroutine for each inner sequence as it arrives, merging all their elements
|
||||
// through a buffered channel. This enables dynamic concurrent processing where inner sequences
|
||||
// can be produced and consumed concurrently.
|
||||
//
|
||||
// Unlike Merge which takes a pre-defined slice of sequences, MergeAll processes sequences
|
||||
// dynamically as they are produced by the outer sequence. This makes it ideal for scenarios
|
||||
// where the number of sequences isn't known upfront or where sequences are generated on-the-fly.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the inner sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[Seq[T], T]: A function that takes a sequence of sequences and returns a flat sequence
|
||||
//
|
||||
// Behavior:
|
||||
// - Spawns one goroutine for the outer sequence to iterate and spawn inner producers
|
||||
// - Spawns one goroutine per inner sequence as it arrives from the outer sequence
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating, all goroutines are cleaned up
|
||||
// - The output channel is closed when both the outer sequence and all inner sequences are exhausted
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a sequence of sequences dynamically
|
||||
// outer := From(
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// )
|
||||
// mergeAll := MergeAll[int](10)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// Example with Dynamic Generation:
|
||||
//
|
||||
// // Generate sequences on-the-fly
|
||||
// outer := Map(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// })(From(1, 2, 3))
|
||||
// mergeAll := MergeAll[int](10)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// outer := From(
|
||||
// From(1, 2, 3, 4, 5),
|
||||
// From(6, 7, 8, 9, 10),
|
||||
// From(11, 12, 13, 14, 15),
|
||||
// )
|
||||
// mergeAll := MergeAll[int](5)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Stop after 5 elements - all goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 5 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Chain:
|
||||
//
|
||||
// // Use with Chain to flatten nested sequences
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := Chain(func(n int) Seq[int] {
|
||||
// return From(n, n*10)
|
||||
// })(numbers)
|
||||
// // This is equivalent to: MergeAll[int](0)(Map(...)(numbers))
|
||||
//
|
||||
// See Also:
|
||||
// - Merge: Merges a pre-defined slice of sequences
|
||||
// - Chain: Sequentially flattens sequences (deterministic order)
|
||||
// - Flatten: Flattens nested sequences sequentially
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func MergeAll[T any](bufSize int) Operator[Seq[T], T] {
|
||||
buf := N.Max(bufSize, 0)
|
||||
|
||||
return func(s Seq[Seq[T]]) Seq[T] {
|
||||
|
||||
return func(yield func(T) bool) {
|
||||
|
||||
ch := make(chan T, buf)
|
||||
done := make(chan Void)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Outer producer: iterates the outer Seq and spawns an inner
|
||||
// goroutine for each inner Seq it emits.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s(func(inner Seq[T]) bool {
|
||||
select {
|
||||
case <-done:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(seq Seq[T]) {
|
||||
defer wg.Done()
|
||||
seq(func(v T) bool {
|
||||
select {
|
||||
case ch <- v:
|
||||
return true
|
||||
case <-done:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}(inner)
|
||||
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
// Close ch once the outer producer and all inner producers finish.
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
// On exit, signal cancellation and drain so no producer blocks
|
||||
// forever on `ch <- v`.
|
||||
defer func() {
|
||||
close(done)
|
||||
for range ch {
|
||||
}
|
||||
}()
|
||||
|
||||
for v := range ch {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MergeMapBuf applies a function that returns a sequence to each element and merges the results concurrently.
|
||||
// This is the concurrent version of Chain (flatMap), where each mapped sequence is processed in parallel
|
||||
// rather than sequentially. It combines Map and MergeAll into a single operation.
|
||||
//
|
||||
// Unlike Chain which processes sequences sequentially (deterministic order), MergeMapBuf spawns a goroutine
|
||||
// for each mapped sequence and merges their elements concurrently through a buffered channel. This makes
|
||||
// it ideal for I/O-bound operations, parallel data processing, or when the order of results doesn't matter.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input sequence
|
||||
// - B: The type of elements in the output sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms each input element into a sequence of output elements
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that takes a sequence of A and returns a flat sequence of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Applies f to each element in the input sequence to produce inner sequences
|
||||
// - Spawns one goroutine per inner sequence to produce elements concurrently
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating, all goroutines are cleaned up
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Comparison with Chain:
|
||||
// - Chain: Sequential processing, deterministic order, no concurrency overhead
|
||||
// - MergeMapBuf: Concurrent processing, non-deterministic order, better for I/O-bound tasks
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Expand each number into a sequence concurrently
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, 10)
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// Example with I/O Operations:
|
||||
//
|
||||
// // Fetch data concurrently for each ID
|
||||
// fetchData := MergeMapBuf(func(id int) Seq[string] {
|
||||
// // Simulate I/O operation
|
||||
// data := fetchFromAPI(id)
|
||||
// return From(data...)
|
||||
// }, 20)
|
||||
// ids := From(1, 2, 3, 4, 5)
|
||||
// results := fetchData(ids)
|
||||
//
|
||||
// // All fetches happen concurrently
|
||||
// for data := range results {
|
||||
// fmt.Println(data)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, 5)
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Stop after 5 elements - all goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 5 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Unbuffered Channel:
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*2)
|
||||
// }, 0)
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Producers and consumer are synchronized
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - Chain: Sequential version (deterministic order)
|
||||
// - MergeAll: Merges pre-existing sequences concurrently
|
||||
// - Map: Transforms elements without flattening
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func MergeMapBuf[A, B any](f func(A) Seq[B], bufSize int) Operator[A, B] {
|
||||
return F.Flow2(
|
||||
Map(f),
|
||||
MergeAll[B](bufSize),
|
||||
)
|
||||
}
|
||||
|
||||
// MergeMap applies a function that returns a sequence to each element and merges the results concurrently using a default buffer size.
|
||||
// This is a convenience wrapper around MergeMapBuf that uses a default buffer size of 8.
|
||||
// It's the concurrent version of Chain (flatMap), where each mapped sequence is processed in parallel.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input sequence
|
||||
// - B: The type of elements in the output sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms each input element into a sequence of output elements
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that takes a sequence of A and returns a flat sequence of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Applies f to each element in the input sequence to produce inner sequences
|
||||
// - Spawns one goroutine per inner sequence to produce elements concurrently
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Comparison with Chain:
|
||||
// - Chain: Sequential processing, deterministic order, no concurrency overhead
|
||||
// - MergeMap: Concurrent processing, non-deterministic order, better for I/O-bound tasks
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Expand each number into a sequence concurrently
|
||||
// expand := MergeMap(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// })
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - MergeMapBuf: MergeMap with custom buffer size
|
||||
// - Chain: Sequential version (deterministic order)
|
||||
// - MergeAll: Merges pre-existing sequences concurrently
|
||||
// - Map: Transforms elements without flattening
|
||||
func MergeMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return MergeMapBuf(f, defaultBufferSize)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+134
-1
@@ -21,7 +21,13 @@ import (
|
||||
)
|
||||
|
||||
// Monoid returns a Monoid instance for Seq[T].
|
||||
// The monoid's concat operation concatenates sequences, and the empty value is an empty sequence.
|
||||
// The monoid's concat operation concatenates sequences sequentially, and the empty value is an empty sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Concat: --1--2--3--4--5--6--|
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -35,3 +41,130 @@ import (
|
||||
func Monoid[T any]() M.Monoid[Seq[T]] {
|
||||
return G.Monoid[Seq[T]]()
|
||||
}
|
||||
|
||||
// ConcatMonoid returns a Monoid instance for Seq[T] that concatenates sequences sequentially.
|
||||
// This is an alias for Monoid that makes the sequential concatenation behavior explicit.
|
||||
//
|
||||
// A Monoid is an algebraic structure with an associative binary operation (concat)
|
||||
// and an identity element (empty). For sequences, the concat operation appends one
|
||||
// sequence after another in deterministic order, and the identity is an empty sequence.
|
||||
//
|
||||
// This monoid is useful for functional composition patterns where you need to combine
|
||||
// multiple sequences sequentially using monoid operations like Reduce, FoldMap, or when
|
||||
// working with monadic operations that require a monoid instance.
|
||||
//
|
||||
// Marble Diagram (Sequential Concatenation):
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Concat: --1--2--3--4--5--6--|
|
||||
// (deterministic order)
|
||||
//
|
||||
// Marble Diagram (vs MergeMonoid):
|
||||
//
|
||||
// ConcatMonoid:
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1--2--3--4--5--6--|
|
||||
//
|
||||
// MergeMonoid:
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1-4-2-5-3-6--|
|
||||
// (non-deterministic)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Returns:
|
||||
// - Monoid[Seq[T]]: A monoid instance with:
|
||||
// - Concat: Appends sequences sequentially (deterministic order)
|
||||
// - Empty: Returns an empty sequence
|
||||
//
|
||||
// Properties:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(a, b), c) = concat(a, concat(b, c))
|
||||
// - Deterministic: Elements always appear in the order of the input sequences
|
||||
//
|
||||
// Comparison with MergeMonoid:
|
||||
//
|
||||
// ConcatMonoid and MergeMonoid serve different purposes:
|
||||
//
|
||||
// - ConcatMonoid: Sequential concatenation
|
||||
//
|
||||
// - Order: Deterministic - elements from first sequence, then second, etc.
|
||||
//
|
||||
// - Concurrency: No concurrency - sequences are processed one after another
|
||||
//
|
||||
// - Performance: Lower overhead, no goroutines or channels
|
||||
//
|
||||
// - Use when: Order matters, no I/O operations, or simplicity is preferred
|
||||
//
|
||||
// - MergeMonoid: Concurrent merging
|
||||
//
|
||||
// - Order: Non-deterministic - elements interleaved based on timing
|
||||
//
|
||||
// - Concurrency: Spawns goroutines for each sequence
|
||||
//
|
||||
// - Performance: Better for I/O-bound operations, higher overhead for CPU-bound
|
||||
//
|
||||
// - Use when: Order doesn't matter, parallel I/O, or concurrent processing needed
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a monoid for concatenating integer sequences
|
||||
// monoid := ConcatMonoid[int]()
|
||||
//
|
||||
// // Use with Reduce to concatenate multiple sequences
|
||||
// sequences := []Seq[int]{
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// }
|
||||
// concatenated := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty)
|
||||
// // yields: 1, 2, 3, 4, 5, 6, 7, 8, 9 (deterministic order)
|
||||
//
|
||||
// Example with Empty Identity:
|
||||
//
|
||||
// monoid := ConcatMonoid[int]()
|
||||
// seq := From(1, 2, 3)
|
||||
//
|
||||
// // Concatenating with empty is identity
|
||||
// result1 := monoid.Concat(monoid.Empty, seq) // same as seq
|
||||
// result2 := monoid.Concat(seq, monoid.Empty) // same as seq
|
||||
//
|
||||
// Example with FoldMap:
|
||||
//
|
||||
// // Convert each number to a sequence and concatenate all results
|
||||
// monoid := ConcatMonoid[int]()
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, monoid)
|
||||
// // yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (deterministic order)
|
||||
//
|
||||
// Example Comparing ConcatMonoid vs MergeMonoid:
|
||||
//
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
//
|
||||
// // ConcatMonoid: Sequential, deterministic
|
||||
// concatMonoid := ConcatMonoid[int]()
|
||||
// concat := concatMonoid.Concat(seq1, seq2)
|
||||
// // Always yields: 1, 2, 3, 4, 5, 6
|
||||
//
|
||||
// // MergeMonoid: Concurrent, non-deterministic
|
||||
// mergeMonoid := MergeMonoid[int](10)
|
||||
// merged := mergeMonoid.Concat(seq1, seq2)
|
||||
// // May yield: 1, 4, 2, 5, 3, 6 (order varies)
|
||||
//
|
||||
// See Also:
|
||||
// - Monoid: The base monoid function (alias)
|
||||
// - MergeMonoid: Concurrent merging monoid
|
||||
// - MonadChain: Sequential flattening of sequences
|
||||
// - Empty: Creates an empty sequence
|
||||
//
|
||||
//go:inline
|
||||
func ConcatMonoid[T any]() M.Monoid[Seq[T]] {
|
||||
return Monoid[T]()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// 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 iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConcatMonoid_Identity(t *testing.T) {
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
result := monoid.Concat(monoid.Empty(), seq)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
result := monoid.Concat(seq, monoid.Empty())
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_Associativity(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2)
|
||||
seq2 := From(3, 4)
|
||||
seq3 := From(5, 6)
|
||||
|
||||
// (a + b) + c
|
||||
left := monoid.Concat(monoid.Concat(seq1, seq2), seq3)
|
||||
leftResult := slices.Collect(left)
|
||||
|
||||
// a + (b + c)
|
||||
right := monoid.Concat(seq1, monoid.Concat(seq2, seq3))
|
||||
rightResult := slices.Collect(right)
|
||||
|
||||
assert.Equal(t, leftResult, rightResult)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, leftResult)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_DeterministicOrder(t *testing.T) {
|
||||
t.Run("concatenates in deterministic order", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
seq3 := From(7, 8, 9)
|
||||
|
||||
result := monoid.Concat(monoid.Concat(seq1, seq2), seq3)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Order is always deterministic
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, collected)
|
||||
})
|
||||
|
||||
t.Run("multiple runs produce same order", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
// Run multiple times
|
||||
results := make([][]int, 5)
|
||||
for i := range 5 {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
results[i] = slices.Collect(result)
|
||||
}
|
||||
|
||||
// All results should be identical
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
for i, result := range results {
|
||||
assert.Equal(t, expected, result, "run %d should match", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithReduce(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
}
|
||||
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, collected)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithFoldMap(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
numbers := From(1, 2, 3)
|
||||
|
||||
result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
return From(n, n*10, n*100)
|
||||
}, monoid)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Deterministic order: each number's expansion in sequence
|
||||
assert.Equal(t, []int{1, 10, 100, 2, 20, 200, 3, 30, 300}, collected)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_ComparisonWithMergeMonoid(t *testing.T) {
|
||||
t.Run("ConcatMonoid is deterministic", func(t *testing.T) {
|
||||
concatMonoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := concatMonoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Always the same order
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, collected)
|
||||
})
|
||||
|
||||
t.Run("MergeMonoid may be non-deterministic", func(t *testing.T) {
|
||||
mergeMonoid := MergeMonoid[int](10)
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := mergeMonoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Contains all elements but order may vary
|
||||
assert.ElementsMatch(t, []int{1, 2, 3, 4, 5, 6}, collected)
|
||||
// Note: We can't assert exact order as it's non-deterministic
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_EmptySequences(t *testing.T) {
|
||||
t.Run("concatenating empty sequences", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
empty1 := Empty[int]()
|
||||
empty2 := Empty[int]()
|
||||
|
||||
result := monoid.Concat(empty1, empty2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("concatenating with empty in middle", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2)
|
||||
empty := Empty[int]()
|
||||
seq2 := From(3, 4)
|
||||
|
||||
result := monoid.Concat(monoid.Concat(seq1, empty), seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
monoid := ConcatMonoid[Person]()
|
||||
seq1 := From(Person{"Alice", 30}, Person{"Bob", 25})
|
||||
seq2 := From(Person{"Charlie", 35}, Person{"Diana", 28})
|
||||
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Charlie", 35},
|
||||
{"Diana", 28},
|
||||
}
|
||||
assert.Equal(t, expected, collected)
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_TwoSequences(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3, 4, 5)
|
||||
seq2 := From(6, 7, 8, 9, 10)
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_Reduce(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
From(10, 11, 12),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_VsMergeMonoid(b *testing.B) {
|
||||
seq1 := From(1, 2, 3, 4, 5)
|
||||
seq2 := From(6, 7, 8, 9, 10)
|
||||
|
||||
b.Run("ConcatMonoid", func(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MergeMonoid", func(b *testing.B) {
|
||||
monoid := MergeMonoid[int](10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_identity() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
// Left identity
|
||||
result1 := monoid.Concat(monoid.Empty(), seq)
|
||||
for v := range result1 {
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
// Right identity
|
||||
result2 := monoid.Concat(seq, monoid.Empty())
|
||||
for v := range result2 {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_reduce() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
}
|
||||
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
// 7
|
||||
// 8
|
||||
// 9
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_comparison() {
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
// ConcatMonoid: Sequential, deterministic
|
||||
concatMonoid := ConcatMonoid[int]()
|
||||
concat := concatMonoid.Concat(seq1, seq2)
|
||||
fmt.Println("ConcatMonoid (always same order):")
|
||||
for v := range concat {
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
// MergeMonoid: Concurrent, non-deterministic
|
||||
// Note: Output order may vary in actual runs
|
||||
mergeMonoid := MergeMonoid[int](10)
|
||||
merged := mergeMonoid.Concat(seq1, seq2)
|
||||
fmt.Println("\nMergeMonoid (order may vary):")
|
||||
collected := slices.Collect(merged)
|
||||
// Sort for consistent test output
|
||||
slices.Sort(collected)
|
||||
for _, v := range collected {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// ConcatMonoid (always same order):
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
//
|
||||
// MergeMonoid (order may vary):
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
@@ -28,6 +28,13 @@ import (
|
||||
//
|
||||
// This is the monadic form that takes the sequence as the first parameter.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// ChainOptionK(x => x % 2 == 0 ? Some(x * 10) : None)
|
||||
// Output: -----20----40---->
|
||||
// (filters and transforms)
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
@@ -72,6 +79,13 @@ func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
|
||||
// This is the curried version of [MonadChainOptionK], useful for function composition
|
||||
// and creating reusable transformations.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// ChainOptionK(x => x > 2 ? Some(x) : None)
|
||||
// Output: --------3--4--5-->
|
||||
// (filters out values <= 2)
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
|
||||
@@ -24,6 +24,13 @@ package iter
|
||||
//
|
||||
// The operation is lazy - intermediate values are computed only as they are consumed.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// Scan((acc, x) => acc + x, 0)
|
||||
// Output: --1--3--6--10-15->
|
||||
// (running sum)
|
||||
//
|
||||
// RxJS Equivalent: [scan] - https://rxjs.dev/api/operators/scan
|
||||
//
|
||||
// Scan is useful for:
|
||||
|
||||
@@ -27,6 +27,12 @@ import F "github.com/IBM/fp-go/v2/function"
|
||||
// Once n elements have been yielded, iteration stops immediately without consuming
|
||||
// the remaining elements from the source.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--6--7--8-->
|
||||
// Take(3)
|
||||
// Output: --1--2--3|
|
||||
//
|
||||
// RxJS Equivalent: [take] - https://rxjs.dev/api/operators/take
|
||||
//
|
||||
// Type Parameters:
|
||||
@@ -78,3 +84,158 @@ func Take[U any](n int) Operator[U, U] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TakeWhile returns an operator that emits elements from a sequence while a predicate is satisfied.
|
||||
//
|
||||
// This function creates a transformation that yields elements from the source sequence
|
||||
// as long as each element satisfies the provided predicate. Once an element fails the
|
||||
// predicate test, the sequence terminates immediately, and no further elements are
|
||||
// emitted, even if subsequent elements would satisfy the predicate.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// Once the predicate returns false, iteration stops immediately without consuming
|
||||
// the remaining elements from the source.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--2--1-->
|
||||
// TakeWhile(x < 4)
|
||||
// Output: --1--2--3|
|
||||
// (stops at 4)
|
||||
//
|
||||
// RxJS Equivalent: [takeWhile] - https://rxjs.dev/api/operators/takeWhile
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests each element. Returns true to continue, false to stop
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by taking elements while the predicate is satisfied
|
||||
//
|
||||
// Example - Take while less than threshold:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
// result := TakeWhile(func(x int) bool { return x < 4 })(seq)
|
||||
// // yields: 1, 2, 3 (stops at 4, doesn't continue to 2, 1)
|
||||
//
|
||||
// Example - Take while condition is met:
|
||||
//
|
||||
// seq := From("a", "b", "c", "1", "d", "e")
|
||||
// isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
// result := TakeWhile(isLetter)(seq)
|
||||
// // yields: "a", "b", "c" (stops at "1")
|
||||
//
|
||||
// Example - Take all when predicate always true:
|
||||
//
|
||||
// seq := From(2, 4, 6, 8)
|
||||
// result := TakeWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
// // yields: 2, 4, 6, 8 (all elements satisfy predicate)
|
||||
//
|
||||
// Example - Take none when first element fails:
|
||||
//
|
||||
// seq := From(5, 1, 2, 3)
|
||||
// result := TakeWhile(func(x int) bool { return x < 5 })(seq)
|
||||
// // yields: nothing (first element fails predicate)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// result := F.Pipe2(
|
||||
// seq,
|
||||
// MonadMap(seq, func(x int) int { return x * 2 }),
|
||||
// TakeWhile(func(x int) bool { return x < 10 }),
|
||||
// )
|
||||
// // yields: 2, 4, 6, 8 (stops when doubled value reaches 10)
|
||||
func TakeWhile[U any](p Predicate[U]) Operator[U, U] {
|
||||
return func(s Seq[U]) Seq[U] {
|
||||
return func(yield func(U) bool) {
|
||||
for u := range s {
|
||||
if !p(u) || !yield(u) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SkipWhile returns an operator that skips elements from a sequence while a predicate is satisfied.
|
||||
//
|
||||
// This function creates a transformation that discards elements from the source sequence
|
||||
// as long as each element satisfies the provided predicate. Once an element fails the
|
||||
// predicate test, that element and all subsequent elements are yielded, regardless of
|
||||
// whether they satisfy the predicate.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// Once the predicate returns false, all remaining elements are yielded without further
|
||||
// predicate evaluation.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--2--1-->
|
||||
// SkipWhile(x < 4)
|
||||
// Output: -----------4--5--2--1-->
|
||||
// (starts at 4, continues with all)
|
||||
//
|
||||
// RxJS Equivalent: [skipWhile] - https://rxjs.dev/api/operators/skipWhile
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests each element. Returns true to skip, false to start yielding
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by skipping elements while the predicate is satisfied
|
||||
//
|
||||
// Example - Skip while less than threshold:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
// result := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
// // yields: 4, 5, 2, 1 (starts at 4, continues with all remaining)
|
||||
//
|
||||
// Example - Skip while condition is met:
|
||||
//
|
||||
// seq := From("a", "b", "c", "1", "d", "e")
|
||||
// isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
// result := SkipWhile(isLetter)(seq)
|
||||
// // yields: "1", "d", "e" (starts at "1", continues with all remaining)
|
||||
//
|
||||
// Example - Skip none when first element fails:
|
||||
//
|
||||
// seq := From(5, 1, 2, 3)
|
||||
// result := SkipWhile(func(x int) bool { return x < 5 })(seq)
|
||||
// // yields: 5, 1, 2, 3 (first element fails predicate, all yielded)
|
||||
//
|
||||
// Example - Skip all when predicate always true:
|
||||
//
|
||||
// seq := From(2, 4, 6, 8)
|
||||
// result := SkipWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
// // yields: nothing (all elements satisfy predicate)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// result := F.Pipe2(
|
||||
// seq,
|
||||
// SkipWhile(func(x int) bool { return x < 5 }),
|
||||
// MonadMap(seq, func(x int) int { return x * 2 }),
|
||||
// )
|
||||
// // yields: 10, 12, 14, 16, 18, 20 (skip until 5, then double remaining)
|
||||
func SkipWhile[U any](p Predicate[U]) Operator[U, U] {
|
||||
return func(s Seq[U]) Seq[U] {
|
||||
return func(yield func(U) bool) {
|
||||
skipping := true
|
||||
for u := range s {
|
||||
if skipping && p(u) {
|
||||
continue
|
||||
}
|
||||
skipping = false
|
||||
if !yield(u) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,3 +461,831 @@ func ExampleTake_chained() {
|
||||
}
|
||||
// Output: 4 5 6 7 8
|
||||
}
|
||||
|
||||
// TestSkipWhile tests basic SkipWhile functionality
|
||||
func TestSkipWhile(t *testing.T) {
|
||||
t.Run("skips while predicate is true", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{4, 5, 2, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("skips none when first element fails", func(t *testing.T) {
|
||||
seq := From(5, 1, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 5 })(seq))
|
||||
assert.Equal(t, []int{5, 1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all when predicate always true", func(t *testing.T) {
|
||||
seq := From(2, 4, 6, 8)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x%2 == 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
result := toSlice(SkipWhile(isLetter)(seq))
|
||||
assert.Equal(t, []string{"1", "d", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("continues after predicate fails", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 1, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{4, 1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("skips single element", func(t *testing.T) {
|
||||
seq := From(1, 10, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{10, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileEmpty tests SkipWhile with empty sequences
|
||||
func TestSkipWhileEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when predicate always satisfied", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithComplexTypes tests SkipWhile with complex data types
|
||||
func TestSkipWhileWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("skips structs while condition met", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 25},
|
||||
Person{"Bob", 30},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(SkipWhile(func(p Person) bool { return p.Age < 35 })(seq))
|
||||
expected := []Person{
|
||||
{"Charlie", 35},
|
||||
{"David", 28},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("skips pointers while condition met", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 25}
|
||||
p2 := &Person{"Bob", 30}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
p4 := &Person{"David", 28}
|
||||
seq := From(p1, p2, p3, p4)
|
||||
result := toSlice(SkipWhile(func(p *Person) bool { return p.Age < 35 })(seq))
|
||||
assert.Equal(t, []*Person{p3, p4}, result)
|
||||
})
|
||||
|
||||
t.Run("skips slices while condition met", func(t *testing.T) {
|
||||
seq := From([]int{1}, []int{1, 2}, []int{1, 2, 3}, []int{1})
|
||||
result := toSlice(SkipWhile(func(s []int) bool { return len(s) < 3 })(seq))
|
||||
expected := [][]int{{1, 2, 3}, {1}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithChainedOperations tests SkipWhile with other sequence operations
|
||||
func TestSkipWhileWithChainedOperations(t *testing.T) {
|
||||
t.Run("skipWhile after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 8 })(mapped))
|
||||
assert.Equal(t, []int{8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile after filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 6 })(filtered))
|
||||
assert.Equal(t, []int{6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("map after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
result := toSlice(MonadMap(skipped, N.Mul(10)))
|
||||
assert.Equal(t, []int{40, 50}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
result := toSlice(MonadFilter(skipped, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 20 })(chained))
|
||||
assert.Equal(t, []int{20, 3, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("skip after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped1 := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
skipped2 := Skip[int](2)(skipped1)
|
||||
result := toSlice(skipped2)
|
||||
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile after skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped := Skip[int](3)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 7 })(skipped))
|
||||
assert.Equal(t, []int{7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
taken := TakeWhile(func(x int) bool { return x < 8 })(skipped)
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{4, 5, 6, 7}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken := TakeWhile(func(x int) bool { return x < 8 })(seq)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(taken)
|
||||
result := toSlice(skipped)
|
||||
assert.Equal(t, []int{4, 5, 6, 7}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithReplicate tests SkipWhile with Replicate
|
||||
func TestSkipWhileWithReplicate(t *testing.T) {
|
||||
t.Run("skips all from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x == 5 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips none when predicate fails on replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 10)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{10, 10, 10, 10, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithMakeBy tests SkipWhile with MakeBy
|
||||
func TestSkipWhileWithMakeBy(t *testing.T) {
|
||||
t.Run("skips from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(10, func(i int) int { return i * i })
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 25 })(seq))
|
||||
assert.Equal(t, []int{25, 36, 49, 64, 81}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i + 1 })
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 100 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithPrependAppend tests SkipWhile with Prepend and Append
|
||||
func TestSkipWhileWithPrependAppend(t *testing.T) {
|
||||
t.Run("skipWhile from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(prepended))
|
||||
assert.Equal(t, []int{4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(10)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(appended))
|
||||
assert.Equal(t, []int{10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 3 })(appended))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithFlatten tests SkipWhile with Flatten
|
||||
func TestSkipWhileWithFlatten(t *testing.T) {
|
||||
t.Run("skips from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5, 6))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(flattened))
|
||||
assert.Equal(t, []int{4, 5, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("skips from flattened with empty inner sequences", func(t *testing.T) {
|
||||
nested := From(From(1, 2), Empty[int](), From(3, 4))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 3 })(flattened))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileDoesNotConsumeEntireSequence tests that SkipWhile is lazy
|
||||
func TestSkipWhileDoesNotConsumeEntireSequence(t *testing.T) {
|
||||
t.Run("only consumes needed elements", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})
|
||||
|
||||
skipped := SkipWhile(func(x int) bool { return x < 8 })(seq)
|
||||
|
||||
result := []int{}
|
||||
for v := range skipped {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{8, 10, 12, 14, 16, 18, 20}, result)
|
||||
// Should process all elements since we iterate through all remaining
|
||||
assert.Equal(t, 10, callCount, "should process all elements")
|
||||
})
|
||||
|
||||
t.Run("stops early when consumer stops", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool {
|
||||
callCount++
|
||||
return x%2 == 0
|
||||
})
|
||||
|
||||
skipped := SkipWhile(func(x int) bool { return x < 6 })(filtered)
|
||||
|
||||
result := []int{}
|
||||
count := 0
|
||||
for v := range skipped {
|
||||
result = append(result, v)
|
||||
count++
|
||||
if count == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{6, 8}, result)
|
||||
// Should stop after getting 2 elements
|
||||
assert.LessOrEqual(t, callCount, 9, "should not consume all elements")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileEdgeCases tests edge cases
|
||||
func TestSkipWhileEdgeCases(t *testing.T) {
|
||||
t.Run("skipWhile with always false predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return false })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile with always true predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return true })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile from single element that passes", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile from single element that fails", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 0 })(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile with complex predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(SkipWhile(func(x int) bool {
|
||||
return x%2 == 1 || x < 5
|
||||
})(seq))
|
||||
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile yields elements that satisfy predicate after first failure", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 10, 1, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{10, 1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for SkipWhile
|
||||
func BenchmarkSkipWhile(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := SkipWhile(func(x int) bool { return x < 6 })(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWhileLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := SkipWhile(func(x int) bool { return x < 100 })(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWhileWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
skipped := SkipWhile(func(x int) bool { return x < 12 })(mapped)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWhileWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
skipped := SkipWhile(func(x int) bool { return x < 6 })(filtered)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleSkipWhile() {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 4 5 2 1
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_allSatisfy() {
|
||||
seq := From(2, 4, 6, 8)
|
||||
skipped := SkipWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
|
||||
count := 0
|
||||
for range skipped {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_firstFails() {
|
||||
seq := From(5, 1, 2, 3)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 5 })(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 5 1 2 3
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
skipped := SkipWhile(func(x int) bool { return x < 8 })(doubled)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 8 10
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_strings() {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
skipped := SkipWhile(isLetter)(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: 1 d e
|
||||
}
|
||||
|
||||
// TestTakeWhile tests basic TakeWhile functionality
|
||||
func TestTakeWhile(t *testing.T) {
|
||||
t.Run("takes while predicate is true", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all when predicate always true", func(t *testing.T) {
|
||||
seq := From(2, 4, 6, 8)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x%2 == 0 })(seq))
|
||||
assert.Equal(t, []int{2, 4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("takes none when first element fails", func(t *testing.T) {
|
||||
seq := From(5, 1, 2, 3)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("takes from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
result := toSlice(TakeWhile(isLetter)(seq))
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
|
||||
t.Run("takes single element", func(t *testing.T) {
|
||||
seq := From(1, 10, 2, 3)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{1}, result)
|
||||
})
|
||||
|
||||
t.Run("stops at first false predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 1, 2, 3)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileEmpty tests TakeWhile with empty sequences
|
||||
func TestTakeWhileEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when predicate never satisfied", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithComplexTypes tests TakeWhile with complex data types
|
||||
func TestTakeWhileWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("takes structs while condition met", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 25},
|
||||
Person{"Bob", 30},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(TakeWhile(func(p Person) bool { return p.Age < 35 })(seq))
|
||||
expected := []Person{
|
||||
{"Alice", 25},
|
||||
{"Bob", 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("takes pointers while condition met", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 25}
|
||||
p2 := &Person{"Bob", 30}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
result := toSlice(TakeWhile(func(p *Person) bool { return p.Age < 35 })(seq))
|
||||
assert.Equal(t, []*Person{p1, p2}, result)
|
||||
})
|
||||
|
||||
t.Run("takes slices while condition met", func(t *testing.T) {
|
||||
seq := From([]int{1}, []int{1, 2}, []int{1, 2, 3}, []int{1})
|
||||
result := toSlice(TakeWhile(func(s []int) bool { return len(s) < 3 })(seq))
|
||||
expected := [][]int{{1}, {1, 2}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithChainedOperations tests TakeWhile with other sequence operations
|
||||
func TestTakeWhileWithChainedOperations(t *testing.T) {
|
||||
t.Run("takeWhile after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 8 })(mapped))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile after filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 7 })(filtered))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("map after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := TakeWhile(func(x int) bool { return x < 4 })(seq)
|
||||
result := toSlice(MonadMap(taken, N.Mul(10)))
|
||||
assert.Equal(t, []int{10, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
taken := TakeWhile(func(x int) bool { return x < 7 })(seq)
|
||||
result := toSlice(MonadFilter(taken, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 20 })(chained))
|
||||
assert.Equal(t, []int{1, 10, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("take after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken1 := TakeWhile(func(x int) bool { return x < 8 })(seq)
|
||||
taken2 := Take[int](3)(taken1)
|
||||
result := toSlice(taken2)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile after take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken := Take[int](7)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(taken))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithReplicate tests TakeWhile with Replicate
|
||||
func TestTakeWhileWithReplicate(t *testing.T) {
|
||||
t.Run("takes from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 5)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x == 5 })(seq))
|
||||
assert.Equal(t, []int{5, 5, 5, 5, 5, 5, 5, 5, 5, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("takes none when predicate fails on replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 10)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithMakeBy tests TakeWhile with MakeBy
|
||||
func TestTakeWhileWithMakeBy(t *testing.T) {
|
||||
t.Run("takes from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(10, func(i int) int { return i * i })
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 25 })(seq))
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i + 1 })
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 100 })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithPrependAppend tests TakeWhile with Prepend and Append
|
||||
func TestTakeWhileWithPrependAppend(t *testing.T) {
|
||||
t.Run("takeWhile from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(prepended))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(10)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(appended))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(appended))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithFlatten tests TakeWhile with Flatten
|
||||
func TestTakeWhileWithFlatten(t *testing.T) {
|
||||
t.Run("takes from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5, 6))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(flattened))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from flattened with empty inner sequences", func(t *testing.T) {
|
||||
nested := From(From(1, 2), Empty[int](), From(3, 4))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(flattened))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileDoesNotConsumeEntireSequence tests that TakeWhile is lazy
|
||||
func TestTakeWhileDoesNotConsumeEntireSequence(t *testing.T) {
|
||||
t.Run("only consumes needed elements", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})
|
||||
|
||||
taken := TakeWhile(func(x int) bool { return x < 8 })(seq)
|
||||
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
// Should stop after finding element that fails predicate
|
||||
assert.LessOrEqual(t, callCount, 5, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 4, "should consume at least enough to find failure")
|
||||
})
|
||||
|
||||
t.Run("stops early with filter", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool {
|
||||
callCount++
|
||||
return x%2 == 0
|
||||
})
|
||||
|
||||
taken := TakeWhile(func(x int) bool { return x < 7 })(filtered)
|
||||
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
// Should stop after finding even number >= 7
|
||||
assert.LessOrEqual(t, callCount, 9, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 7, "should consume at least enough to find 8")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileEdgeCases tests edge cases
|
||||
func TestTakeWhileEdgeCases(t *testing.T) {
|
||||
t.Run("takeWhile with always false predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return false })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile with always true predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return true })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile from single element that passes", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile from single element that fails", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile with complex predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(TakeWhile(func(x int) bool {
|
||||
return x%2 == 1 || x < 5
|
||||
})(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for TakeWhile
|
||||
func BenchmarkTakeWhile(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
taken := TakeWhile(func(x int) bool { return x < 6 })(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWhileLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
taken := TakeWhile(func(x int) bool { return x < 100 })(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWhileWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
taken := TakeWhile(func(x int) bool { return x < 12 })(mapped)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWhileWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
taken := TakeWhile(func(x int) bool { return x < 7 })(filtered)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleTakeWhile() {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
taken := TakeWhile(func(x int) bool { return x < 4 })(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_allSatisfy() {
|
||||
seq := From(2, 4, 6, 8)
|
||||
taken := TakeWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_firstFails() {
|
||||
seq := From(5, 1, 2, 3)
|
||||
taken := TakeWhile(func(x int) bool { return x < 5 })(seq)
|
||||
|
||||
count := 0
|
||||
for range taken {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
taken := TakeWhile(func(x int) bool { return x < 8 })(doubled)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_strings() {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
taken := TakeWhile(isLetter)(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: a b c
|
||||
}
|
||||
|
||||
@@ -32,6 +32,13 @@ import (
|
||||
// the number of unique keys encountered. The operation is lazy - elements are processed
|
||||
// and filtered as they are consumed.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--2--4--1--5-->
|
||||
// Uniq(identity)
|
||||
// Output: --1--2--3-----4-----5-->
|
||||
// (first occurrence only)
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
@@ -119,6 +126,13 @@ func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
|
||||
// The operation maintains a map of seen elements internally, so memory usage grows with
|
||||
// the number of unique elements. Only the first occurrence of each unique element is kept.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--2--4--1--5-->
|
||||
// StrictUniq
|
||||
// Output: --1--2--3-----4-----5-->
|
||||
// (first occurrence only)
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
+2
-8
@@ -115,10 +115,7 @@ func Inc[T Number](value T) T {
|
||||
// result := Min(5, 10) // returns 5
|
||||
// result := Min(3.14, 2.71) // returns 2.71
|
||||
func Min[A C.Ordered](a, b A) A {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
return min(a, b)
|
||||
}
|
||||
|
||||
// Max returns the maximum of two ordered values.
|
||||
@@ -132,10 +129,7 @@ func Min[A C.Ordered](a, b A) A {
|
||||
// result := Max(5, 10) // returns 10
|
||||
// result := Max(3.14, 2.71) // returns 3.14
|
||||
func Max[A C.Ordered](a, b A) A {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
return max(a, b)
|
||||
}
|
||||
|
||||
// MoreThan is a curried comparison function that checks if a value is more than (greater than) another.
|
||||
|
||||
@@ -522,3 +522,199 @@ func MarshalJSON[T any](
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// FromNonZero creates a bidirectional codec for non-zero values of comparable types.
|
||||
// This codec validates that values are not equal to their zero value (e.g., 0 for int,
|
||||
// "" for string, false for bool, nil for pointers).
|
||||
//
|
||||
// The codec uses a refinement (prism) that:
|
||||
// - Decodes: Validates that the input is not the zero value of type T
|
||||
// - Encodes: Returns the value unchanged (identity function)
|
||||
// - Validates: Ensures the value is non-zero/non-default
|
||||
//
|
||||
// This is useful for enforcing that required fields have meaningful values rather than
|
||||
// their default zero values, which often represent "not set" or "missing" states.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: A comparable type (must support == and != operators)
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[T, T, T] codec that validates non-zero values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for non-zero integers
|
||||
// nonZeroInt := FromNonZero[int]()
|
||||
//
|
||||
// // Decode non-zero value succeeds
|
||||
// result := nonZeroInt.Decode(42)
|
||||
// // result is Right(42)
|
||||
//
|
||||
// // Decode zero value fails
|
||||
// result := nonZeroInt.Decode(0)
|
||||
// // result is Left(ValidationError{...})
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := nonZeroInt.Encode(42)
|
||||
// // encoded is 42
|
||||
//
|
||||
// // Works with strings
|
||||
// nonEmptyStr := FromNonZero[string]()
|
||||
// result := nonEmptyStr.Decode("hello") // Right("hello")
|
||||
// result = nonEmptyStr.Decode("") // Left(ValidationError{...})
|
||||
//
|
||||
// // Works with pointers
|
||||
// nonNilPtr := FromNonZero[*int]()
|
||||
// value := 42
|
||||
// result := nonNilPtr.Decode(&value) // Right(&value)
|
||||
// result = nonNilPtr.Decode(nil) // Left(ValidationError{...})
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating required numeric fields are not zero
|
||||
// - Ensuring string fields are not empty
|
||||
// - Checking pointers are not nil
|
||||
// - Validating boolean flags are explicitly set to true
|
||||
// - Composing with other codecs for multi-stage validation
|
||||
//
|
||||
// See Also:
|
||||
// - NonEmptyString: Specialized version for strings with clearer intent
|
||||
// - FromRefinement: General function for creating codecs from prisms
|
||||
func FromNonZero[T comparable]() Type[T, T, T] {
|
||||
return FromRefinement(prism.FromNonZero[T]())
|
||||
}
|
||||
|
||||
// NonEmptyString creates a bidirectional codec for non-empty strings.
|
||||
// This codec validates that string values are not empty, providing a type-safe
|
||||
// way to work with strings that must contain at least one character.
|
||||
//
|
||||
// This is a specialized version of FromNonZero[string]() that makes the intent
|
||||
// clearer when working specifically with strings that must not be empty.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Validates that the input string is not empty ("")
|
||||
// - Encodes: Returns the string unchanged (identity function)
|
||||
// - Validates: Ensures the string has length > 0
|
||||
//
|
||||
// Note: This codec only checks for empty strings, not whitespace-only strings.
|
||||
// A string containing only spaces, tabs, or newlines will pass validation.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[string, string, string] codec that validates non-empty strings
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nonEmpty := NonEmptyString()
|
||||
//
|
||||
// // Decode non-empty string succeeds
|
||||
// result := nonEmpty.Decode("hello")
|
||||
// // result is Right("hello")
|
||||
//
|
||||
// // Decode empty string fails
|
||||
// result := nonEmpty.Decode("")
|
||||
// // result is Left(ValidationError{...})
|
||||
//
|
||||
// // Whitespace-only strings pass validation
|
||||
// result := nonEmpty.Decode(" ")
|
||||
// // result is Right(" ")
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := nonEmpty.Encode("world")
|
||||
// // encoded is "world"
|
||||
//
|
||||
// // Compose with other codecs for validation pipelines
|
||||
// intFromNonEmptyString := Pipe(IntFromString())(nonEmpty)
|
||||
// result := intFromNonEmptyString.Decode("42") // Right(42)
|
||||
// result = intFromNonEmptyString.Decode("") // Left(ValidationError{...})
|
||||
// result = intFromNonEmptyString.Decode("abc") // Left(ValidationError{...})
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating required string fields (usernames, names, IDs)
|
||||
// - Ensuring configuration values are provided
|
||||
// - Validating user input before processing
|
||||
// - Composing with parsing codecs to validate before parsing
|
||||
// - Building validation pipelines for string data
|
||||
//
|
||||
// See Also:
|
||||
// - FromNonZero: General version for any comparable type
|
||||
// - String: Basic string codec without validation
|
||||
// - IntFromString: Codec for parsing integers from strings
|
||||
func NonEmptyString() Type[string, string, string] {
|
||||
return F.Pipe1(
|
||||
FromRefinement(prism.NonEmptyString()),
|
||||
WithName[string, string, string]("NonEmptyString"),
|
||||
)
|
||||
}
|
||||
|
||||
// WithName creates an endomorphism that renames a codec without changing its behavior.
|
||||
// This function returns a higher-order function that takes a codec and returns a new codec
|
||||
// with the specified name, while preserving all validation, encoding, and type-checking logic.
|
||||
//
|
||||
// This is useful for:
|
||||
// - Providing more descriptive names for composed codecs
|
||||
// - Creating domain-specific codec names for better error messages
|
||||
// - Documenting the purpose of complex codec pipelines
|
||||
// - Improving debugging and logging output
|
||||
//
|
||||
// The renamed codec maintains the same:
|
||||
// - Type checking behavior (Is function)
|
||||
// - Validation logic (Validate function)
|
||||
// - Encoding behavior (Encode function)
|
||||
//
|
||||
// Only the name returned by the Name() method changes.
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The new name for the codec
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism[Type[A, O, I]] that renames the codec
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec with a generic name
|
||||
// positiveInt := Pipe[int, int, string, int](
|
||||
// FromRefinement(prism.FromPredicate(func(n int) bool { return n > 0 })),
|
||||
// )(IntFromString())
|
||||
// // positiveInt.Name() returns something like "Pipe(FromRefinement(...), IntFromString)"
|
||||
//
|
||||
// // Rename it for clarity
|
||||
// namedCodec := WithName[int, string, string]("PositiveIntFromString")(positiveInt)
|
||||
// // namedCodec.Name() returns "PositiveIntFromString"
|
||||
//
|
||||
// // Use in a pipeline with F.Pipe
|
||||
// userAgeCodec := F.Pipe1(
|
||||
// IntFromString(),
|
||||
// WithName[int, string, string]("UserAge"),
|
||||
// )
|
||||
//
|
||||
// // Validation errors will show the custom name
|
||||
// result := userAgeCodec.Decode("invalid")
|
||||
// // Error context will reference "UserAge" instead of "IntFromString"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Naming composed codecs for better error messages
|
||||
// - Creating domain-specific codec names (e.g., "EmailAddress", "PhoneNumber")
|
||||
// - Documenting complex validation pipelines
|
||||
// - Improving debugging output in logs
|
||||
// - Making codec composition more readable
|
||||
//
|
||||
// Note: This function creates a new codec instance with the same behavior but a different
|
||||
// name. The original codec is not modified.
|
||||
//
|
||||
// See Also:
|
||||
// - MakeType: For creating codecs with custom names from scratch
|
||||
// - Pipe: For composing codecs (which generates automatic names)
|
||||
func WithName[A, O, I any](name string) Endomorphism[Type[A, O, I]] {
|
||||
return func(codec Type[A, O, I]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
name,
|
||||
codec.Is,
|
||||
codec.Validate,
|
||||
codec.Encode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
@@ -688,6 +689,596 @@ func TestBoolFromString_Integration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FromNonZero
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestFromNonZero_Decode_Success(t *testing.T) {
|
||||
t.Run("int - decodes non-zero value", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(42)
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("int - decodes negative value", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(-5)
|
||||
assert.Equal(t, validation.Success(-5), result)
|
||||
})
|
||||
|
||||
t.Run("string - decodes non-empty string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode("hello")
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("string - decodes whitespace string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode(" ")
|
||||
assert.Equal(t, validation.Success(" "), result)
|
||||
})
|
||||
|
||||
t.Run("bool - decodes true", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
result := c.Decode(true)
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("float64 - decodes non-zero value", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(3.14)
|
||||
assert.Equal(t, validation.Success(3.14), result)
|
||||
})
|
||||
|
||||
t.Run("float64 - decodes negative value", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(-2.5)
|
||||
assert.Equal(t, validation.Success(-2.5), result)
|
||||
})
|
||||
|
||||
t.Run("pointer - decodes non-nil pointer", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
value := 42
|
||||
result := c.Decode(&value)
|
||||
assert.True(t, either.IsRight(result))
|
||||
ptr := either.MonadFold(result, func(validation.Errors) *int { return nil }, func(p *int) *int { return p })
|
||||
require.NotNil(t, ptr)
|
||||
assert.Equal(t, 42, *ptr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Decode_Failure(t *testing.T) {
|
||||
t.Run("int - fails on zero", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("string - fails on empty string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("bool - fails on false", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
result := c.Decode(false)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("float64 - fails on zero", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(0.0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("pointer - fails on nil", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
result := c.Decode(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Encode(t *testing.T) {
|
||||
t.Run("int - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
assert.Equal(t, 42, c.Encode(42))
|
||||
})
|
||||
|
||||
t.Run("string - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
assert.Equal(t, "hello", c.Encode("hello"))
|
||||
})
|
||||
|
||||
t.Run("bool - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
assert.Equal(t, true, c.Encode(true))
|
||||
})
|
||||
|
||||
t.Run("float64 - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
assert.Equal(t, 3.14, c.Encode(3.14))
|
||||
})
|
||||
|
||||
t.Run("pointer - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
value := 42
|
||||
ptr := &value
|
||||
assert.Equal(t, ptr, c.Encode(ptr))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode then encode", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
original := 42
|
||||
result := c.Decode(original)
|
||||
require.True(t, either.IsRight(result))
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
|
||||
assert.Equal(t, original, c.Encode(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Name(t *testing.T) {
|
||||
t.Run("int codec name", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
assert.Contains(t, c.Name(), "FromRefinement")
|
||||
assert.Contains(t, c.Name(), "PrismFromNonZero")
|
||||
})
|
||||
|
||||
t.Run("string codec name", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
assert.Contains(t, c.Name(), "FromRefinement")
|
||||
assert.Contains(t, c.Name(), "PrismFromNonZero")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Integration(t *testing.T) {
|
||||
t.Run("validates multiple non-zero integers", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
values := []int{1, -1, 42, -100, 999}
|
||||
for _, v := range values {
|
||||
result := c.Decode(v)
|
||||
require.True(t, either.IsRight(result), "expected success for %d", v)
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
|
||||
assert.Equal(t, v, decoded)
|
||||
assert.Equal(t, v, c.Encode(decoded))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects zero values", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("works with custom comparable types", func(t *testing.T) {
|
||||
type UserID string
|
||||
c := FromNonZero[UserID]()
|
||||
|
||||
result := c.Decode(UserID("user123"))
|
||||
assert.Equal(t, validation.Success(UserID("user123")), result)
|
||||
|
||||
result = c.Decode(UserID(""))
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NonEmptyString
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNonEmptyString_Decode_Success(t *testing.T) {
|
||||
t.Run("decodes non-empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("hello")
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes single character", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("a")
|
||||
assert.Equal(t, validation.Success("a"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes whitespace string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode(" ")
|
||||
assert.Equal(t, validation.Success(" "), result)
|
||||
})
|
||||
|
||||
t.Run("decodes string with newlines", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("\n\t")
|
||||
assert.Equal(t, validation.Success("\n\t"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes unicode string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("你好")
|
||||
assert.Equal(t, validation.Success("你好"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes emoji string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("🎉")
|
||||
assert.Equal(t, validation.Success("🎉"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes multiline string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
multiline := "line1\nline2\nline3"
|
||||
result := c.Decode(multiline)
|
||||
assert.Equal(t, validation.Success(multiline), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Decode_Failure(t *testing.T) {
|
||||
t.Run("fails on empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("error contains context", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
require.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(string) validation.Errors { return nil })
|
||||
require.NotEmpty(t, errors)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Encode(t *testing.T) {
|
||||
t.Run("encodes string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, "hello", c.Encode("hello"))
|
||||
})
|
||||
|
||||
t.Run("encodes unicode string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, "你好", c.Encode("你好"))
|
||||
})
|
||||
|
||||
t.Run("encodes whitespace string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, " ", c.Encode(" "))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode then encode", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
original := "test string"
|
||||
result := c.Decode(original)
|
||||
require.True(t, either.IsRight(result))
|
||||
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(s string) string { return s })
|
||||
assert.Equal(t, original, c.Encode(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Name(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, c.Name(), "NonEmptyString")
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Integration(t *testing.T) {
|
||||
t.Run("validates multiple non-empty strings", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
strings := []string{"a", "hello", "world", "test123", " spaces ", "🎉"}
|
||||
for _, s := range strings {
|
||||
result := c.Decode(s)
|
||||
require.True(t, either.IsRight(result), "expected success for %q", s)
|
||||
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(str string) string { return str })
|
||||
assert.Equal(t, s, decoded)
|
||||
assert.Equal(t, s, c.Encode(decoded))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("compose with IntFromString", func(t *testing.T) {
|
||||
// Create a codec that only parses non-empty strings to integers
|
||||
nonEmptyThenInt := Pipe[string, string](IntFromString())(NonEmptyString())
|
||||
|
||||
// Valid non-empty string with integer
|
||||
result := nonEmptyThenInt.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Empty string fails at NonEmptyString stage
|
||||
result = nonEmptyThenInt.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
// Non-empty but invalid integer fails at IntFromString stage
|
||||
result = nonEmptyThenInt.Decode("abc")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("use in validation pipeline", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
|
||||
// Simulate validating user input
|
||||
inputs := []struct {
|
||||
value string
|
||||
expected bool
|
||||
}{
|
||||
{"john_doe", true},
|
||||
{"", false},
|
||||
{"a", true},
|
||||
{"user@example.com", true},
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
result := c.Decode(input.value)
|
||||
if input.expected {
|
||||
assert.True(t, either.IsRight(result), "expected success for %q", input.value)
|
||||
} else {
|
||||
assert.True(t, either.IsLeft(result), "expected failure for %q", input.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WithName
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestWithName_BasicFunctionality(t *testing.T) {
|
||||
t.Run("renames codec without changing behavior", func(t *testing.T) {
|
||||
original := IntFromString()
|
||||
renamed := WithName[int, string, string]("CustomIntCodec")(original)
|
||||
|
||||
// Name should be changed
|
||||
assert.Equal(t, "CustomIntCodec", renamed.Name())
|
||||
assert.NotEqual(t, original.Name(), renamed.Name())
|
||||
|
||||
// Behavior should be unchanged
|
||||
result := renamed.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
encoded := renamed.Encode(42)
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
|
||||
t.Run("preserves validation logic", func(t *testing.T) {
|
||||
original := IntFromString()
|
||||
renamed := WithName[int, string, string]("MyInt")(original)
|
||||
|
||||
// Valid input should succeed
|
||||
result := renamed.Decode("123")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Invalid input should fail
|
||||
result = renamed.Decode("not a number")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("preserves encoding logic", func(t *testing.T) {
|
||||
original := BoolFromString()
|
||||
renamed := WithName[bool, string, string]("CustomBool")(original)
|
||||
|
||||
assert.Equal(t, "true", renamed.Encode(true))
|
||||
assert.Equal(t, "false", renamed.Encode(false))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_WithComposedCodecs(t *testing.T) {
|
||||
t.Run("renames composed codec", func(t *testing.T) {
|
||||
// Create a composed codec
|
||||
composed := Pipe[string, string](IntFromString())(NonEmptyString())
|
||||
|
||||
// Rename it
|
||||
renamed := WithName[int, string, string]("NonEmptyIntString")(composed)
|
||||
|
||||
assert.Equal(t, "NonEmptyIntString", renamed.Name())
|
||||
|
||||
// Behavior should be preserved
|
||||
result := renamed.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Empty string should fail
|
||||
result = renamed.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
// Non-numeric should fail
|
||||
result = renamed.Decode("abc")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
|
||||
codec := F.Pipe1(
|
||||
IntFromString(),
|
||||
WithName[int, string, string]("UserAge"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "UserAge", codec.Name())
|
||||
|
||||
result := codec.Decode("25")
|
||||
assert.Equal(t, validation.Success(25), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_PreservesTypeChecking(t *testing.T) {
|
||||
t.Run("preserves Is function", func(t *testing.T) {
|
||||
original := String()
|
||||
renamed := WithName[string, string, any]("CustomString")(original)
|
||||
|
||||
// Should accept string
|
||||
result := renamed.Is("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should reject non-string
|
||||
result = renamed.Is(42)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("preserves complex type checking", func(t *testing.T) {
|
||||
original := Array(Int())
|
||||
renamed := WithName[[]int, []int, any]("IntArray")(original)
|
||||
|
||||
// Should accept []int
|
||||
result := renamed.Is([]int{1, 2, 3})
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should reject []string
|
||||
result = renamed.Is([]string{"a", "b"})
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_RoundTrip(t *testing.T) {
|
||||
t.Run("maintains round-trip property", func(t *testing.T) {
|
||||
original := Int64FromString()
|
||||
renamed := WithName[int64, string, string]("CustomInt64")(original)
|
||||
|
||||
testValues := []string{"0", "42", "-100", "9223372036854775807"}
|
||||
for _, input := range testValues {
|
||||
result := renamed.Decode(input)
|
||||
require.True(t, either.IsRight(result), "expected success for %s", input)
|
||||
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int64 { return 0 }, func(n int64) int64 { return n })
|
||||
encoded := renamed.Encode(decoded)
|
||||
assert.Equal(t, input, encoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_ErrorMessages(t *testing.T) {
|
||||
t.Run("custom name appears in validation context", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("PositiveInteger")(IntFromString())
|
||||
|
||||
result := codec.Decode("not a number")
|
||||
require.True(t, either.IsLeft(result))
|
||||
|
||||
// The error context should reference the custom name
|
||||
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(int) validation.Errors { return nil })
|
||||
require.NotEmpty(t, errors)
|
||||
|
||||
// Check that at least one error references our custom name
|
||||
found := false
|
||||
for _, err := range errors {
|
||||
if len(err.Context) > 0 {
|
||||
for _, ctx := range err.Context {
|
||||
if ctx.Type == "PositiveInteger" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "expected custom name 'PositiveInteger' in error context")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_MultipleRenames(t *testing.T) {
|
||||
t.Run("can rename multiple times", func(t *testing.T) {
|
||||
codec := IntFromString()
|
||||
|
||||
renamed1 := WithName[int, string, string]("FirstName")(codec)
|
||||
assert.Equal(t, "FirstName", renamed1.Name())
|
||||
|
||||
renamed2 := WithName[int, string, string]("SecondName")(renamed1)
|
||||
assert.Equal(t, "SecondName", renamed2.Name())
|
||||
|
||||
// Behavior should still work
|
||||
result := renamed2.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_WithDifferentTypes(t *testing.T) {
|
||||
t.Run("works with string codec", func(t *testing.T) {
|
||||
codec := WithName[string, string, string]("Username")(NonEmptyString())
|
||||
assert.Equal(t, "Username", codec.Name())
|
||||
|
||||
result := codec.Decode("john_doe")
|
||||
assert.Equal(t, validation.Success("john_doe"), result)
|
||||
})
|
||||
|
||||
t.Run("works with bool codec", func(t *testing.T) {
|
||||
codec := WithName[bool, string, string]("IsActive")(BoolFromString())
|
||||
assert.Equal(t, "IsActive", codec.Name())
|
||||
|
||||
result := codec.Decode("true")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("works with URL codec", func(t *testing.T) {
|
||||
codec := WithName[*url.URL, string, string]("WebsiteURL")(URL())
|
||||
assert.Equal(t, "WebsiteURL", codec.Name())
|
||||
|
||||
result := codec.Decode("https://example.com")
|
||||
assert.True(t, either.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("works with array codec", func(t *testing.T) {
|
||||
codec := WithName[[]int, []int, any]("Numbers")(Array(Int()))
|
||||
assert.Equal(t, "Numbers", codec.Name())
|
||||
|
||||
result := codec.Decode([]int{1, 2, 3})
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_AsDecoderEncoder(t *testing.T) {
|
||||
t.Run("AsDecoder returns decoder interface", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("MyInt")(IntFromString())
|
||||
decoder := codec.AsDecoder()
|
||||
|
||||
result := decoder.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("AsEncoder returns encoder interface", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("MyInt")(IntFromString())
|
||||
encoder := codec.AsEncoder()
|
||||
|
||||
encoded := encoder.Encode(42)
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_Integration(t *testing.T) {
|
||||
t.Run("domain-specific codec names", func(t *testing.T) {
|
||||
// Create domain-specific codecs with meaningful names
|
||||
emailCodec := WithName[string, string, string]("EmailAddress")(NonEmptyString())
|
||||
phoneCodec := WithName[string, string, string]("PhoneNumber")(NonEmptyString())
|
||||
ageCodec := WithName[int, string, string]("Age")(IntFromString())
|
||||
|
||||
// Test email
|
||||
result := emailCodec.Decode("user@example.com")
|
||||
assert.True(t, either.IsRight(result))
|
||||
assert.Equal(t, "EmailAddress", emailCodec.Name())
|
||||
|
||||
// Test phone
|
||||
result = phoneCodec.Decode("+1234567890")
|
||||
assert.True(t, either.IsRight(result))
|
||||
assert.Equal(t, "PhoneNumber", phoneCodec.Name())
|
||||
|
||||
// Test age
|
||||
ageResult := ageCodec.Decode("25")
|
||||
assert.True(t, either.IsRight(ageResult))
|
||||
assert.Equal(t, "Age", ageCodec.Name())
|
||||
})
|
||||
|
||||
t.Run("naming complex validation pipelines", func(t *testing.T) {
|
||||
// Create a complex codec and give it a clear name
|
||||
positiveIntCodec := F.Pipe2(
|
||||
NonEmptyString(),
|
||||
Pipe[string, string](IntFromString()),
|
||||
WithName[int, string, string]("PositiveIntegerFromString"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "PositiveIntegerFromString", positiveIntCodec.Name())
|
||||
|
||||
result := positiveIntCodec.Decode("42")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
result = positiveIntCodec.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MarshalJSON
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -773,7 +1364,7 @@ func TestIntFromString_PipeComposition(t *testing.T) {
|
||||
func(n int) int { return n },
|
||||
"PositiveInt",
|
||||
)
|
||||
positiveIntCodec := Pipe[string, string, int, int](
|
||||
positiveIntCodec := Pipe[string, string](
|
||||
FromRefinement(positiveIntPrism),
|
||||
)(IntFromString())
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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/optics/codec/decode"
|
||||
)
|
||||
|
||||
// FromIso creates a Type codec from an Iso (isomorphism).
|
||||
//
|
||||
// An isomorphism represents a bidirectional transformation between types I and A
|
||||
// without any loss of information. This function converts an Iso[I, A] into a
|
||||
// Type[A, I, I] codec that can validate, decode, and encode values using the
|
||||
// isomorphism's transformations.
|
||||
//
|
||||
// The resulting codec:
|
||||
// - Decode: Uses iso.Get to transform I → A, always succeeds (no validation)
|
||||
// - Encode: Uses iso.ReverseGet to transform A → I
|
||||
// - Validation: Always succeeds since isomorphisms are lossless transformations
|
||||
// - Type checking: Uses standard type checking for type A
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Creating codecs for newtype patterns (wrapping/unwrapping types)
|
||||
// - Building codecs for types with lossless conversions
|
||||
// - Composing with other codecs using Pipe or other operators
|
||||
// - Implementing bidirectional transformations in codec pipelines
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - I: The input/output type (what we decode from and encode to)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - iso: An Iso[I, A] that defines the bidirectional transformation:
|
||||
// - Get: I → A (converts input to target type)
|
||||
// - ReverseGet: A → I (converts target back to input type)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Type[A, I, I] codec where:
|
||||
// - Decode: I → Validation[A] - transforms using iso.Get, always succeeds
|
||||
// - Encode: A → I - transforms using iso.ReverseGet
|
||||
// - Is: Checks if a value is of type A
|
||||
// - Name: Returns "FromIso[iso_string_representation]"
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// Decoding:
|
||||
// - Applies iso.Get to transform the input value
|
||||
// - Wraps the result in decode.Of (always successful validation)
|
||||
// - No validation errors can occur since isomorphisms are lossless
|
||||
//
|
||||
// Encoding:
|
||||
// - Applies iso.ReverseGet to transform back to the input type
|
||||
// - Always succeeds as isomorphisms guarantee reversibility
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Creating a codec for a newtype pattern:
|
||||
//
|
||||
// type UserId int
|
||||
//
|
||||
// // Define an isomorphism between int and UserId
|
||||
// userIdIso := iso.MakeIso(
|
||||
// func(id UserId) int { return int(id) },
|
||||
// func(i int) UserId { return UserId(i) },
|
||||
// )
|
||||
//
|
||||
// // Create a codec from the isomorphism
|
||||
// userIdCodec := codec.FromIso[int, UserId](userIdIso)
|
||||
//
|
||||
// // Decode: UserId → int
|
||||
// result := userIdCodec.Decode(UserId(42)) // Success: Right(42)
|
||||
//
|
||||
// // Encode: int → UserId
|
||||
// encoded := userIdCodec.Encode(42) // Returns: UserId(42)
|
||||
//
|
||||
// Using with temperature conversions:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Fahrenheit float64
|
||||
//
|
||||
// celsiusToFahrenheit := iso.MakeIso(
|
||||
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
// )
|
||||
//
|
||||
// tempCodec := codec.FromIso[Fahrenheit, Celsius](celsiusToFahrenheit)
|
||||
//
|
||||
// // Decode: Celsius → Fahrenheit
|
||||
// result := tempCodec.Decode(Celsius(20)) // Success: Right(68°F)
|
||||
//
|
||||
// // Encode: Fahrenheit → Celsius
|
||||
// encoded := tempCodec.Encode(Fahrenheit(68)) // Returns: 20°C
|
||||
//
|
||||
// Composing with other codecs:
|
||||
//
|
||||
// type Email string
|
||||
// type ValidatedEmail struct{ value Email }
|
||||
//
|
||||
// emailIso := iso.MakeIso(
|
||||
// func(ve ValidatedEmail) Email { return ve.value },
|
||||
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
|
||||
// )
|
||||
//
|
||||
// // Compose with string codec for validation
|
||||
// emailCodec := F.Pipe2(
|
||||
// codec.String(), // Type[string, string, any]
|
||||
// codec.Pipe(codec.FromIso[Email, string]( // Add string → Email iso
|
||||
// iso.MakeIso(
|
||||
// func(s string) Email { return Email(s) },
|
||||
// func(e Email) string { return string(e) },
|
||||
// ),
|
||||
// )),
|
||||
// codec.Pipe(codec.FromIso[ValidatedEmail, Email](emailIso)), // Add Email → ValidatedEmail iso
|
||||
// )
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Newtype patterns: Wrapping primitive types for type safety
|
||||
// - Unit conversions: Temperature, distance, time, etc.
|
||||
// - Format transformations: Between equivalent representations
|
||||
// - Type aliasing: Creating semantic types from base types
|
||||
// - Codec composition: Building complex codecs from simple isomorphisms
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Isomorphisms must satisfy the round-trip laws:
|
||||
// - iso.ReverseGet(iso.Get(i)) == i
|
||||
// - iso.Get(iso.ReverseGet(a)) == a
|
||||
// - Validation always succeeds since isomorphisms are lossless
|
||||
// - The codec name includes the isomorphism's string representation
|
||||
// - Type checking is performed using the standard Is[A]() function
|
||||
// - This codec is ideal for lossless transformations without validation logic
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - iso.Iso: The isomorphism type used by this function
|
||||
// - iso.MakeIso: Constructor for creating isomorphisms
|
||||
// - Pipe: For composing this codec with other codecs
|
||||
// - MakeType: For creating codecs with custom validation logic
|
||||
func FromIso[A, I any](iso Iso[I, A]) Type[A, I, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("FromIso[%s]", iso),
|
||||
Is[A](),
|
||||
F.Flow2(
|
||||
iso.Get,
|
||||
decode.Of[Context],
|
||||
),
|
||||
iso.ReverseGet,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for newtype pattern
|
||||
type UserId int
|
||||
type Email string
|
||||
type Celsius float64
|
||||
type Fahrenheit float64
|
||||
|
||||
func TestFromIso_Success(t *testing.T) {
|
||||
t.Run("decodes using iso.Get", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes using iso.ReverseGet", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(42)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip preserves value", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
original := UserId(123)
|
||||
|
||||
// Act
|
||||
decoded := codec.Decode(original)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(decoded))
|
||||
roundTrip := either.Fold[validation.Errors, int, UserId](
|
||||
func(validation.Errors) UserId { return UserId(0) },
|
||||
codec.Encode,
|
||||
)(decoded)
|
||||
assert.Equal(t, original, roundTrip)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_StringTypes(t *testing.T) {
|
||||
t.Run("handles string newtype", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Email("user@example.com"))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success("user@example.com"), result)
|
||||
})
|
||||
|
||||
t.Run("encodes string newtype", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode("admin@example.com")
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Email("admin@example.com"), encoded)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Email(""))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_NumericConversions(t *testing.T) {
|
||||
t.Run("converts Celsius to Fahrenheit", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Celsius(0))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(Fahrenheit(32)), result)
|
||||
})
|
||||
|
||||
t.Run("converts Fahrenheit to Celsius", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(Fahrenheit(68))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Celsius(20), encoded)
|
||||
})
|
||||
|
||||
t.Run("handles negative temperatures", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Celsius(-40))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(Fahrenheit(-40)), result)
|
||||
})
|
||||
|
||||
t.Run("temperature round-trip", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
original := Celsius(25)
|
||||
|
||||
// Act
|
||||
decoded := codec.Decode(original)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(decoded))
|
||||
roundTrip := either.Fold[validation.Errors, Fahrenheit, Celsius](
|
||||
func(validation.Errors) Celsius { return Celsius(0) },
|
||||
codec.Encode,
|
||||
)(decoded)
|
||||
// Allow small floating point error
|
||||
diff := float64(original - roundTrip)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
assert.True(t, diff < 0.0001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(0))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(0), result)
|
||||
})
|
||||
|
||||
t.Run("handles negative values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(-1))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(-1), result)
|
||||
})
|
||||
|
||||
t.Run("handles large values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(999999999))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(999999999), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_TypeChecking(t *testing.T) {
|
||||
t.Run("Is checks target type", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
isResult := codec.Is(42)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(isResult))
|
||||
})
|
||||
|
||||
t.Run("Is rejects wrong type", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
isResult := codec.Is("not an int")
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsLeft(isResult))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Name(t *testing.T) {
|
||||
t.Run("includes iso in name", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
name := codec.Name()
|
||||
|
||||
// Assert
|
||||
assert.True(t, len(name) > 0)
|
||||
assert.True(t, name[:7] == "FromIso")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Composition(t *testing.T) {
|
||||
t.Run("composes with Pipe", func(t *testing.T) {
|
||||
// Arrange
|
||||
type PositiveInt int
|
||||
|
||||
// First iso: UserId -> int
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
|
||||
// Second iso: int -> PositiveInt (no validation, just type conversion)
|
||||
positiveIso := iso.MakeIso(
|
||||
func(i int) PositiveInt { return PositiveInt(i) },
|
||||
func(p PositiveInt) int { return int(p) },
|
||||
)
|
||||
|
||||
// Compose codecs
|
||||
codec := F.Pipe1(
|
||||
FromIso[int, UserId](userIdIso),
|
||||
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Of(PositiveInt(42)), result)
|
||||
})
|
||||
|
||||
t.Run("composed codec encodes correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
type PositiveInt int
|
||||
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
|
||||
positiveIso := iso.MakeIso(
|
||||
func(i int) PositiveInt { return PositiveInt(i) },
|
||||
func(p PositiveInt) int { return int(p) },
|
||||
)
|
||||
|
||||
codec := F.Pipe1(
|
||||
FromIso[int, UserId](userIdIso),
|
||||
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
|
||||
)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(PositiveInt(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Integration(t *testing.T) {
|
||||
t.Run("works with Array codec", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
result := arrayCodec.Decode([]UserId{UserId(1), UserId(2), UserId(3)})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("encodes array correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
encoded := arrayCodec.Encode([]int{1, 2, 3})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []UserId{UserId(1), UserId(2), UserId(3)}, encoded)
|
||||
})
|
||||
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
result := arrayCodec.Decode([]UserId{})
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(result))
|
||||
decoded := either.Fold[validation.Errors, []int, []int](
|
||||
func(validation.Errors) []int { return nil },
|
||||
func(arr []int) []int { return arr },
|
||||
)(result)
|
||||
assert.Equal(t, 0, len(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_ComplexTypes(t *testing.T) {
|
||||
t.Run("handles struct wrapping", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Wrapper struct{ Value int }
|
||||
|
||||
wrapperIso := iso.MakeIso(
|
||||
func(w Wrapper) int { return w.Value },
|
||||
func(i int) Wrapper { return Wrapper{Value: i} },
|
||||
)
|
||||
codec := FromIso[int, Wrapper](wrapperIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Wrapper{Value: 42})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes struct wrapping", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Wrapper struct{ Value int }
|
||||
|
||||
wrapperIso := iso.MakeIso(
|
||||
func(w Wrapper) int { return w.Value },
|
||||
func(i int) Wrapper { return Wrapper{Value: i} },
|
||||
)
|
||||
codec := FromIso[int, Wrapper](wrapperIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(42)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Wrapper{Value: 42}, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_AsDecoder(t *testing.T) {
|
||||
t.Run("returns decoder interface", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
decoder := codec.AsDecoder()
|
||||
|
||||
// Assert
|
||||
result := decoder.Decode(UserId(42))
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_AsEncoder(t *testing.T) {
|
||||
t.Run("returns encoder interface", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
encoder := codec.AsEncoder()
|
||||
|
||||
// Assert
|
||||
encoded := encoder.Encode(42)
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Validate(t *testing.T) {
|
||||
t.Run("validate method works correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
validateFn := codec.Validate(UserId(42))
|
||||
result := validateFn([]validation.ContextEntry{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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/iso"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
@@ -494,4 +495,7 @@ type (
|
||||
// - function.VOID: The single value of type Void
|
||||
// - Empty: Codec function that uses Void for unit types
|
||||
Void = function.Void
|
||||
|
||||
// Iso represents an isomorphism - a bidirectional transformation between two types.
|
||||
Iso[S, A any] = iso.Iso[S, A]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
)
|
||||
|
||||
// AsTraversal converts a iso to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(I.Iso[S, A]) R {
|
||||
return func(sa I.Iso[S, A]) R {
|
||||
saSet := fmap(sa.ReverseGet)
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return F.Flow3(
|
||||
sa.Get,
|
||||
f,
|
||||
saSet,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,5 @@ import (
|
||||
)
|
||||
|
||||
func AsTraversal[E, S, A any]() func(L.Lens[S, A]) T.Traversal[E, S, A] {
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.MonadMap[E, A, S])
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.Map[E, A, S])
|
||||
}
|
||||
|
||||
@@ -16,19 +16,24 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// AsTraversal converts a lens to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(L.Lens[S, A]) R {
|
||||
return func(sa L.Lens[S, A]) R {
|
||||
return func(f func(a A) HKTA) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return fmap(f(sa.Get(s)), func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
})
|
||||
return F.Pipe1(
|
||||
f(sa.Get(s)),
|
||||
fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
|
||||
@@ -909,6 +910,83 @@ func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyF transforms a value through a lens using a function that returns a value in a functor context.
|
||||
//
|
||||
// This is the functorial version of Modify, allowing transformations that produce effects
|
||||
// (like Option, Either, IO, etc.) while updating the focused value. The functor's map operation
|
||||
// is used to apply the lens's setter to the transformed value, preserving the computational context.
|
||||
//
|
||||
// This function corresponds to modifyF from monocle-ts, enabling effectful updates through lenses.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - S: Structure type
|
||||
// - A: Focus type (the value being transformed)
|
||||
// - HKTA: Higher-kinded type containing the transformed value (e.g., Option[A], Either[E, A])
|
||||
// - HKTS: Higher-kinded type containing the updated structure (e.g., Option[S], Either[E, S])
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fmap: A functor map operation that transforms A to S within the functor context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A curried function that takes:
|
||||
// 1. A transformation function (A → HKTA)
|
||||
// 2. A Lens[S, A]
|
||||
// 3. A structure S
|
||||
// And returns the updated structure in the functor context (HKTS)
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, age int) Person { p.Age = age; return p },
|
||||
// )
|
||||
//
|
||||
// // Validate age is positive, returning Option
|
||||
// validateAge := func(age int) option.Option[int] {
|
||||
// if age > 0 {
|
||||
// return option.Some(age)
|
||||
// }
|
||||
// return option.None[int]()
|
||||
// }
|
||||
//
|
||||
// // Create a modifier that validates while updating
|
||||
// modifyAge := lens.ModifyF[Person, int](option.Functor[int, Person]().Map)
|
||||
//
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
// result := modifyAge(validateAge)(ageLens)(person)
|
||||
// // result is Some(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
// invalidResult := modifyAge(func(age int) option.Option[int] {
|
||||
// return option.None[int]()
|
||||
// })(ageLens)(person)
|
||||
// // invalidResult is None[Person]()
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Modify: Non-functorial version for simple transformations
|
||||
// - functor.Functor: The functor interface used for mapping
|
||||
func ModifyF[S, A, HKTA, HKTS any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
|
||||
return func(sa Lens[S, A]) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
})(f(sa.Get(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IMap transforms the focus type of a lens using an isomorphism.
|
||||
//
|
||||
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package lens
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
@@ -937,3 +938,367 @@ func TestMakeLensWithEq_WithNilState_MultipleOperations(t *testing.T) {
|
||||
assert.NotNil(t, street4)
|
||||
assert.Equal(t, "", street4.name)
|
||||
}
|
||||
|
||||
// TestModifyF_Success tests ModifyF with a simple Maybe-like functor for successful transformations
|
||||
func TestModifyF_Success(t *testing.T) {
|
||||
// Define a simple Maybe type for testing
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
none := func() Maybe[int] {
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
// Functor map for Maybe
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("transforms value with successful result", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
// Function that returns Some for positive values
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n * 2)
|
||||
}
|
||||
return none()
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
result := modifyAge(validatePositive)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
updated := *result.value
|
||||
assert.Equal(t, 10, updated.Value)
|
||||
assert.Equal(t, "test", updated.Foo)
|
||||
})
|
||||
|
||||
t.Run("preserves structure with identity transformation", func(t *testing.T) {
|
||||
type MaybeStr struct {
|
||||
value *string
|
||||
}
|
||||
|
||||
someStr := func(s string) MaybeStr {
|
||||
return MaybeStr{value: &s}
|
||||
}
|
||||
|
||||
maybeStrMap := func(f func(string) Street) func(MaybeStr) struct{ value *Street } {
|
||||
return func(ma MaybeStr) struct{ value *Street } {
|
||||
if ma.value == nil {
|
||||
return struct{ value *Street }{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return struct{ value *Street }{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
nameLens := MakeLens(
|
||||
func(s Street) string { return s.name },
|
||||
func(s Street, name string) Street { s.name = name; return s },
|
||||
)
|
||||
|
||||
identity := func(s string) MaybeStr {
|
||||
return someStr(s)
|
||||
}
|
||||
|
||||
modifyName := ModifyF[Street, string](maybeStrMap)
|
||||
|
||||
street := Street{num: 1, name: "Main"}
|
||||
result := modifyName(identity)(nameLens)(street)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, street, *result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_Failure tests ModifyF with failures
|
||||
func TestModifyF_Failure(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
none := func() Maybe[int] {
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("returns None when transformation fails", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n)
|
||||
}
|
||||
return none()
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: -5, Foo: "test"}
|
||||
result := modifyAge(validatePositive)(ageLens)(person)
|
||||
|
||||
assert.Nil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_WithResult tests ModifyF with Result/Either-like functor
|
||||
func TestModifyF_WithResult(t *testing.T) {
|
||||
type Result[A any] struct {
|
||||
value *A
|
||||
err error
|
||||
}
|
||||
|
||||
ok := func(a int) Result[int] {
|
||||
return Result[int]{value: &a, err: nil}
|
||||
}
|
||||
|
||||
fail := func(e error) Result[int] {
|
||||
return Result[int]{value: nil, err: e}
|
||||
}
|
||||
|
||||
resultMap := func(f func(int) Inner) func(Result[int]) Result[Inner] {
|
||||
return func(r Result[int]) Result[Inner] {
|
||||
if r.err != nil {
|
||||
return Result[Inner]{value: nil, err: r.err}
|
||||
}
|
||||
result := f(*r.value)
|
||||
return Result[Inner]{value: &result, err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("returns success for valid transformation", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validateAge := func(n int) Result[int] {
|
||||
if n >= 0 && n <= 150 {
|
||||
return ok(n + 1)
|
||||
}
|
||||
return fail(errors.New("age out of range"))
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](resultMap)
|
||||
|
||||
person := Inner{Value: 30, Foo: "test"}
|
||||
result := modifyAge(validateAge)(ageLens)(person)
|
||||
|
||||
assert.Nil(t, result.err)
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, 31, result.value.Value)
|
||||
assert.Equal(t, "test", result.value.Foo)
|
||||
})
|
||||
|
||||
t.Run("returns error for failed validation", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validateAge := func(n int) Result[int] {
|
||||
if n >= 0 && n <= 150 {
|
||||
return ok(n)
|
||||
}
|
||||
return fail(errors.New("age out of range"))
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](resultMap)
|
||||
|
||||
person := Inner{Value: 200, Foo: "test"}
|
||||
result := modifyAge(validateAge)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.err)
|
||||
assert.Equal(t, "age out of range", result.err.Error())
|
||||
assert.Nil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_EdgeCases tests edge cases for ModifyF
|
||||
func TestModifyF_EdgeCases(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
identity := func(n int) Maybe[int] {
|
||||
return some(n)
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 0, Foo: ""}
|
||||
result := modifyAge(identity)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, person, *result.value)
|
||||
})
|
||||
|
||||
t.Run("works with composed lenses", func(t *testing.T) {
|
||||
innerLens := MakeLens(
|
||||
Outer.GetInner,
|
||||
Outer.SetInner,
|
||||
)
|
||||
valueLens := MakeLensRef(
|
||||
(*Inner).GetValue,
|
||||
(*Inner).SetValue,
|
||||
)
|
||||
|
||||
composedLens := Compose[Outer](valueLens)(innerLens)
|
||||
|
||||
maybeMapOuter := func(f func(int) Outer) func(Maybe[int]) Maybe[Outer] {
|
||||
return func(ma Maybe[int]) Maybe[Outer] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Outer]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Outer]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n * 2)
|
||||
}
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
modifyValue := ModifyF[Outer, int](maybeMapOuter)
|
||||
|
||||
outer := Outer{inner: &Inner{Value: 5, Foo: "test"}}
|
||||
result := modifyValue(validatePositive)(composedLens)(outer)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, 10, result.value.inner.Value)
|
||||
assert.Equal(t, "test", result.value.inner.Foo)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_Integration tests integration scenarios
|
||||
func TestModifyF_Integration(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("chains multiple ModifyF operations", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
increment := func(n int) Maybe[int] {
|
||||
return some(n + 1)
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
|
||||
// Apply transformation twice
|
||||
result1 := modifyAge(increment)(ageLens)(person)
|
||||
assert.NotNil(t, result1.value)
|
||||
|
||||
result2 := modifyAge(increment)(ageLens)(*result1.value)
|
||||
assert.NotNil(t, result2.value)
|
||||
|
||||
assert.Equal(t, 7, result2.value.Value)
|
||||
})
|
||||
|
||||
t.Run("combines with regular Modify", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
// First use regular Modify
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
modified := F.Pipe2(
|
||||
ageLens,
|
||||
Modify[Inner](func(n int) int { return n * 2 }),
|
||||
func(endoFn func(Inner) Inner) Inner {
|
||||
return endoFn(person)
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, modified.Value)
|
||||
|
||||
// Then use ModifyF with validation
|
||||
validateRange := func(n int) Maybe[int] {
|
||||
if n >= 0 && n <= 100 {
|
||||
return some(n)
|
||||
}
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
result := modifyAge(validateRange)(ageLens)(modified)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,5 +60,5 @@ import (
|
||||
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
|
||||
// // Apply operations across all configs using the traversal
|
||||
func AsTraversal[S, A any]() func(Lens[S, A]) T.Traversal[S, A] {
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.Map[A, S])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/traversal/generic"
|
||||
)
|
||||
|
||||
// Compose composes a lens with a traversal to create a new traversal.
|
||||
//
|
||||
// This function allows you to focus deeper into a data structure by first using
|
||||
// a lens to access a field, then using a traversal to access multiple values within
|
||||
// that field. The result is a traversal that can operate on all the nested values.
|
||||
//
|
||||
// The composition follows the pattern: Lens[S, A] → Traversal[A, B] → Traversal[S, B]
|
||||
// where the lens focuses on field A within structure S, and the traversal focuses on
|
||||
// multiple B values within A.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The outer structure type
|
||||
// - A: The intermediate field type (target of the lens)
|
||||
// - B: The final focus type (targets of the traversal)
|
||||
//
|
||||
// Parameters:
|
||||
// - t: A traversal that focuses on B values within A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Traversal[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// LT "github.com/IBM/fp-go/v2/optics/lens/traversal"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// type Team struct {
|
||||
// Name string
|
||||
// Members []string
|
||||
// }
|
||||
//
|
||||
// // Lens to access the Members field
|
||||
// membersLens := lens.MakeLens(
|
||||
// func(t Team) []string { return t.Members },
|
||||
// func(t Team, m []string) Team { t.Members = m; return t },
|
||||
// )
|
||||
//
|
||||
// // Traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[string]()
|
||||
//
|
||||
// // Compose lens with traversal to access all member names
|
||||
// memberTraversal := F.Pipe1(
|
||||
// membersLens,
|
||||
// LT.Compose[Team, []string, string](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// team := Team{Name: "Engineering", Members: []string{"Alice", "Bob"}}
|
||||
// // Uppercase all member names
|
||||
// updated := memberTraversal(strings.ToUpper)(team)
|
||||
// // updated.Members: ["ALICE", "BOB"]
|
||||
//
|
||||
// See Also:
|
||||
// - Lens: A functional reference to a subpart of a data structure
|
||||
// - Traversal: A functional reference to multiple subparts
|
||||
// - traversal.Compose: Composes two traversals
|
||||
func Compose[S, A, B any](t Traversal[A, B, A, B]) func(Lens[S, A]) Traversal[S, B, S, B] {
|
||||
return G.Compose[S, A, B, S, A, B](
|
||||
I.Map,
|
||||
)(t)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
Name string
|
||||
Members []string
|
||||
}
|
||||
|
||||
type Company struct {
|
||||
Name string
|
||||
Teams []Team
|
||||
}
|
||||
|
||||
func TestCompose_Success(t *testing.T) {
|
||||
t.Run("composes lens with array traversal to modify nested values", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob", "charlie"},
|
||||
}
|
||||
|
||||
// Act - uppercase all member names
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
expected := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"ALICE", "BOB", "CHARLIE"},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal on empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal to transform numbers", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Stats struct {
|
||||
Name string
|
||||
Scores []int
|
||||
}
|
||||
|
||||
scoresLens := lens.MakeLens(
|
||||
func(s Stats) []int { return s.Scores },
|
||||
func(s Stats, scores []int) Stats {
|
||||
s.Scores = scores
|
||||
return s
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
|
||||
scoreTraversal := F.Pipe1(
|
||||
scoresLens,
|
||||
Compose[Stats, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
stats := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{10, 20, 30},
|
||||
}
|
||||
|
||||
// Act - double all scores
|
||||
result := scoreTraversal(func(n int) int { return n * 2 })(stats)
|
||||
|
||||
// Assert
|
||||
expected := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{20, 40, 60},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_Integration(t *testing.T) {
|
||||
t.Run("composes multiple lenses and traversals", func(t *testing.T) {
|
||||
// Arrange - nested structure with Company -> Teams -> Members
|
||||
teamsLens := lens.MakeLens(
|
||||
func(c Company) []Team { return c.Teams },
|
||||
func(c Company, teams []Team) Company {
|
||||
c.Teams = teams
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
// First compose: Company -> []Team -> Team
|
||||
teamArrayTraversal := AI.FromArray[Team]()
|
||||
companyToTeamTraversal := F.Pipe1(
|
||||
teamsLens,
|
||||
Compose[Company, []Team, Team](teamArrayTraversal),
|
||||
)
|
||||
|
||||
// Second compose: Team -> []string -> string
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
memberArrayTraversal := AI.FromArray[string]()
|
||||
teamToMemberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](memberArrayTraversal),
|
||||
)
|
||||
|
||||
company := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"alice", "bob"}},
|
||||
{Name: "Design", Members: []string{"charlie", "diana"}},
|
||||
},
|
||||
}
|
||||
|
||||
// Act - uppercase all members in all teams
|
||||
// First traverse to teams, then for each team traverse to members
|
||||
result := companyToTeamTraversal(func(team Team) Team {
|
||||
return teamToMemberTraversal(strings.ToUpper)(team)
|
||||
})(company)
|
||||
|
||||
// Assert
|
||||
expected := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"ALICE", "BOB"}},
|
||||
{Name: "Design", Members: []string{"CHARLIE", "DIANA"}},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_EdgeCases(t *testing.T) {
|
||||
t.Run("preserves structure name when modifying members", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert - Name should be unchanged
|
||||
assert.Equal(t, "Engineering", result.Name)
|
||||
assert.Equal(t, AR.From("ALICE"), result.Members)
|
||||
})
|
||||
|
||||
t.Run("handles identity transformation", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob"},
|
||||
}
|
||||
|
||||
// Act - apply identity function
|
||||
result := memberTraversal(F.Identity[string])(team)
|
||||
|
||||
// Assert - should be unchanged
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/generic"
|
||||
TG "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
func Compose[S, A, B, HKTS, HKTA, HKTB any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
lensTrav := G.AsTraversal[Traversal[S, A, HKTS, HKTA]](fmap)
|
||||
|
||||
return func(ab Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
return F.Flow2(
|
||||
lensTrav,
|
||||
TG.Compose[
|
||||
Traversal[A, B, HKTA, HKTB],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, B, HKTS, HKTB],
|
||||
](ab),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
@@ -5,6 +5,7 @@ package lenses
|
||||
// 2026-01-27 16:08:47.5483589 +0100 CET m=+0.003380301
|
||||
|
||||
import (
|
||||
"net"
|
||||
url "net/url"
|
||||
|
||||
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
|
||||
@@ -119,6 +120,8 @@ type URLLenses struct {
|
||||
RawQuery __lens.Lens[url.URL, string]
|
||||
Fragment __lens.Lens[url.URL, string]
|
||||
RawFragment __lens.Lens[url.URL, string]
|
||||
Hostname __lens.Lens[url.URL, string]
|
||||
Port __lens.Lens[url.URL, string]
|
||||
// optional fields
|
||||
SchemeO __lens_option.LensO[url.URL, string]
|
||||
OpaqueO __lens_option.LensO[url.URL, string]
|
||||
@@ -131,6 +134,8 @@ type URLLenses struct {
|
||||
RawQueryO __lens_option.LensO[url.URL, string]
|
||||
FragmentO __lens_option.LensO[url.URL, string]
|
||||
RawFragmentO __lens_option.LensO[url.URL, string]
|
||||
HostnameO __lens_option.LensO[url.URL, string]
|
||||
PortO __lens_option.LensO[url.URL, string]
|
||||
}
|
||||
|
||||
// URLRefLenses provides lenses for accessing fields of url.URL via a reference to url.URL
|
||||
@@ -147,6 +152,8 @@ type URLRefLenses struct {
|
||||
RawQuery __lens.Lens[*url.URL, string]
|
||||
Fragment __lens.Lens[*url.URL, string]
|
||||
RawFragment __lens.Lens[*url.URL, string]
|
||||
Hostname __lens.Lens[*url.URL, string]
|
||||
Port __lens.Lens[*url.URL, string]
|
||||
// optional fields
|
||||
SchemeO __lens_option.LensO[*url.URL, string]
|
||||
OpaqueO __lens_option.LensO[*url.URL, string]
|
||||
@@ -159,6 +166,8 @@ type URLRefLenses struct {
|
||||
RawQueryO __lens_option.LensO[*url.URL, string]
|
||||
FragmentO __lens_option.LensO[*url.URL, string]
|
||||
RawFragmentO __lens_option.LensO[*url.URL, string]
|
||||
HostnameO __lens_option.LensO[*url.URL, string]
|
||||
PortO __lens_option.LensO[*url.URL, string]
|
||||
}
|
||||
|
||||
// MakeURLLenses creates a new URLLenses with lenses for all fields
|
||||
@@ -219,6 +228,38 @@ func MakeURLLenses() URLLenses {
|
||||
func(s url.URL, v string) url.URL { s.RawFragment = v; return s },
|
||||
"URL.RawFragment",
|
||||
)
|
||||
lensHostname := __lens.MakeLensWithName(
|
||||
func(s url.URL) string {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
return s.Host
|
||||
}
|
||||
return host
|
||||
},
|
||||
func(s url.URL, v string) url.URL {
|
||||
_, port, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = v
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(v, port)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Hostname",
|
||||
)
|
||||
lensPort := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.Port() },
|
||||
func(s url.URL, v string) url.URL {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = net.JoinHostPort(s.Host, v)
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(host, v)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Port",
|
||||
)
|
||||
// optional lenses
|
||||
lensSchemeO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensScheme)
|
||||
lensOpaqueO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensOpaque)
|
||||
@@ -231,6 +272,8 @@ func MakeURLLenses() URLLenses {
|
||||
lensRawQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawQuery)
|
||||
lensFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensFragment)
|
||||
lensRawFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawFragment)
|
||||
lensHostnameO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensHostname)
|
||||
lensPortO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensPort)
|
||||
return URLLenses{
|
||||
// mandatory lenses
|
||||
Scheme: lensScheme,
|
||||
@@ -244,6 +287,8 @@ func MakeURLLenses() URLLenses {
|
||||
RawQuery: lensRawQuery,
|
||||
Fragment: lensFragment,
|
||||
RawFragment: lensRawFragment,
|
||||
Hostname: lensHostname,
|
||||
Port: lensPort,
|
||||
// optional lenses
|
||||
SchemeO: lensSchemeO,
|
||||
OpaqueO: lensOpaqueO,
|
||||
@@ -256,6 +301,8 @@ func MakeURLLenses() URLLenses {
|
||||
RawQueryO: lensRawQueryO,
|
||||
FragmentO: lensFragmentO,
|
||||
RawFragmentO: lensRawFragmentO,
|
||||
HostnameO: lensHostnameO,
|
||||
PortO: lensPortO,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +364,38 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
func(s *url.URL, v string) *url.URL { s.RawFragment = v; return s },
|
||||
"(*url.URL).RawFragment",
|
||||
)
|
||||
lensHostname := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
return s.Host
|
||||
}
|
||||
return host
|
||||
},
|
||||
func(s *url.URL, v string) *url.URL {
|
||||
_, port, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = v
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(v, port)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Hostname",
|
||||
)
|
||||
lensPort := __lens.MakeLensStrictWithName(
|
||||
(*url.URL).Port,
|
||||
func(s *url.URL, v string) *url.URL {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = net.JoinHostPort(s.Host, v)
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(host, v)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Port",
|
||||
)
|
||||
// optional lenses
|
||||
lensSchemeO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensScheme)
|
||||
lensOpaqueO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensOpaque)
|
||||
@@ -329,6 +408,8 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
lensRawQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawQuery)
|
||||
lensFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensFragment)
|
||||
lensRawFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawFragment)
|
||||
lensHostnameO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensHostname)
|
||||
lensPortO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensPort)
|
||||
return URLRefLenses{
|
||||
// mandatory lenses
|
||||
Scheme: lensScheme,
|
||||
@@ -342,6 +423,8 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
RawQuery: lensRawQuery,
|
||||
Fragment: lensFragment,
|
||||
RawFragment: lensRawFragment,
|
||||
Hostname: lensHostname,
|
||||
Port: lensPort,
|
||||
// optional lenses
|
||||
SchemeO: lensSchemeO,
|
||||
OpaqueO: lensOpaqueO,
|
||||
@@ -354,6 +437,8 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
RawQueryO: lensRawQueryO,
|
||||
FragmentO: lensFragmentO,
|
||||
RawFragmentO: lensRawFragmentO,
|
||||
HostnameO: lensHostnameO,
|
||||
PortO: lensPortO,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+501
-590
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user