mirror of
https://github.com/IBM/fp-go.git
synced 2026-04-09 15:26:02 +02:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6834f72856 | ||
|
|
8cfb7ef659 | ||
|
|
622c87d734 | ||
|
|
2ce406a410 | ||
|
|
3743361b9f | ||
|
|
69d11f0a4b | ||
|
|
e4dd1169c4 | ||
|
|
1657569f1d | ||
|
|
545876d013 | ||
|
|
9492c5d994 | ||
|
|
94b1ea30d1 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
continue-on-error: true
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
run: |
|
||||
cd v2
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
continue-on-error: true
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
|
||||
4
context7.json
Normal file
4
context7.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"url": "https://context7.com/ibm/fp-go",
|
||||
"public_key": "pk_7wJdJRn8zGHxvIYu7eh9h"
|
||||
}
|
||||
318
skills/fp-go-http/SKILL.md
Normal file
318
skills/fp-go-http/SKILL.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# fp-go HTTP Requests
|
||||
|
||||
## Overview
|
||||
|
||||
fp-go wraps `net/http` in the `ReaderIOResult` monad, giving you composable, context-aware HTTP operations with automatic error propagation. The core package is:
|
||||
|
||||
```
|
||||
github.com/IBM/fp-go/v2/context/readerioresult/http
|
||||
```
|
||||
|
||||
All HTTP operations are lazy — they describe what to do but do not execute until you call the resulting function with a `context.Context`.
|
||||
|
||||
## Core Types
|
||||
|
||||
```go
|
||||
// Requester builds an *http.Request given a context.
|
||||
type Requester = ReaderIOResult[*http.Request] // func(context.Context) func() result.Result[*http.Request]
|
||||
|
||||
// Client executes a Requester and returns the response wrapped in ReaderIOResult.
|
||||
type Client interface {
|
||||
Do(Requester) ReaderIOResult[*http.Response]
|
||||
}
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### 1. Create a Client
|
||||
|
||||
```go
|
||||
import (
|
||||
HTTP "net/http"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
)
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
|
||||
// Or with a custom client:
|
||||
custom := &HTTP.Client{Timeout: 10 * time.Second}
|
||||
client := H.MakeClient(custom)
|
||||
```
|
||||
|
||||
### 2. Build a Request
|
||||
|
||||
```go
|
||||
// GET request (most common)
|
||||
req := H.MakeGetRequest("https://api.example.com/users/1")
|
||||
|
||||
// Arbitrary method + body
|
||||
req := H.MakeRequest("POST", "https://api.example.com/users", bodyReader)
|
||||
```
|
||||
|
||||
### 3. Execute and Parse
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
|
||||
// ReadJSON validates status, Content-Type, then unmarshals JSON
|
||||
result := H.ReadJSON[User](client)(H.MakeGetRequest("https://api.example.com/users/1"))
|
||||
|
||||
// Execute — provide context once
|
||||
user, err := result(context.Background())()
|
||||
```
|
||||
|
||||
## Response Readers
|
||||
|
||||
All accept a `Client` and return a function `Requester → ReaderIOResult[A]`:
|
||||
|
||||
| Function | Returns | Notes |
|
||||
|----------|---------|-------|
|
||||
| `ReadJSON[A](client)` | `ReaderIOResult[A]` | Validates status + Content-Type, unmarshals JSON |
|
||||
| `ReadText(client)` | `ReaderIOResult[string]` | Validates status, reads body as UTF-8 string |
|
||||
| `ReadAll(client)` | `ReaderIOResult[[]byte]` | Validates status, returns raw body bytes |
|
||||
| `ReadFullResponse(client)` | `ReaderIOResult[FullResponse]` | Returns `Pair[*http.Response, []byte]` |
|
||||
|
||||
`FullResponse = Pair[*http.Response, []byte]` — use `pair.First` / `pair.Second` to access components.
|
||||
|
||||
## Composing Requests in Pipelines
|
||||
|
||||
```go
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
readPost := H.ReadJSON[Post](client)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
H.MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1"),
|
||||
readPost,
|
||||
RIO.ChainFirstIOK(IO.Logf[Post]("Got post: %v")),
|
||||
)
|
||||
|
||||
post, err := pipeline(context.Background())()
|
||||
```
|
||||
|
||||
## Parallel Requests — Homogeneous Types
|
||||
|
||||
Use `RIO.TraverseArray` when all requests return the same type:
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
type PostItem struct {
|
||||
UserID uint `json:"userId"`
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
readPost := H.ReadJSON[PostItem](client)
|
||||
|
||||
// Fetch 10 posts in parallel
|
||||
data := F.Pipe3(
|
||||
A.MakeBy(10, func(i int) string {
|
||||
return fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", i+1)
|
||||
}),
|
||||
RIO.TraverseArray(F.Flow3(
|
||||
H.MakeGetRequest,
|
||||
readPost,
|
||||
RIO.ChainFirstIOK(IO.Logf[PostItem]("Post: %v")),
|
||||
)),
|
||||
RIO.ChainFirstIOK(IO.Logf[[]PostItem]("All posts: %v")),
|
||||
RIO.Map(A.Size[PostItem]),
|
||||
)
|
||||
|
||||
count, err := data(context.Background())()
|
||||
```
|
||||
|
||||
## Parallel Requests — Heterogeneous Types
|
||||
|
||||
Use `RIO.TraverseTuple2` (or `Tuple3`, etc.) when requests return different types:
|
||||
|
||||
```go
|
||||
import (
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
type CatFact struct {
|
||||
Fact string `json:"fact"`
|
||||
}
|
||||
|
||||
client := H.MakeClient(HTTP.DefaultClient)
|
||||
readPost := H.ReadJSON[PostItem](client)
|
||||
readCatFact := H.ReadJSON[CatFact](client)
|
||||
|
||||
// Execute both requests in parallel with different response types
|
||||
data := F.Pipe3(
|
||||
T.MakeTuple2(
|
||||
"https://jsonplaceholder.typicode.com/posts/1",
|
||||
"https://catfact.ninja/fact",
|
||||
),
|
||||
T.Map2(H.MakeGetRequest, H.MakeGetRequest), // build both requesters
|
||||
RIO.TraverseTuple2(readPost, readCatFact), // run in parallel, typed
|
||||
RIO.ChainFirstIOK(IO.Logf[T.Tuple2[PostItem, CatFact]]("Result: %v")),
|
||||
)
|
||||
|
||||
both, err := data(context.Background())()
|
||||
// both.F1 is PostItem, both.F2 is CatFact
|
||||
```
|
||||
|
||||
## Building Requests with the Builder API
|
||||
|
||||
For complex requests (custom headers, query params, JSON body), use the builder:
|
||||
|
||||
```go
|
||||
import (
|
||||
B "github.com/IBM/fp-go/v2/http/builder"
|
||||
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// GET with query parameters
|
||||
req := F.Pipe2(
|
||||
B.Default,
|
||||
B.WithURL("https://api.example.com/items?page=1"),
|
||||
B.WithQueryArg("limit")("50"),
|
||||
)
|
||||
requester := RB.Requester(req)
|
||||
|
||||
// POST with JSON body
|
||||
req := F.Pipe3(
|
||||
B.Default,
|
||||
B.WithURL("https://api.example.com/users"),
|
||||
B.WithMethod("POST"),
|
||||
B.WithJSON(map[string]string{"name": "Alice"}),
|
||||
// sets Content-Type: application/json automatically
|
||||
)
|
||||
requester := RB.Requester(req)
|
||||
|
||||
// With authentication and custom headers
|
||||
req := F.Pipe3(
|
||||
B.Default,
|
||||
B.WithURL("https://api.example.com/protected"),
|
||||
B.WithBearer("my-token"), // sets Authorization: Bearer my-token
|
||||
B.WithHeader("X-Request-ID")("123"),
|
||||
)
|
||||
requester := RB.Requester(req)
|
||||
|
||||
// Execute
|
||||
result := H.ReadJSON[Response](client)(requester)
|
||||
data, err := result(ctx)()
|
||||
```
|
||||
|
||||
### Builder Functions
|
||||
|
||||
| Function | Effect |
|
||||
|----------|--------|
|
||||
| `B.WithURL(url)` | Set the target URL |
|
||||
| `B.WithMethod(method)` | Set HTTP method (GET, POST, PUT, DELETE, …) |
|
||||
| `B.WithJSON(v)` | Marshal `v` as JSON body, set `Content-Type: application/json` |
|
||||
| `B.WithBytes(data)` | Set raw bytes body, set `Content-Length` automatically |
|
||||
| `B.WithHeader(key)(value)` | Add a request header |
|
||||
| `B.WithBearer(token)` | Set `Authorization: Bearer <token>` |
|
||||
| `B.WithQueryArg(key)(value)` | Append a query parameter |
|
||||
|
||||
## Error Handling
|
||||
|
||||
Errors from request creation, HTTP status codes, Content-Type validation, and JSON parsing all propagate automatically through the `Result` monad. You only handle errors at the call site:
|
||||
|
||||
```go
|
||||
// Pattern 1: direct extraction
|
||||
value, err := pipeline(ctx)()
|
||||
if err != nil { /* handle */ }
|
||||
|
||||
// Pattern 2: Fold for clean HTTP handler
|
||||
RIO.Fold(
|
||||
func(err error) { http.Error(w, err.Error(), http.StatusInternalServerError) },
|
||||
func(data MyType) { json.NewEncoder(w).Encode(data) },
|
||||
)(pipeline)(ctx)()
|
||||
```
|
||||
|
||||
## Full HTTP Handler Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
HTTP "net/http"
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
var client = H.MakeClient(HTTP.DefaultClient)
|
||||
|
||||
func fetchPost(id int) RIO.ReaderIOResult[Post] {
|
||||
url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id)
|
||||
return F.Pipe2(
|
||||
H.MakeGetRequest(url),
|
||||
H.ReadJSON[Post](client),
|
||||
RIO.ChainFirstIOK(IO.Logf[Post]("fetched: %v")),
|
||||
)
|
||||
}
|
||||
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
RIO.Fold(
|
||||
func(err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadGateway)
|
||||
},
|
||||
func(post Post) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(post)
|
||||
},
|
||||
)(fetchPost(1))(r.Context())()
|
||||
}
|
||||
```
|
||||
|
||||
## Import Reference
|
||||
|
||||
```go
|
||||
import (
|
||||
HTTP "net/http"
|
||||
|
||||
H "github.com/IBM/fp-go/v2/context/readerioresult/http"
|
||||
RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
|
||||
B "github.com/IBM/fp-go/v2/http/builder"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
)
|
||||
```
|
||||
|
||||
Requires Go 1.24+.
|
||||
410
skills/fp-go-logging/SKILL.md
Normal file
410
skills/fp-go-logging/SKILL.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# fp-go Logging
|
||||
|
||||
## Overview
|
||||
|
||||
fp-go provides logging utilities that integrate naturally with functional pipelines. Logging is always a **side effect** — it should not change the value being processed. The library achieves this through `ChainFirst`-style combinators that thread the original value through unchanged while performing the log.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `github.com/IBM/fp-go/v2/logging` | Global logger, context-embedded logger, `LoggingCallbacks` |
|
||||
| `github.com/IBM/fp-go/v2/io` | `Logf`, `Logger`, `LogGo`, `Printf`, `PrintGo` — IO-level logging helpers |
|
||||
| `github.com/IBM/fp-go/v2/readerio` | `SLog`, `SLogWithCallback` — structured logging for ReaderIO |
|
||||
| `github.com/IBM/fp-go/v2/context/readerio` | `SLog`, `SLogWithCallback` — structured logging for context ReaderIO |
|
||||
| `github.com/IBM/fp-go/v2/context/readerresult` | `SLog`, `TapSLog`, `SLogWithCallback` — structured logging for ReaderResult |
|
||||
| `github.com/IBM/fp-go/v2/context/readerioresult` | `SLog`, `TapSLog`, `SLogWithCallback`, `LogEntryExit`, `LogEntryExitWithCallback` — full suite for ReaderIOResult |
|
||||
|
||||
## Logging Inside Pipelines
|
||||
|
||||
The idiomatic way to log inside a monadic pipeline is `ChainFirstIOK` (or `ChainFirst` where the monad is already IO). These combinators execute a side-effecting function and pass the **original value** downstream unchanged.
|
||||
|
||||
### With `IOResult` / `ReaderIOResult` — printf-style
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(42),
|
||||
RIO.ChainEitherK(validateUser),
|
||||
// Log after validation — value flows through unchanged
|
||||
RIO.ChainFirstIOK(IO.Logf[User]("Validated user: %v")),
|
||||
RIO.Map(enrichUser),
|
||||
)
|
||||
```
|
||||
|
||||
`IO.Logf[A](format string) func(A) IO[A]` logs using `log.Printf` and returns the value unchanged. It's a Kleisli arrow suitable for `ChainFirst` and `ChainFirstIOK`.
|
||||
|
||||
### With `IOEither` / plain `IO`
|
||||
|
||||
```go
|
||||
import (
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
file.ReadFile("config.json"),
|
||||
IOE.ChainEitherK(J.Unmarshal[Config]),
|
||||
IOE.ChainFirstIOK(IO.Logf[Config]("Loaded config: %v")),
|
||||
IOE.Map[error](processConfig),
|
||||
)
|
||||
```
|
||||
|
||||
### Logging Arrays in TraverseArray
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Log each item individually, then log the final slice
|
||||
pipeline := F.Pipe2(
|
||||
A.MakeBy(3, idxToFilename),
|
||||
RIO.TraverseArray(F.Flow3(
|
||||
file.ReadFile,
|
||||
RIO.ChainEitherK(J.Unmarshal[Record]),
|
||||
RIO.ChainFirstIOK(IO.Logf[Record]("Parsed record: %v")),
|
||||
)),
|
||||
RIO.ChainFirstIOK(IO.Logf[[]Record]("All records: %v")),
|
||||
)
|
||||
```
|
||||
|
||||
## IO Logging Functions
|
||||
|
||||
All live in `github.com/IBM/fp-go/v2/io`:
|
||||
|
||||
### `Logf` — printf-style
|
||||
|
||||
```go
|
||||
IO.Logf[A any](format string) func(A) IO[A]
|
||||
```
|
||||
|
||||
Uses `log.Printf`. The format string works like `fmt.Sprintf`.
|
||||
|
||||
```go
|
||||
IO.Logf[User]("Processing user: %+v")
|
||||
IO.Logf[int]("Count: %d")
|
||||
```
|
||||
|
||||
### `Logger` — with custom `*log.Logger`
|
||||
|
||||
```go
|
||||
IO.Logger[A any](loggers ...*log.Logger) func(prefix string) func(A) IO[A]
|
||||
```
|
||||
|
||||
Uses `logger.Printf(prefix+": %v", value)`. Pass your own `*log.Logger` instance.
|
||||
|
||||
```go
|
||||
customLog := log.New(os.Stderr, "APP ", log.LstdFlags)
|
||||
logUser := IO.Logger[User](customLog)("user")
|
||||
// logs: "APP user: {ID:42 Name:Alice}"
|
||||
```
|
||||
|
||||
### `LogGo` — Go template syntax
|
||||
|
||||
```go
|
||||
IO.LogGo[A any](tmpl string) func(A) IO[A]
|
||||
```
|
||||
|
||||
Uses Go's `text/template`. The template receives the value as `.`.
|
||||
|
||||
```go
|
||||
type User struct{ Name string; Age int }
|
||||
IO.LogGo[User]("User {{.Name}} is {{.Age}} years old")
|
||||
```
|
||||
|
||||
### `Printf` / `PrintGo` — stdout instead of log
|
||||
|
||||
Same signatures as `Logf` / `LogGo` but use `fmt.Printf`/`fmt.Println` (no log prefix, no timestamp).
|
||||
|
||||
```go
|
||||
IO.Printf[Result]("Result: %v\n")
|
||||
IO.PrintGo[User]("Name: {{.Name}}")
|
||||
```
|
||||
|
||||
## Structured Logging in the `context` Package
|
||||
|
||||
The `context/readerioresult`, `context/readerresult`, and `context/readerio` packages provide structured `slog`-based logging functions that are context-aware: they retrieve the logger from the context (via `logging.GetLoggerFromContext`) rather than using a fixed logger instance.
|
||||
|
||||
### `TapSLog` — inline structured logging in a ReaderIOResult pipeline
|
||||
|
||||
`TapSLog` is an **Operator** (`func(ReaderIOResult[A]) ReaderIOResult[A]`). It sits directly in a `F.Pipe` call on a `ReaderIOResult`, logs the current value or error using `slog`, and passes the result through unchanged.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe4(
|
||||
fetchOrder(orderID),
|
||||
RIO.TapSLog[Order]("Order fetched"), // logs value=<Order> or error=<err>
|
||||
RIO.Chain(validateOrder),
|
||||
RIO.TapSLog[Order]("Order validated"),
|
||||
RIO.Chain(processPayment),
|
||||
)
|
||||
|
||||
result, err := pipeline(ctx)()
|
||||
```
|
||||
|
||||
- Logs **both** success values (`value=<A>`) and errors (`error=<err>`) using `slog` structured attributes.
|
||||
- Respects the logger level — if the logger is configured to discard Info-level logs, nothing is written.
|
||||
- Available in both `context/readerioresult` and `context/readerresult`.
|
||||
|
||||
### `SLog` — Kleisli-style structured logging
|
||||
|
||||
`SLog` is a **Kleisli arrow** (`func(Result[A]) ReaderResult[A]` / `func(Result[A]) ReaderIOResult[A]`). It is used with `Chain` when you want to intercept the raw `Result` directly.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchData(id),
|
||||
RIO.Chain(RIO.SLog[Data]("Data fetched")), // log raw Result, pass it through
|
||||
RIO.Chain(validateData),
|
||||
RIO.Chain(RIO.SLog[Data]("Data validated")),
|
||||
RIO.Chain(processData),
|
||||
)
|
||||
```
|
||||
|
||||
**Difference from `TapSLog`:**
|
||||
- `TapSLog[A](msg)` is an `Operator[A, A]` — used directly in `F.Pipe` on a `ReaderIOResult[A]`.
|
||||
- `SLog[A](msg)` is a `Kleisli[Result[A], A]` — used with `Chain`, giving access to the raw `Result[A]`.
|
||||
|
||||
Both log in the same format. `TapSLog` is more ergonomic in most pipelines.
|
||||
|
||||
### `SLogWithCallback` — custom log level and logger source
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Log at DEBUG level with a custom logger extracted from context
|
||||
debugLog := RIO.SLogWithCallback[User](
|
||||
slog.LevelDebug,
|
||||
logging.GetLoggerFromContext, // or any func(context.Context) *slog.Logger
|
||||
"Fetched user",
|
||||
)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
fetchUser(123),
|
||||
RIO.Chain(debugLog),
|
||||
RIO.Map(func(u User) string { return u.Name }),
|
||||
)
|
||||
```
|
||||
|
||||
### `LogEntryExit` — automatic entry/exit timing with correlation IDs
|
||||
|
||||
`LogEntryExit` wraps a `ReaderIOResult` computation with structured entry and exit log messages. It assigns a unique **correlation ID** (`ID=<n>`) to each invocation so concurrent or nested operations can be correlated in logs.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(123),
|
||||
RIO.LogEntryExit[User]("fetchUser"), // wraps the operation
|
||||
RIO.Chain(func(user User) RIO.ReaderIOResult[[]Order] {
|
||||
return F.Pipe1(
|
||||
fetchOrders(user.ID),
|
||||
RIO.LogEntryExit[[]Order]("fetchOrders"),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := pipeline(ctx)()
|
||||
// Logs:
|
||||
// level=INFO msg="[entering]" name=fetchUser ID=1
|
||||
// level=INFO msg="[exiting ]" name=fetchUser ID=1 duration=42ms
|
||||
// level=INFO msg="[entering]" name=fetchOrders ID=2
|
||||
// level=INFO msg="[exiting ]" name=fetchOrders ID=2 duration=18ms
|
||||
```
|
||||
|
||||
On error, the exit log changes to `[throwing]` and includes the error:
|
||||
|
||||
```
|
||||
level=INFO msg="[throwing]" name=fetchUser ID=3 duration=5ms error="user not found"
|
||||
```
|
||||
|
||||
Key properties:
|
||||
- **Correlation ID** (`ID=`) is unique per operation, monotonically increasing, and stored in the context so nested operations can access the parent's ID.
|
||||
- **Duration** (`duration=`) is measured from entry to exit.
|
||||
- **Logger is taken from the context** — embed a request-scoped logger with `logging.WithLogger` before executing the pipeline and `LogEntryExit` picks it up automatically.
|
||||
- **Level-aware** — if the logger does not have the log level enabled, the entire entry/exit instrumentation is skipped (zero overhead).
|
||||
- The original `ReaderIOResult[A]` value flows through **unchanged**.
|
||||
|
||||
```go
|
||||
// Use a context logger so all log messages carry request metadata
|
||||
cancelFn, ctxWithLogger := pair.Unpack(
|
||||
logging.WithLogger(
|
||||
slog.Default().With("requestID", r.Header.Get("X-Request-ID")),
|
||||
)(r.Context()),
|
||||
)
|
||||
defer cancelFn()
|
||||
|
||||
result, err := pipeline(ctxWithLogger)()
|
||||
```
|
||||
|
||||
### `LogEntryExitWithCallback` — custom log level
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Log at DEBUG level instead of INFO
|
||||
debugPipeline := F.Pipe1(
|
||||
expensiveComputation(),
|
||||
RIO.LogEntryExitWithCallback[Result](
|
||||
slog.LevelDebug,
|
||||
logging.GetLoggerFromContext,
|
||||
"expensiveComputation",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### `SLog` / `SLogWithCallback` in `context/readerresult`
|
||||
|
||||
The same `SLog` and `TapSLog` functions are also available in `context/readerresult` for use with the synchronous `ReaderResult[A] = func(context.Context) (A, error)` monad:
|
||||
|
||||
```go
|
||||
import RR "github.com/IBM/fp-go/v2/context/readerresult"
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
queryDB(id),
|
||||
RR.TapSLog[Row]("Row fetched"),
|
||||
RR.Chain(parseRow),
|
||||
RR.TapSLog[Record]("Record parsed"),
|
||||
)
|
||||
```
|
||||
|
||||
## Global Logger (`logging` package)
|
||||
|
||||
The `logging` package manages a global `*slog.Logger` (structured logging, Go 1.21+).
|
||||
|
||||
```go
|
||||
import "github.com/IBM/fp-go/v2/logging"
|
||||
|
||||
// Get the current global logger (defaults to slog.Default())
|
||||
logger := logging.GetLogger()
|
||||
logger.Info("application started", "version", "1.0")
|
||||
|
||||
// Replace the global logger; returns the old one for deferred restore
|
||||
old := logging.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
defer logging.SetLogger(old)
|
||||
```
|
||||
|
||||
## Context-Embedded Logger
|
||||
|
||||
Embed a `*slog.Logger` in a `context.Context` to carry request-scoped loggers across the call stack. All context-package logging functions (`TapSLog`, `SLog`, `LogEntryExit`) pick up this logger automatically.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Create a request-scoped logger
|
||||
reqLogger := slog.Default().With("requestID", "abc-123")
|
||||
|
||||
// Embed it into a context using the Kleisli arrow WithLogger
|
||||
cancelFn, ctxWithLogger := pair.Unpack(logging.WithLogger(reqLogger)(ctx))
|
||||
defer cancelFn()
|
||||
|
||||
// All downstream logging (TapSLog, LogEntryExit, etc.) uses reqLogger
|
||||
result, err := pipeline(ctxWithLogger)()
|
||||
```
|
||||
|
||||
`WithLogger` returns a `ContextCancel = Pair[context.CancelFunc, context.Context]`. The cancel function is a no-op — the context is only enriched, not made cancellable.
|
||||
|
||||
`GetLoggerFromContext` falls back to the global logger if no logger is found in the context.
|
||||
|
||||
## `LoggingCallbacks` — Dual-Logger Pattern
|
||||
|
||||
```go
|
||||
import "github.com/IBM/fp-go/v2/logging"
|
||||
|
||||
// Returns (infoCallback, errorCallback) — both are func(string, ...any)
|
||||
infoLog, errLog := logging.LoggingCallbacks() // use log.Default() for both
|
||||
infoLog, errLog := logging.LoggingCallbacks(myLogger) // same logger for both
|
||||
infoLog, errLog := logging.LoggingCallbacks(infoLog, errorLog) // separate loggers
|
||||
```
|
||||
|
||||
Used internally by `io.Logger` and by packages that need separate info/error sinks.
|
||||
|
||||
## Choosing the Right Logging Function
|
||||
|
||||
| Situation | Use |
|
||||
|-----------|-----|
|
||||
| Quick printf logging mid-pipeline | `IO.Logf[A]("fmt")` with `ChainFirstIOK` |
|
||||
| Go template formatting mid-pipeline | `IO.LogGo[A]("tmpl")` with `ChainFirstIOK` |
|
||||
| Print to stdout (no log prefix) | `IO.Printf[A]("fmt")` with `ChainFirstIOK` |
|
||||
| Structured slog — log value or error inline | `RIO.TapSLog[A]("msg")` (Operator, used in Pipe) |
|
||||
| Structured slog — intercept raw Result | `RIO.Chain(RIO.SLog[A]("msg"))` (Kleisli) |
|
||||
| Structured slog — custom log level | `RIO.SLogWithCallback[A](level, cb, "msg")` |
|
||||
| Entry/exit timing + correlation IDs | `RIO.LogEntryExit[A]("name")` |
|
||||
| Entry/exit at custom log level | `RIO.LogEntryExitWithCallback[A](level, cb, "name")` |
|
||||
| Structured logging globally | `logging.GetLogger()` / `logging.SetLogger()` |
|
||||
| Request-scoped logger in context | `logging.WithLogger(logger)` + `logging.GetLoggerFromContext(ctx)` |
|
||||
| Custom `*log.Logger` in pipeline | `IO.Logger[A](logger)("prefix")` with `ChainFirstIOK` |
|
||||
|
||||
## Complete Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure JSON structured logging globally
|
||||
L.SetLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||
|
||||
// Embed a request-scoped logger into the context
|
||||
_, ctx := P.Unpack(L.WithLogger(
|
||||
L.GetLogger().With("requestID", "req-001"),
|
||||
)(context.Background()))
|
||||
|
||||
pipeline := F.Pipe5(
|
||||
fetchData(42),
|
||||
RIO.LogEntryExit[Data]("fetchData"), // entry/exit with timing + ID
|
||||
RIO.TapSLog[Data]("raw data"), // inline structured value log
|
||||
RIO.ChainEitherK(transformData),
|
||||
RIO.LogEntryExit[Result]("transformData"),
|
||||
RIO.ChainFirstIOK(IO.LogGo[Result]("result: {{.Value}}")), // template log
|
||||
)
|
||||
|
||||
value, err := pipeline(ctx)()
|
||||
if err != nil {
|
||||
L.GetLogger().Error("pipeline failed", "error", err)
|
||||
}
|
||||
_ = value
|
||||
}
|
||||
```
|
||||
520
skills/fp-go-monadic-operations/SKILL.md
Normal file
520
skills/fp-go-monadic-operations/SKILL.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# fp-go Monadic Operations
|
||||
|
||||
## Overview
|
||||
|
||||
`fp-go` (import path `github.com/IBM/fp-go/v2`) brings type-safe functional programming to Go using generics. Every monad follows a **consistent interface**: once you know the pattern in one monad, it transfers to all others.
|
||||
|
||||
All functions use the **data-last** principle: the data being transformed is always the last argument, enabling partial application and pipeline composition.
|
||||
|
||||
## Core Types
|
||||
|
||||
| Type | Package | Represents |
|
||||
|------|---------|------------|
|
||||
| `Option[A]` | `option` | A value that may or may not be present (replaces nil) |
|
||||
| `Either[E, A]` | `either` | A value that is either a left error `E` or a right success `A` |
|
||||
| `Result[A]` | `result` | `Either[error, A]` — shorthand for the common case |
|
||||
| `IO[A]` | `io` | A lazy computation that produces `A` (possibly with side effects) |
|
||||
| `IOResult[A]` | `ioresult` | `IO[Result[A]]` — lazy computation that can fail |
|
||||
| `ReaderIOResult[A]` | `context/readerioresult` | `func(context.Context) IOResult[A]` — context-aware IO with errors |
|
||||
| `Effect[C, A]` | `effect` | `func(C) ReaderIOResult[A]` — typed dependency injection + IO + errors |
|
||||
|
||||
Idiomatic (high-performance, tuple-based) equivalents live in `idiomatic/`:
|
||||
- `idiomatic/option` — `(A, bool)` tuples
|
||||
- `idiomatic/result` — `(A, error)` tuples
|
||||
- `idiomatic/ioresult` — `func() (A, error)`
|
||||
- `idiomatic/context/readerresult` — `func(context.Context) (A, error)`
|
||||
|
||||
## Standard Operations
|
||||
|
||||
Every monad exports these operations (PascalCase for exported Go names):
|
||||
|
||||
| fp-go | fp-ts / Haskell | Description |
|
||||
|-------|----------------|-------------|
|
||||
| `Of` | `of` / `pure` | Lift a pure value into the monad |
|
||||
| `Map` | `map` / `fmap` | Transform the value inside without changing the context |
|
||||
| `Chain` | `chain` / `>>=` | Sequence a computation that itself returns a monadic value |
|
||||
| `Ap` | `ap` / `<*>` | Apply a wrapped function to a wrapped value |
|
||||
| `Fold` | `fold` / `either` | Eliminate the context — handle every case and extract a plain value |
|
||||
| `GetOrElse` | `getOrElse` / `fromMaybe` | Extract the value or use a default (Option/Result) |
|
||||
| `Filter` | `filter` / `mfilter` | Keep only values satisfying a predicate |
|
||||
| `Flatten` | `flatten` / `join` | Remove one level of nesting (`M[M[A]]` → `M[A]`) |
|
||||
| `ChainFirst` | `chainFirst` / `>>` | Sequence for side effects; keeps the original value |
|
||||
| `Alt` | `alt` / `<\|>` | Provide an alternative when the first computation fails |
|
||||
| `FromPredicate` | `fromPredicate` / `guard` | Build a monadic value from a predicate |
|
||||
| `Sequence` | `sequence` | Turn `[]M[A]` into `M[[]A]` |
|
||||
| `Traverse` | `traverse` | Map and sequence in one step |
|
||||
|
||||
Curried (composable) vs. monadic (direct) form:
|
||||
|
||||
```go
|
||||
// Curried — data last, returns a transformer function
|
||||
option.Map(strings.ToUpper) // func(Option[string]) Option[string]
|
||||
|
||||
// Monadic — data first, immediate execution
|
||||
option.MonadMap(option.Some("hello"), strings.ToUpper)
|
||||
```
|
||||
|
||||
Use curried form for pipelines; use `Monad*` form when you already have all arguments.
|
||||
|
||||
## Key Type Aliases (defined per monad)
|
||||
|
||||
```go
|
||||
// A Kleisli arrow: a function from A to a monadic B
|
||||
type Kleisli[A, B any] = func(A) M[B]
|
||||
|
||||
// An operator: transforms one monadic value into another
|
||||
type Operator[A, B any] = func(M[A]) M[B]
|
||||
```
|
||||
|
||||
`Chain` takes a `Kleisli`, `Map` returns an `Operator`. The naming is consistent across all monads.
|
||||
|
||||
## Examples
|
||||
|
||||
### Option — nullable values without nil
|
||||
|
||||
```go
|
||||
import (
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
parseAndDouble := F.Flow2(
|
||||
O.FromPredicate(func(s string) bool { return s != "" }),
|
||||
O.Chain(func(s string) O.Option[int] {
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(n * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
parseAndDouble("21") // Some(42)
|
||||
parseAndDouble("") // None
|
||||
parseAndDouble("abc") // None
|
||||
```
|
||||
|
||||
### Result — error handling without if-err boilerplate
|
||||
|
||||
```go
|
||||
import (
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"strconv"
|
||||
"errors"
|
||||
)
|
||||
|
||||
parse := R.Eitherize1(strconv.Atoi) // lifts (int, error) → Result[int]
|
||||
|
||||
validate := func(n int) R.Result[int] {
|
||||
if n < 0 {
|
||||
return R.Error[int](errors.New("must be non-negative"))
|
||||
}
|
||||
return R.Of(n)
|
||||
}
|
||||
|
||||
pipeline := F.Flow2(parse, R.Chain(validate))
|
||||
|
||||
pipeline("42") // Ok(42)
|
||||
pipeline("-1") // Error("must be non-negative")
|
||||
pipeline("abc") // Error(strconv parse error)
|
||||
```
|
||||
|
||||
### IOResult — lazy IO with error handling
|
||||
|
||||
```go
|
||||
import (
|
||||
IOE "github.com/IBM/fp-go/v2/ioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
J "github.com/IBM/fp-go/v2/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
readConfig := F.Flow2(
|
||||
IOE.Eitherize1(os.ReadFile), // func(string) IOResult[[]byte]
|
||||
IOE.ChainEitherK(J.Unmarshal[Config]), // parse JSON, propagate errors
|
||||
)
|
||||
|
||||
result := readConfig("config.json")() // execute lazily
|
||||
```
|
||||
|
||||
### ReaderIOResult — context-aware pipelines (recommended for services)
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"context"
|
||||
)
|
||||
|
||||
// type ReaderIOResult[A any] = func(context.Context) func() result.Result[A]
|
||||
|
||||
fetchUser := func(id int) RIO.ReaderIOResult[User] {
|
||||
return func(ctx context.Context) func() result.Result[User] {
|
||||
return func() result.Result[User] {
|
||||
// perform IO here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
fetchUser(42),
|
||||
RIO.ChainEitherK(validateUser), // lift pure (User, error) function
|
||||
RIO.Map(enrichUser), // lift pure User → User function
|
||||
RIO.ChainFirstIOK(IO.Logf[User]("Fetched: %v")), // side-effect logging
|
||||
)
|
||||
|
||||
user, err := pipeline(ctx)() // provide context once, execute
|
||||
```
|
||||
|
||||
### Traversal — process slices monadically
|
||||
|
||||
```go
|
||||
import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Fetch all users, stop on first error
|
||||
fetchAll := F.Pipe1(
|
||||
A.MakeBy(10, userID),
|
||||
RIO.TraverseArray(fetchUser), // []ReaderIOResult[User] → ReaderIOResult[[]User]
|
||||
)
|
||||
```
|
||||
|
||||
## Function Composition with Flow and Pipe
|
||||
|
||||
```go
|
||||
import F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Flow: compose functions left-to-right, returns a new function
|
||||
transform := F.Flow3(
|
||||
option.Map(strings.TrimSpace),
|
||||
option.Filter(func(s string) bool { return s != "" }),
|
||||
option.GetOrElse(func() string { return "default" }),
|
||||
)
|
||||
result := transform(option.Some(" hello ")) // "hello"
|
||||
|
||||
// Pipe: apply a value through a pipeline immediately
|
||||
result := F.Pipe3(
|
||||
option.Some(" hello "),
|
||||
option.Map(strings.TrimSpace),
|
||||
option.Filter(func(s string) bool { return s != "" }),
|
||||
option.GetOrElse(func() string { return "default" }),
|
||||
)
|
||||
```
|
||||
|
||||
## Lifting Pure Functions into Monadic Context
|
||||
|
||||
fp-go provides helpers to promote non-monadic functions:
|
||||
|
||||
| Helper | Lifts |
|
||||
|--------|-------|
|
||||
| `ChainEitherK` | `func(A) (B, error)` → works inside the monad |
|
||||
| `ChainOptionK` | `func(A) Option[B]` → works inside the monad |
|
||||
| `ChainFirstIOK` | `func(A) IO[B]` for side effects, keeps original value |
|
||||
| `Eitherize1..N` | `func(A) (B, error)` → `func(A) Result[B]` |
|
||||
| `FromPredicate` | `func(A) bool` + error builder → `func(A) Result[A]` |
|
||||
|
||||
## Type Parameter Ordering Rule (V2)
|
||||
|
||||
Non-inferrable type parameters come **first**, so the compiler can infer the rest:
|
||||
|
||||
```go
|
||||
// B cannot be inferred from the argument — it comes first
|
||||
result := either.Ap[string](value)(funcInEither)
|
||||
|
||||
// All types inferrable — no explicit params needed
|
||||
result := either.Map(transform)(value)
|
||||
result := either.Chain(validator)(value)
|
||||
```
|
||||
|
||||
## When to Use Which Monad
|
||||
|
||||
| Situation | Use |
|
||||
|-----------|-----|
|
||||
| Value that might be absent | `Option[A]` |
|
||||
| Operation that can fail with custom error type | `Either[E, A]` |
|
||||
| Operation that can fail with `error` | `Result[A]` |
|
||||
| Lazy IO, side effects | `IO[A]` |
|
||||
| IO that can fail | `IOResult[A]` |
|
||||
| IO + context (cancellation, deadlines) | `ReaderIOResult[A]` from `context/readerioresult` |
|
||||
| IO + context + typed dependencies | `Effect[C, A]` |
|
||||
| High-performance services | Idiomatic packages in `idiomatic/` |
|
||||
|
||||
## Do-Notation: Accumulating State with `Bind` and `ApS`
|
||||
|
||||
When a pipeline needs to carry **multiple intermediate results** forward — not just a single value — the `Chain`/`Map` style becomes unwieldy because each step only threads one value and prior results are lost. Do-notation solves this by accumulating results into a growing struct (the "state") at each step.
|
||||
|
||||
Every monad that supports do-notation exports the same family of functions. The examples below use `context/readerioresult` (`RIO`), but the identical API is available in `result`, `option`, `ioresult`, `readerioresult`, and others.
|
||||
|
||||
### The Function Family
|
||||
|
||||
| Function | Kind | What it does |
|
||||
|----------|------|-------------|
|
||||
| `Do(empty S)` | — | Lift an empty struct into the monad; starting point |
|
||||
| `BindTo(setter)` | monadic | Convert an existing `M[T]` into `M[S]`; alternative start |
|
||||
| `Bind(setter, f)` | monadic | Add a result; `f` receives the **current state** and returns `M[T]` |
|
||||
| `ApS(setter, fa)` | applicative | Add a result; `fa` is **independent** of the current state |
|
||||
| `Let(setter, f)` | pure | Add a value computed by a **pure function** of the state |
|
||||
| `LetTo(setter, value)` | pure | Add a **constant** value |
|
||||
|
||||
Lens variants (`BindL`, `ApSL`, `LetL`, `LetToL`) accept a `Lens[S, T]` instead of a manual setter, integrating naturally with the optics system.
|
||||
|
||||
### `Bind` — Sequential, Dependent Steps
|
||||
|
||||
`Bind` sequences two monadic computations. The function `f` receives the **full accumulated state** so it can read anything gathered so far. Errors short-circuit automatically.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"context"
|
||||
)
|
||||
|
||||
type Pipeline struct {
|
||||
User User
|
||||
Config Config
|
||||
Posts []Post
|
||||
}
|
||||
|
||||
// Lenses — focus on individual fields; .Set is already func(T) func(S) S
|
||||
var (
|
||||
userLens = L.MakeLens(func(s Pipeline) User { return s.User }, func(s Pipeline, u User) Pipeline { s.User = u; return s })
|
||||
configLens = L.MakeLens(func(s Pipeline) Config { return s.Config }, func(s Pipeline, c Config) Pipeline { s.Config = c; return s })
|
||||
postsLens = L.MakeLens(func(s Pipeline) []Post { return s.Posts }, func(s Pipeline, p []Post) Pipeline { s.Posts = p; return s })
|
||||
)
|
||||
|
||||
result := F.Pipe3(
|
||||
RIO.Do(Pipeline{}), // lift empty struct
|
||||
RIO.Bind(userLens.Set, func(_ Pipeline) RIO.ReaderIOResult[User] { return fetchUser(42) }),
|
||||
RIO.Bind(configLens.Set, F.Flow2(userLens.Get, fetchConfigForUser)), // read s.User, pass to fetcher
|
||||
RIO.Bind(postsLens.Set, F.Flow2(userLens.Get, fetchPostsForUser)), // read s.User, pass to fetcher
|
||||
)
|
||||
|
||||
pipeline, err := result(context.Background())()
|
||||
// pipeline.User, pipeline.Config, pipeline.Posts are all populated
|
||||
```
|
||||
|
||||
The setter signature is `func(T) func(S1) S2` — it takes the new value and returns a state transformer. `lens.Set` already has this shape, so no manual setter functions are needed. `F.Flow2(lens.Get, f)` composes the field getter with any Kleisli arrow `f` point-free.
|
||||
|
||||
### `ApS` — Independent, Applicative Steps
|
||||
|
||||
`ApS` uses **applicative** semantics: `fa` is evaluated without any access to the current state. Use it when steps have no dependency on each other — the library can choose to execute them concurrently.
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
type Summary struct {
|
||||
User User
|
||||
Weather Weather
|
||||
}
|
||||
|
||||
var (
|
||||
userLens = L.MakeLens(func(s Summary) User { return s.User }, func(s Summary, u User) Summary { s.User = u; return s })
|
||||
weatherLens = L.MakeLens(func(s Summary) Weather { return s.Weather }, func(s Summary, w Weather) Summary { s.Weather = w; return s })
|
||||
)
|
||||
|
||||
// Both are independent — neither needs the other's result
|
||||
result := F.Pipe2(
|
||||
RIO.Do(Summary{}),
|
||||
RIO.ApS(userLens.Set, fetchUser(42)),
|
||||
RIO.ApS(weatherLens.Set, fetchWeather("NYC")),
|
||||
)
|
||||
```
|
||||
|
||||
**Key difference from `Bind`:**
|
||||
|
||||
| | `Bind(setter, f)` | `ApS(setter, fa)` |
|
||||
|-|---|---|
|
||||
| Second argument | `func(S1) M[T]` — a **function** of state | `M[T]` — a **fixed** monadic value |
|
||||
| Can read prior state? | Yes — receives `S1` | No — no access to state |
|
||||
| Semantics | Monadic (sequential) | Applicative (independent) |
|
||||
|
||||
### `Let` and `LetTo` — Pure Additions
|
||||
|
||||
`Let` adds a value computed by a **pure function** of the current state (no monad, cannot fail):
|
||||
|
||||
```go
|
||||
import (
|
||||
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
type Enriched struct {
|
||||
User User
|
||||
FullName string
|
||||
}
|
||||
|
||||
var (
|
||||
userLens = L.MakeLens(func(s Enriched) User { return s.User }, func(s Enriched, u User) Enriched { s.User = u; return s })
|
||||
fullNameLens = L.MakeLens(func(s Enriched) string { return s.FullName }, func(s Enriched, n string) Enriched { s.FullName = n; return s })
|
||||
)
|
||||
|
||||
fullName := func(u User) string { return u.FirstName + " " + u.LastName }
|
||||
|
||||
result := F.Pipe2(
|
||||
RIO.Do(Enriched{}),
|
||||
RIO.Bind(userLens.Set, func(_ Enriched) RIO.ReaderIOResult[User] { return fetchUser(42) }),
|
||||
RIO.Let(fullNameLens.Set, F.Flow2(userLens.Get, fullName)), // read s.User, compute pure string
|
||||
)
|
||||
```
|
||||
|
||||
`LetTo` adds a **constant** with no computation:
|
||||
|
||||
```go
|
||||
RIO.LetTo(setVersion, "v1.2.3")
|
||||
```
|
||||
|
||||
### `BindTo` — Starting from an Existing Value
|
||||
|
||||
When you have an existing `M[T]` and want to project it into a state struct rather than starting from `Do(empty)`:
|
||||
|
||||
```go
|
||||
type State struct{ User User }
|
||||
|
||||
result := F.Pipe1(
|
||||
fetchUser(42), // ReaderIOResult[User]
|
||||
RIO.BindTo(func(u User) State { return State{User: u} }),// ReaderIOResult[State]
|
||||
)
|
||||
```
|
||||
|
||||
### Lens Variants (`ApSL`, `BindL`, `LetL`, `LetToL`)
|
||||
|
||||
If you have a `Lens[S, T]` (from the optics system or code generation), you can skip writing the setter function entirely:
|
||||
|
||||
```go
|
||||
import (
|
||||
RO "github.com/IBM/fp-go/v2/readeroption"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Lenses generated by go:generate (see optics/README.md)
|
||||
// personLenses.Name : Lens[*Person, Name]
|
||||
// personLenses.Age : Lens[*Person, Age]
|
||||
|
||||
makePerson := F.Pipe2(
|
||||
RO.Do[*PartialPerson](emptyPerson),
|
||||
RO.ApSL(personLenses.Name, maybeName), // replaces: ApS(personLenses.Name.Set, maybeName)
|
||||
RO.ApSL(personLenses.Age, maybeAge),
|
||||
)
|
||||
```
|
||||
|
||||
This exact pattern is used in [`samples/builder`](samples/builder/builder.go) to validate and construct a `Person` from an unvalidated `PartialPerson`.
|
||||
|
||||
### Lifted Variants for Mixed Monads
|
||||
|
||||
`context/readerioresult` provides `Bind*K` helpers that lift simpler computations directly into the do-chain:
|
||||
|
||||
| Helper | Lifts |
|
||||
|--------|-------|
|
||||
| `BindResultK` / `BindEitherK` | `func(S1) (T, error)` — pure result |
|
||||
| `BindIOResultK` / `BindIOEitherK` | `func(S1) func() (T, error)` — lazy IO result |
|
||||
| `BindIOK` | `func(S1) func() T` — infallible IO |
|
||||
| `BindReaderK` | `func(S1) func(ctx) T` — context reader |
|
||||
|
||||
```go
|
||||
RIO.BindResultK(setUser, func(s Pipeline) (User, error) {
|
||||
return validateAndBuild(s) // plain (value, error) function, no wrapping needed
|
||||
})
|
||||
```
|
||||
|
||||
### Decision Guide
|
||||
|
||||
```
|
||||
Does the new step need to read prior accumulated state?
|
||||
YES → Bind (monadic, sequential; f receives current S)
|
||||
NO → ApS (applicative, independent; fa is a fixed M[T])
|
||||
|
||||
Is the new value derived purely from state, with no monad?
|
||||
YES → Let (pure function of S)
|
||||
|
||||
Is the new value a compile-time or runtime constant?
|
||||
YES → LetTo
|
||||
|
||||
Starting from an existing M[T] rather than an empty struct?
|
||||
YES → BindTo
|
||||
```
|
||||
|
||||
### Complete Example — `result` Monad
|
||||
|
||||
The same pattern works with simpler monads. Here with `result.Result[A]`:
|
||||
|
||||
`Eitherize1` converts any standard `func(A) (B, error)` into `func(A) Result[B]`. Define these lifted functions once as variables. Then use lenses to focus on individual struct fields and compose with `F.Flow2(lens.Get, f)` — no inline lambdas, no manual error handling.
|
||||
|
||||
```go
|
||||
import (
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Parsed struct {
|
||||
Raw string
|
||||
Number int
|
||||
Double int
|
||||
}
|
||||
|
||||
// Lenses — focus on individual fields of Parsed.
|
||||
var (
|
||||
rawLens = L.MakeLens(
|
||||
func(s Parsed) string { return s.Raw },
|
||||
func(s Parsed, v string) Parsed { s.Raw = v; return s },
|
||||
)
|
||||
numberLens = L.MakeLens(
|
||||
func(s Parsed) int { return s.Number },
|
||||
func(s Parsed, v int) Parsed { s.Number = v; return s },
|
||||
)
|
||||
doubleLens = L.MakeLens(
|
||||
func(s Parsed) int { return s.Double },
|
||||
func(s Parsed, v int) Parsed { s.Double = v; return s },
|
||||
)
|
||||
)
|
||||
|
||||
// Lifted functions — convert standard (value, error) functions into Result-returning ones.
|
||||
var (
|
||||
atoi = R.Eitherize1(strconv.Atoi) // func(string) Result[int]
|
||||
)
|
||||
|
||||
parse := func(input string) R.Result[Parsed] {
|
||||
return F.Pipe3(
|
||||
R.Do(Parsed{}),
|
||||
R.LetTo(rawLens.Set, input), // set Raw to constant input
|
||||
R.Bind(numberLens.Set, F.Flow2(rawLens.Get, atoi)), // get Raw, parse → Result[int]
|
||||
R.Let(doubleLens.Set, F.Flow2(numberLens.Get, N.Mul(2))), // get Number, multiply → int
|
||||
)
|
||||
}
|
||||
|
||||
parse("21") // Ok(Parsed{Raw:"21", Number:21, Double:42})
|
||||
parse("abc") // Error(strconv parse error)
|
||||
```
|
||||
|
||||
`rawLens.Set` is already `func(string) func(Parsed) Parsed`, matching the setter signature `Bind` and `LetTo` expect — no manual setter functions to write. `F.Flow2(rawLens.Get, atoi)` composes the field getter with the eitherized parse function into a `Kleisli[Parsed, int]` without any intermediate lambda.
|
||||
|
||||
## Import Paths
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/effect"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
)
|
||||
```
|
||||
|
||||
Requires Go 1.24+ (generic type aliases).
|
||||
125
v2/AGENTS.md
125
v2/AGENTS.md
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go/v2)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
[](https://goreportcard.com/report/github.com/IBM/fp-go/v2)
|
||||
[](https://context7.com/ibm/fp-go)
|
||||
|
||||
**fp-go** is a comprehensive functional programming library for Go, bringing type-safe functional patterns inspired by [fp-ts](https://gcanti.github.io/fp-ts/) to the Go ecosystem. Version 2 leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
522
v2/array/array_nil_test.go
Normal file
522
v2/array/array_nil_test.go
Normal file
@@ -0,0 +1,522 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package array
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestNilSlice_IsEmpty verifies that IsEmpty handles nil slices correctly
|
||||
func TestNilSlice_IsEmpty(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.True(t, IsEmpty(nilSlice), "nil slice should be empty")
|
||||
}
|
||||
|
||||
// TestNilSlice_IsNonEmpty verifies that IsNonEmpty handles nil slices correctly
|
||||
func TestNilSlice_IsNonEmpty(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.False(t, IsNonEmpty(nilSlice), "nil slice should not be non-empty")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadMap verifies that MonadMap handles nil slices correctly
|
||||
func TestNilSlice_MonadMap(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadMap(nilSlice, func(v int) string {
|
||||
return fmt.Sprintf("%d", v)
|
||||
})
|
||||
assert.NotNil(t, result, "MonadMap should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MonadMap should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadMapRef verifies that MonadMapRef handles nil slices correctly
|
||||
func TestNilSlice_MonadMapRef(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadMapRef(nilSlice, func(v *int) string {
|
||||
return fmt.Sprintf("%d", *v)
|
||||
})
|
||||
assert.NotNil(t, result, "MonadMapRef should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MonadMapRef should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Map verifies that Map handles nil slices correctly
|
||||
func TestNilSlice_Map(t *testing.T) {
|
||||
var nilSlice []int
|
||||
mapper := Map(func(v int) string {
|
||||
return fmt.Sprintf("%d", v)
|
||||
})
|
||||
result := mapper(nilSlice)
|
||||
assert.NotNil(t, result, "Map should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Map should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_MapRef verifies that MapRef handles nil slices correctly
|
||||
func TestNilSlice_MapRef(t *testing.T) {
|
||||
var nilSlice []int
|
||||
mapper := MapRef(func(v *int) string {
|
||||
return fmt.Sprintf("%d", *v)
|
||||
})
|
||||
result := mapper(nilSlice)
|
||||
assert.NotNil(t, result, "MapRef should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MapRef should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_MapWithIndex verifies that MapWithIndex handles nil slices correctly
|
||||
func TestNilSlice_MapWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
mapper := MapWithIndex(func(i int, v int) string {
|
||||
return fmt.Sprintf("%d:%d", i, v)
|
||||
})
|
||||
result := mapper(nilSlice)
|
||||
assert.NotNil(t, result, "MapWithIndex should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MapWithIndex should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Filter verifies that Filter handles nil slices correctly
|
||||
func TestNilSlice_Filter(t *testing.T) {
|
||||
var nilSlice []int
|
||||
filter := Filter(func(v int) bool {
|
||||
return v > 0
|
||||
})
|
||||
result := filter(nilSlice)
|
||||
assert.NotNil(t, result, "Filter should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Filter should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_FilterWithIndex verifies that FilterWithIndex handles nil slices correctly
|
||||
func TestNilSlice_FilterWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
filter := FilterWithIndex(func(i int, v int) bool {
|
||||
return v > 0
|
||||
})
|
||||
result := filter(nilSlice)
|
||||
assert.NotNil(t, result, "FilterWithIndex should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "FilterWithIndex should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_FilterRef verifies that FilterRef handles nil slices correctly
|
||||
func TestNilSlice_FilterRef(t *testing.T) {
|
||||
var nilSlice []int
|
||||
filter := FilterRef(func(v *int) bool {
|
||||
return *v > 0
|
||||
})
|
||||
result := filter(nilSlice)
|
||||
assert.NotNil(t, result, "FilterRef should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "FilterRef should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadFilterMap verifies that MonadFilterMap handles nil slices correctly
|
||||
func TestNilSlice_MonadFilterMap(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadFilterMap(nilSlice, func(v int) O.Option[string] {
|
||||
return O.Some(fmt.Sprintf("%d", v))
|
||||
})
|
||||
assert.NotNil(t, result, "MonadFilterMap should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MonadFilterMap should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadFilterMapWithIndex verifies that MonadFilterMapWithIndex handles nil slices correctly
|
||||
func TestNilSlice_MonadFilterMapWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadFilterMapWithIndex(nilSlice, func(i int, v int) O.Option[string] {
|
||||
return O.Some(fmt.Sprintf("%d:%d", i, v))
|
||||
})
|
||||
assert.NotNil(t, result, "MonadFilterMapWithIndex should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MonadFilterMapWithIndex should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_FilterMap verifies that FilterMap handles nil slices correctly
|
||||
func TestNilSlice_FilterMap(t *testing.T) {
|
||||
var nilSlice []int
|
||||
filter := FilterMap(func(v int) O.Option[string] {
|
||||
return O.Some(fmt.Sprintf("%d", v))
|
||||
})
|
||||
result := filter(nilSlice)
|
||||
assert.NotNil(t, result, "FilterMap should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "FilterMap should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_FilterMapWithIndex verifies that FilterMapWithIndex handles nil slices correctly
|
||||
func TestNilSlice_FilterMapWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
filter := FilterMapWithIndex(func(i int, v int) O.Option[string] {
|
||||
return O.Some(fmt.Sprintf("%d:%d", i, v))
|
||||
})
|
||||
result := filter(nilSlice)
|
||||
assert.NotNil(t, result, "FilterMapWithIndex should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "FilterMapWithIndex should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadReduce verifies that MonadReduce handles nil slices correctly
|
||||
func TestNilSlice_MonadReduce(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadReduce(nilSlice, func(acc int, v int) int {
|
||||
return acc + v
|
||||
}, 10)
|
||||
assert.Equal(t, 10, result, "MonadReduce should return initial value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadReduceWithIndex verifies that MonadReduceWithIndex handles nil slices correctly
|
||||
func TestNilSlice_MonadReduceWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadReduceWithIndex(nilSlice, func(i int, acc int, v int) int {
|
||||
return acc + v
|
||||
}, 10)
|
||||
assert.Equal(t, 10, result, "MonadReduceWithIndex should return initial value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Reduce verifies that Reduce handles nil slices correctly
|
||||
func TestNilSlice_Reduce(t *testing.T) {
|
||||
var nilSlice []int
|
||||
reducer := Reduce(func(acc int, v int) int {
|
||||
return acc + v
|
||||
}, 10)
|
||||
result := reducer(nilSlice)
|
||||
assert.Equal(t, 10, result, "Reduce should return initial value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_ReduceWithIndex verifies that ReduceWithIndex handles nil slices correctly
|
||||
func TestNilSlice_ReduceWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
reducer := ReduceWithIndex(func(i int, acc int, v int) int {
|
||||
return acc + v
|
||||
}, 10)
|
||||
result := reducer(nilSlice)
|
||||
assert.Equal(t, 10, result, "ReduceWithIndex should return initial value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_ReduceRight verifies that ReduceRight handles nil slices correctly
|
||||
func TestNilSlice_ReduceRight(t *testing.T) {
|
||||
var nilSlice []int
|
||||
reducer := ReduceRight(func(v int, acc int) int {
|
||||
return acc + v
|
||||
}, 10)
|
||||
result := reducer(nilSlice)
|
||||
assert.Equal(t, 10, result, "ReduceRight should return initial value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_ReduceRightWithIndex verifies that ReduceRightWithIndex handles nil slices correctly
|
||||
func TestNilSlice_ReduceRightWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
reducer := ReduceRightWithIndex(func(i int, v int, acc int) int {
|
||||
return acc + v
|
||||
}, 10)
|
||||
result := reducer(nilSlice)
|
||||
assert.Equal(t, 10, result, "ReduceRightWithIndex should return initial value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_ReduceRef verifies that ReduceRef handles nil slices correctly
|
||||
func TestNilSlice_ReduceRef(t *testing.T) {
|
||||
var nilSlice []int
|
||||
reducer := ReduceRef(func(acc int, v *int) int {
|
||||
return acc + *v
|
||||
}, 10)
|
||||
result := reducer(nilSlice)
|
||||
assert.Equal(t, 10, result, "ReduceRef should return initial value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Append verifies that Append handles nil slices correctly
|
||||
func TestNilSlice_Append(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := Append(nilSlice, 42)
|
||||
assert.NotNil(t, result, "Append should return non-nil slice")
|
||||
assert.Equal(t, 1, len(result), "Append should create slice with one element")
|
||||
assert.Equal(t, 42, result[0], "Append should add element correctly")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadChain verifies that MonadChain handles nil slices correctly
|
||||
func TestNilSlice_MonadChain(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadChain(nilSlice, func(v int) []string {
|
||||
return []string{fmt.Sprintf("%d", v)}
|
||||
})
|
||||
assert.NotNil(t, result, "MonadChain should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MonadChain should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Chain verifies that Chain handles nil slices correctly
|
||||
func TestNilSlice_Chain(t *testing.T) {
|
||||
var nilSlice []int
|
||||
chain := Chain(func(v int) []string {
|
||||
return []string{fmt.Sprintf("%d", v)}
|
||||
})
|
||||
result := chain(nilSlice)
|
||||
assert.NotNil(t, result, "Chain should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Chain should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadAp verifies that MonadAp handles nil slices correctly
|
||||
func TestNilSlice_MonadAp(t *testing.T) {
|
||||
var nilFuncs []func(int) string
|
||||
var nilValues []int
|
||||
|
||||
// nil functions, nil values
|
||||
result1 := MonadAp(nilFuncs, nilValues)
|
||||
assert.NotNil(t, result1, "MonadAp should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result1), "MonadAp should return empty slice for nil inputs")
|
||||
|
||||
// nil functions, non-nil values
|
||||
nonNilValues := []int{1, 2, 3}
|
||||
result2 := MonadAp(nilFuncs, nonNilValues)
|
||||
assert.NotNil(t, result2, "MonadAp should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result2), "MonadAp should return empty slice when functions are nil")
|
||||
|
||||
// non-nil functions, nil values
|
||||
nonNilFuncs := []func(int) string{func(v int) string { return fmt.Sprintf("%d", v) }}
|
||||
result3 := MonadAp(nonNilFuncs, nilValues)
|
||||
assert.NotNil(t, result3, "MonadAp should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result3), "MonadAp should return empty slice when values are nil")
|
||||
}
|
||||
|
||||
// TestNilSlice_Ap verifies that Ap handles nil slices correctly
|
||||
func TestNilSlice_Ap(t *testing.T) {
|
||||
var nilValues []int
|
||||
ap := Ap[string](nilValues)
|
||||
|
||||
var nilFuncs []func(int) string
|
||||
result := ap(nilFuncs)
|
||||
assert.NotNil(t, result, "Ap should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Ap should return empty slice for nil inputs")
|
||||
}
|
||||
|
||||
// TestNilSlice_Head verifies that Head handles nil slices correctly
|
||||
func TestNilSlice_Head(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := Head(nilSlice)
|
||||
assert.True(t, O.IsNone(result), "Head should return None for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_First verifies that First handles nil slices correctly
|
||||
func TestNilSlice_First(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := First(nilSlice)
|
||||
assert.True(t, O.IsNone(result), "First should return None for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Last verifies that Last handles nil slices correctly
|
||||
func TestNilSlice_Last(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := Last(nilSlice)
|
||||
assert.True(t, O.IsNone(result), "Last should return None for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Tail verifies that Tail handles nil slices correctly
|
||||
func TestNilSlice_Tail(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := Tail(nilSlice)
|
||||
assert.True(t, O.IsNone(result), "Tail should return None for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Flatten verifies that Flatten handles nil slices correctly
|
||||
func TestNilSlice_Flatten(t *testing.T) {
|
||||
var nilSlice [][]int
|
||||
result := Flatten(nilSlice)
|
||||
assert.NotNil(t, result, "Flatten should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Flatten should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Lookup verifies that Lookup handles nil slices correctly
|
||||
func TestNilSlice_Lookup(t *testing.T) {
|
||||
var nilSlice []int
|
||||
lookup := Lookup[int](0)
|
||||
result := lookup(nilSlice)
|
||||
assert.True(t, O.IsNone(result), "Lookup should return None for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Size verifies that Size handles nil slices correctly
|
||||
func TestNilSlice_Size(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := Size(nilSlice)
|
||||
assert.Equal(t, 0, result, "Size should return 0 for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadPartition verifies that MonadPartition handles nil slices correctly
|
||||
func TestNilSlice_MonadPartition(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := MonadPartition(nilSlice, func(v int) bool {
|
||||
return v > 0
|
||||
})
|
||||
left := P.Head(result)
|
||||
right := P.Tail(result)
|
||||
assert.NotNil(t, left, "MonadPartition left should return non-nil slice")
|
||||
assert.NotNil(t, right, "MonadPartition right should return non-nil slice")
|
||||
assert.Equal(t, 0, len(left), "MonadPartition left should be empty for nil input")
|
||||
assert.Equal(t, 0, len(right), "MonadPartition right should be empty for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Partition verifies that Partition handles nil slices correctly
|
||||
func TestNilSlice_Partition(t *testing.T) {
|
||||
var nilSlice []int
|
||||
partition := Partition(func(v int) bool {
|
||||
return v > 0
|
||||
})
|
||||
result := partition(nilSlice)
|
||||
left := P.Head(result)
|
||||
right := P.Tail(result)
|
||||
assert.NotNil(t, left, "Partition left should return non-nil slice")
|
||||
assert.NotNil(t, right, "Partition right should return non-nil slice")
|
||||
assert.Equal(t, 0, len(left), "Partition left should be empty for nil input")
|
||||
assert.Equal(t, 0, len(right), "Partition right should be empty for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_IsNil verifies that IsNil handles nil slices correctly
|
||||
func TestNilSlice_IsNil(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.True(t, IsNil(nilSlice), "IsNil should return true for nil slice")
|
||||
|
||||
nonNilSlice := []int{}
|
||||
assert.False(t, IsNil(nonNilSlice), "IsNil should return false for non-nil empty slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_IsNonNil verifies that IsNonNil handles nil slices correctly
|
||||
func TestNilSlice_IsNonNil(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.False(t, IsNonNil(nilSlice), "IsNonNil should return false for nil slice")
|
||||
|
||||
nonNilSlice := []int{}
|
||||
assert.True(t, IsNonNil(nonNilSlice), "IsNonNil should return true for non-nil empty slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Copy verifies that Copy handles nil slices correctly
|
||||
func TestNilSlice_Copy(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := Copy(nilSlice)
|
||||
assert.NotNil(t, result, "Copy should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Copy should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_FoldMap verifies that FoldMap handles nil slices correctly
|
||||
func TestNilSlice_FoldMap(t *testing.T) {
|
||||
var nilSlice []int
|
||||
monoid := S.Monoid
|
||||
foldMap := FoldMap[int](monoid)(func(v int) string {
|
||||
return fmt.Sprintf("%d", v)
|
||||
})
|
||||
result := foldMap(nilSlice)
|
||||
assert.Equal(t, "", result, "FoldMap should return empty value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_FoldMapWithIndex verifies that FoldMapWithIndex handles nil slices correctly
|
||||
func TestNilSlice_FoldMapWithIndex(t *testing.T) {
|
||||
var nilSlice []int
|
||||
monoid := S.Monoid
|
||||
foldMap := FoldMapWithIndex[int](monoid)(func(i int, v int) string {
|
||||
return fmt.Sprintf("%d:%d", i, v)
|
||||
})
|
||||
result := foldMap(nilSlice)
|
||||
assert.Equal(t, "", result, "FoldMapWithIndex should return empty value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Fold verifies that Fold handles nil slices correctly
|
||||
func TestNilSlice_Fold(t *testing.T) {
|
||||
var nilSlice []string
|
||||
monoid := S.Monoid
|
||||
fold := Fold[string](monoid)
|
||||
result := fold(nilSlice)
|
||||
assert.Equal(t, "", result, "Fold should return empty value for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Concat verifies that Concat handles nil slices correctly
|
||||
func TestNilSlice_Concat(t *testing.T) {
|
||||
var nilSlice []int
|
||||
nonNilSlice := []int{1, 2, 3}
|
||||
|
||||
// nil concat non-nil
|
||||
concat1 := Concat(nonNilSlice)
|
||||
result1 := concat1(nilSlice)
|
||||
assert.Equal(t, nonNilSlice, result1, "nil concat non-nil should return non-nil slice")
|
||||
|
||||
// non-nil concat nil
|
||||
concat2 := Concat(nilSlice)
|
||||
result2 := concat2(nonNilSlice)
|
||||
assert.Equal(t, nonNilSlice, result2, "non-nil concat nil should return non-nil slice")
|
||||
|
||||
// nil concat nil
|
||||
concat3 := Concat(nilSlice)
|
||||
result3 := concat3(nilSlice)
|
||||
assert.Nil(t, result3, "nil concat nil should return nil")
|
||||
}
|
||||
|
||||
// TestNilSlice_MonadFlap verifies that MonadFlap handles nil slices correctly
|
||||
func TestNilSlice_MonadFlap(t *testing.T) {
|
||||
var nilSlice []func(int) string
|
||||
result := MonadFlap(nilSlice, 42)
|
||||
assert.NotNil(t, result, "MonadFlap should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "MonadFlap should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Flap verifies that Flap handles nil slices correctly
|
||||
func TestNilSlice_Flap(t *testing.T) {
|
||||
var nilSlice []func(int) string
|
||||
flap := Flap[string, int](42)
|
||||
result := flap(nilSlice)
|
||||
assert.NotNil(t, result, "Flap should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Flap should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Reverse verifies that Reverse handles nil slices correctly
|
||||
func TestNilSlice_Reverse(t *testing.T) {
|
||||
var nilSlice []int
|
||||
result := Reverse(nilSlice)
|
||||
assert.Nil(t, result, "Reverse should return nil for nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Extend verifies that Extend handles nil slices correctly
|
||||
func TestNilSlice_Extend(t *testing.T) {
|
||||
var nilSlice []int
|
||||
extend := Extend(func(as []int) string {
|
||||
return fmt.Sprintf("%v", as)
|
||||
})
|
||||
result := extend(nilSlice)
|
||||
assert.NotNil(t, result, "Extend should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Extend should return empty slice for nil input")
|
||||
}
|
||||
|
||||
// TestNilSlice_Empty verifies that Empty creates an empty non-nil slice
|
||||
func TestNilSlice_Empty(t *testing.T) {
|
||||
result := Empty[int]()
|
||||
assert.NotNil(t, result, "Empty should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Empty should return empty slice")
|
||||
assert.False(t, IsNil(result), "Empty should not return nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Zero verifies that Zero creates an empty non-nil slice
|
||||
func TestNilSlice_Zero(t *testing.T) {
|
||||
result := Zero[int]()
|
||||
assert.NotNil(t, result, "Zero should return non-nil slice")
|
||||
assert.Equal(t, 0, len(result), "Zero should return empty slice")
|
||||
assert.False(t, IsNil(result), "Zero should not return nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_ConstNil verifies that ConstNil returns a nil slice
|
||||
func TestNilSlice_ConstNil(t *testing.T) {
|
||||
result := ConstNil[int]()
|
||||
assert.Nil(t, result, "ConstNil should return nil slice")
|
||||
assert.True(t, IsNil(result), "ConstNil should return nil slice")
|
||||
}
|
||||
|
||||
// TestNilSlice_Of verifies that Of creates a proper singleton slice
|
||||
func TestNilSlice_Of(t *testing.T) {
|
||||
result := Of(42)
|
||||
assert.NotNil(t, result, "Of should return non-nil slice")
|
||||
assert.Equal(t, 1, len(result), "Of should create slice with one element")
|
||||
assert.Equal(t, 42, result[0], "Of should set value correctly")
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
130
v2/context/reader/reader.go
Normal file
130
v2/context/reader/reader.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package reader provides a specialization of the Reader monad for [context.Context].
|
||||
//
|
||||
// This package offers a context-aware Reader monad that simplifies working with
|
||||
// Go's [context.Context] in a functional programming style. It eliminates the need
|
||||
// to explicitly thread context through function calls while maintaining type safety
|
||||
// and composability.
|
||||
//
|
||||
// # Core Concept
|
||||
//
|
||||
// The Reader monad represents computations that depend on a shared environment.
|
||||
// In this package, that environment is fixed to [context.Context], making it
|
||||
// particularly useful for:
|
||||
//
|
||||
// - Request-scoped data propagation
|
||||
// - Cancellation and timeout handling
|
||||
// - Dependency injection via context values
|
||||
// - Avoiding explicit context parameter threading
|
||||
//
|
||||
// # Type Definitions
|
||||
//
|
||||
// - Reader[A]: A computation that depends on context.Context and produces A
|
||||
// - Kleisli[A, B]: A function from A to Reader[B] for composing computations
|
||||
// - Operator[A, B]: A transformation from Reader[A] to Reader[B]
|
||||
//
|
||||
// # Usage Pattern
|
||||
//
|
||||
// Instead of passing context explicitly through every function:
|
||||
//
|
||||
// func processUser(ctx context.Context, userID string) (User, error) {
|
||||
// user := fetchUser(ctx, userID)
|
||||
// profile := fetchProfile(ctx, user.ProfileID)
|
||||
// return enrichUser(ctx, user, profile), nil
|
||||
// }
|
||||
//
|
||||
// You can use Reader to compose context-dependent operations:
|
||||
//
|
||||
// fetchUser := func(userID string) Reader[User] {
|
||||
// return func(ctx context.Context) User {
|
||||
// // Use ctx for database access, cancellation, etc.
|
||||
// return queryDatabase(ctx, userID)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// processUser := func(userID string) Reader[User] {
|
||||
// return F.Pipe2(
|
||||
// fetchUser(userID),
|
||||
// reader.Chain(func(user User) Reader[Profile] {
|
||||
// return fetchProfile(user.ProfileID)
|
||||
// }),
|
||||
// reader.Map(func(profile Profile) User {
|
||||
// return enrichUser(user, profile)
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // Execute with context
|
||||
// ctx := context.Background()
|
||||
// user := processUser("user123")(ctx)
|
||||
//
|
||||
// # Integration with Standard Library
|
||||
//
|
||||
// This package works seamlessly with Go's standard [context] package:
|
||||
//
|
||||
// - Context cancellation and deadlines are preserved
|
||||
// - Context values can be accessed within Reader computations
|
||||
// - Readers can be composed with context-aware libraries
|
||||
//
|
||||
// # Relationship to Other Packages
|
||||
//
|
||||
// This package is a specialization of [github.com/IBM/fp-go/v2/reader] where
|
||||
// the environment type R is fixed to [context.Context]. For more general
|
||||
// Reader operations, see the base reader package.
|
||||
//
|
||||
// For combining Reader with other monads:
|
||||
// - [github.com/IBM/fp-go/v2/context/readerio]: Reader + IO effects
|
||||
// - [github.com/IBM/fp-go/v2/readeroption]: Reader + Option
|
||||
// - [github.com/IBM/fp-go/v2/readerresult]: Reader + Result (Either)
|
||||
//
|
||||
// # Example: HTTP Request Handler
|
||||
//
|
||||
// type RequestContext struct {
|
||||
// UserID string
|
||||
// RequestID string
|
||||
// }
|
||||
//
|
||||
// // Extract request context from context.Context
|
||||
// getRequestContext := func(ctx context.Context) RequestContext {
|
||||
// return RequestContext{
|
||||
// UserID: ctx.Value("userID").(string),
|
||||
// RequestID: ctx.Value("requestID").(string),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // A Reader that logs with request context
|
||||
// logInfo := func(message string) Reader[function.Void] {
|
||||
// return func(ctx context.Context) function.Void {
|
||||
// reqCtx := getRequestContext(ctx)
|
||||
// log.Printf("[%s] User %s: %s", reqCtx.RequestID, reqCtx.UserID, message)
|
||||
// return function.VOID
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose operations
|
||||
// handleRequest := func(data string) Reader[Response] {
|
||||
// return F.Pipe2(
|
||||
// logInfo("Processing request"),
|
||||
// reader.Chain(func(_ function.Void) Reader[Result] {
|
||||
// return processData(data)
|
||||
// }),
|
||||
// reader.Map(func(result Result) Response {
|
||||
// return Response{Data: result}
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
package reader
|
||||
142
v2/context/reader/types.go
Normal file
142
v2/context/reader/types.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package reader
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
// Reader represents a computation that depends on a [context.Context] and produces a value of type A.
|
||||
//
|
||||
// This is a specialization of the generic Reader monad where the environment type is fixed
|
||||
// to [context.Context]. This is particularly useful for Go applications that need to thread
|
||||
// context through computations for cancellation, deadlines, and request-scoped values.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type produced by the computation
|
||||
//
|
||||
// Reader[A] is equivalent to func(context.Context) A
|
||||
//
|
||||
// The Reader monad enables:
|
||||
// - Dependency injection using context values
|
||||
// - Cancellation and timeout handling
|
||||
// - Request-scoped data propagation
|
||||
// - Avoiding explicit context parameter threading
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A Reader that extracts a user ID from context
|
||||
// getUserID := func(ctx context.Context) string {
|
||||
// if userID, ok := ctx.Value("userID").(string); ok {
|
||||
// return userID
|
||||
// }
|
||||
// return "anonymous"
|
||||
// }
|
||||
//
|
||||
// // A Reader that checks if context is cancelled
|
||||
// isCancelled := func(ctx context.Context) bool {
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// return true
|
||||
// default:
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the readers with a context
|
||||
// ctx := context.WithValue(context.Background(), "userID", "user123")
|
||||
// userID := getUserID(ctx) // "user123"
|
||||
// cancelled := isCancelled(ctx) // false
|
||||
Reader[A any] = R.Reader[context.Context, A]
|
||||
|
||||
// Kleisli represents a Kleisli arrow for the context-based Reader monad.
|
||||
//
|
||||
// It's a function from A to Reader[B], used for composing Reader computations
|
||||
// that all depend on the same [context.Context].
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type
|
||||
// - B: The output type wrapped in Reader
|
||||
//
|
||||
// Kleisli[A, B] is equivalent to func(A) func(context.Context) B
|
||||
//
|
||||
// Kleisli arrows are fundamental for monadic composition, allowing you to chain
|
||||
// operations that depend on context without explicitly passing the context through
|
||||
// each function call.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A Kleisli arrow that creates a greeting Reader from a name
|
||||
// greet := func(name string) Reader[string] {
|
||||
// return func(ctx context.Context) string {
|
||||
// if deadline, ok := ctx.Deadline(); ok {
|
||||
// return fmt.Sprintf("Hello %s (deadline: %v)", name, deadline)
|
||||
// }
|
||||
// return fmt.Sprintf("Hello %s", name)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the Kleisli arrow
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
// defer cancel()
|
||||
// greeting := greet("Alice")(ctx) // "Hello Alice (deadline: ...)"
|
||||
Kleisli[A, B any] = R.Reader[A, Reader[B]]
|
||||
|
||||
// Operator represents a transformation from one Reader to another.
|
||||
//
|
||||
// It takes a Reader[A] and produces a Reader[B], where both readers depend on
|
||||
// the same [context.Context]. This type is commonly used for operations like
|
||||
// Map, Chain, and other transformations that convert readers while preserving
|
||||
// the context dependency.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input Reader's result type
|
||||
// - B: The output Reader's result type
|
||||
//
|
||||
// Operator[A, B] is equivalent to func(Reader[A]) func(context.Context) B
|
||||
//
|
||||
// Operators enable building pipelines of context-dependent computations where
|
||||
// each step can transform the result of the previous computation while maintaining
|
||||
// access to the shared context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // An operator that transforms int readers to string readers
|
||||
// intToString := func(r Reader[int]) Reader[string] {
|
||||
// return func(ctx context.Context) string {
|
||||
// value := r(ctx)
|
||||
// return strconv.Itoa(value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // A Reader that extracts a timeout value from context
|
||||
// getTimeout := func(ctx context.Context) int {
|
||||
// if deadline, ok := ctx.Deadline(); ok {
|
||||
// return int(time.Until(deadline).Seconds())
|
||||
// }
|
||||
// return 0
|
||||
// }
|
||||
//
|
||||
// // Transform the Reader
|
||||
// getTimeoutStr := intToString(getTimeout)
|
||||
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
// defer cancel()
|
||||
// result := getTimeoutStr(ctx) // "30" (approximately)
|
||||
Operator[A, B any] = Kleisli[Reader[A], B]
|
||||
)
|
||||
@@ -1,6 +1,7 @@
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
@@ -73,3 +74,117 @@ func Bracket[
|
||||
) ReaderIO[B] {
|
||||
return RIO.Bracket(acquire, use, release)
|
||||
}
|
||||
|
||||
// WithResource creates a higher-order function that manages a resource lifecycle for any operation.
|
||||
// It returns a Kleisli arrow that takes a use function and automatically handles resource
|
||||
// acquisition and cleanup using the bracket pattern.
|
||||
//
|
||||
// This is a more composable alternative to Bracket, allowing you to define resource management
|
||||
// once and reuse it with different use functions. The resource is acquired when the returned
|
||||
// Kleisli arrow is invoked, used by the provided function, and then released regardless of
|
||||
// success or failure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the resource to be managed
|
||||
// - B: The type of the result produced by the use function
|
||||
// - ANY: The type returned by the release function (typically ignored)
|
||||
//
|
||||
// Parameters:
|
||||
// - onCreate: A ReaderIO that acquires/creates the resource
|
||||
// - onRelease: A Kleisli arrow that releases/cleans up the resource
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a use function and returns a ReaderIO managing the full lifecycle
|
||||
//
|
||||
// Example with database connection:
|
||||
//
|
||||
// // Define resource management once
|
||||
// withDB := WithResource(
|
||||
// // Acquire connection
|
||||
// func(ctx context.Context) IO[*sql.DB] {
|
||||
// return func() *sql.DB {
|
||||
// db, _ := sql.Open("postgres", "connection-string")
|
||||
// return db
|
||||
// }
|
||||
// },
|
||||
// // Release connection
|
||||
// func(db *sql.DB) ReaderIO[any] {
|
||||
// return func(ctx context.Context) IO[any] {
|
||||
// return func() any {
|
||||
// db.Close()
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Reuse with different operations
|
||||
// queryUsers := withDB(func(db *sql.DB) ReaderIO[[]User] {
|
||||
// return func(ctx context.Context) IO[[]User] {
|
||||
// return func() []User {
|
||||
// // Query users from db
|
||||
// return users
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// insertUser := withDB(func(db *sql.DB) ReaderIO[int64] {
|
||||
// return func(ctx context.Context) IO[int64] {
|
||||
// return func() int64 {
|
||||
// // Insert user into db
|
||||
// return userID
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// Example with file handling:
|
||||
//
|
||||
// withFile := WithResource(
|
||||
// func(ctx context.Context) IO[*os.File] {
|
||||
// return func() *os.File {
|
||||
// f, _ := os.Open("data.txt")
|
||||
// return f
|
||||
// }
|
||||
// },
|
||||
// func(f *os.File) ReaderIO[any] {
|
||||
// return func(ctx context.Context) IO[any] {
|
||||
// return func() any {
|
||||
// f.Close()
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // Use for reading
|
||||
// readContent := withFile(func(f *os.File) ReaderIO[string] {
|
||||
// return func(ctx context.Context) IO[string] {
|
||||
// return func() string {
|
||||
// data, _ := io.ReadAll(f)
|
||||
// return string(data)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Use for getting file info
|
||||
// getSize := withFile(func(f *os.File) ReaderIO[int64] {
|
||||
// return func(ctx context.Context) IO[int64] {
|
||||
// return func() int64 {
|
||||
// info, _ := f.Stat()
|
||||
// return info.Size()
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// Use Cases:
|
||||
// - Database connections: Acquire connection, execute queries, close connection
|
||||
// - File handles: Open file, read/write, close file
|
||||
// - Network connections: Establish connection, transfer data, close connection
|
||||
// - Locks: Acquire lock, perform critical section, release lock
|
||||
// - Temporary resources: Create temp file/directory, use it, clean up
|
||||
//
|
||||
//go:inline
|
||||
func WithResource[A, B, ANY any](
|
||||
onCreate ReaderIO[A], onRelease Kleisli[A, ANY]) Kleisli[Kleisli[A, B], B] {
|
||||
return function.Bind13of3(Bracket[A, B, ANY])(onCreate, function.Ignore2of2[B](onRelease))
|
||||
}
|
||||
|
||||
454
v2/context/readerio/bracket_test.go
Normal file
454
v2/context/readerio/bracket_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// mockResource simulates a resource that tracks its lifecycle
|
||||
type mockResource struct {
|
||||
id int
|
||||
acquired bool
|
||||
released bool
|
||||
used bool
|
||||
}
|
||||
|
||||
// TestBracket_Success tests that Bracket properly manages resource lifecycle on success
|
||||
func TestBracket_Success(t *testing.T) {
|
||||
resource := &mockResource{id: 1}
|
||||
|
||||
// Acquire resource
|
||||
acquire := func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
resource.acquired = true
|
||||
return resource
|
||||
}
|
||||
}
|
||||
|
||||
// Use resource
|
||||
use := func(r *mockResource) ReaderIO[string] {
|
||||
return func(ctx context.Context) io.IO[string] {
|
||||
return func() string {
|
||||
r.used = true
|
||||
return "success"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release resource
|
||||
release := func(r *mockResource, result string) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
r.released = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute bracket
|
||||
operation := Bracket(acquire, use, release)
|
||||
result := operation(context.Background())()
|
||||
|
||||
// Verify lifecycle
|
||||
assert.True(t, resource.acquired, "Resource should be acquired")
|
||||
assert.True(t, resource.used, "Resource should be used")
|
||||
assert.True(t, resource.released, "Resource should be released")
|
||||
assert.Equal(t, "success", result)
|
||||
}
|
||||
|
||||
// TestBracket_MultipleResources tests managing multiple resources
|
||||
func TestBracket_MultipleResources(t *testing.T) {
|
||||
resource1 := &mockResource{id: 1}
|
||||
resource2 := &mockResource{id: 2}
|
||||
|
||||
acquire1 := func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
resource1.acquired = true
|
||||
return resource1
|
||||
}
|
||||
}
|
||||
|
||||
use1 := func(r1 *mockResource) ReaderIO[*mockResource] {
|
||||
return func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
r1.used = true
|
||||
resource2.acquired = true
|
||||
return resource2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
release1 := func(r1 *mockResource, result string) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
r1.released = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nested bracket for second resource
|
||||
use2 := func(r2 *mockResource) ReaderIO[string] {
|
||||
return func(ctx context.Context) io.IO[string] {
|
||||
return func() string {
|
||||
r2.used = true
|
||||
return "both used"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
release2 := func(r2 *mockResource, result string) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
r2.released = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose brackets
|
||||
operation := Bracket(acquire1, func(r1 *mockResource) ReaderIO[string] {
|
||||
return func(ctx context.Context) io.IO[string] {
|
||||
r2 := use1(r1)(ctx)()
|
||||
return Bracket(
|
||||
func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource { return r2 }
|
||||
},
|
||||
use2,
|
||||
release2,
|
||||
)(ctx)
|
||||
}
|
||||
}, release1)
|
||||
|
||||
result := operation(context.Background())()
|
||||
|
||||
assert.True(t, resource1.acquired)
|
||||
assert.True(t, resource1.used)
|
||||
assert.True(t, resource1.released)
|
||||
assert.True(t, resource2.acquired)
|
||||
assert.True(t, resource2.used)
|
||||
assert.True(t, resource2.released)
|
||||
assert.Equal(t, "both used", result)
|
||||
}
|
||||
|
||||
// TestWithResource_Success tests WithResource with successful operation
|
||||
func TestWithResource_Success(t *testing.T) {
|
||||
resource := &mockResource{id: 1}
|
||||
|
||||
// Define resource management
|
||||
withResource := WithResource[*mockResource, string, any](
|
||||
func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
resource.acquired = true
|
||||
return resource
|
||||
}
|
||||
},
|
||||
func(r *mockResource) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
r.released = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Use resource
|
||||
operation := withResource(func(r *mockResource) ReaderIO[string] {
|
||||
return func(ctx context.Context) io.IO[string] {
|
||||
return func() string {
|
||||
r.used = true
|
||||
return "result"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
result := operation(context.Background())()
|
||||
|
||||
assert.True(t, resource.acquired)
|
||||
assert.True(t, resource.used)
|
||||
assert.True(t, resource.released)
|
||||
assert.Equal(t, "result", result)
|
||||
}
|
||||
|
||||
// TestWithResource_Reusability tests that WithResource can be reused with different operations
|
||||
func TestWithResource_Reusability(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
withResource := WithResource[*mockResource, int, any](
|
||||
func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
callCount++
|
||||
return &mockResource{id: callCount, acquired: true}
|
||||
}
|
||||
},
|
||||
func(r *mockResource) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
r.released = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// First operation
|
||||
op1 := withResource(func(r *mockResource) ReaderIO[int] {
|
||||
return func(ctx context.Context) io.IO[int] {
|
||||
return func() int {
|
||||
r.used = true
|
||||
return r.id * 2
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
result1 := op1(context.Background())()
|
||||
assert.Equal(t, 2, result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Second operation (should create new resource)
|
||||
op2 := withResource(func(r *mockResource) ReaderIO[int] {
|
||||
return func(ctx context.Context) io.IO[int] {
|
||||
return func() int {
|
||||
r.used = true
|
||||
return r.id * 3
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
result2 := op2(context.Background())()
|
||||
assert.Equal(t, 6, result2)
|
||||
assert.Equal(t, 2, callCount)
|
||||
}
|
||||
|
||||
// TestWithResource_DifferentResultTypes tests WithResource with different result types
|
||||
func TestWithResource_DifferentResultTypes(t *testing.T) {
|
||||
resource := &mockResource{id: 42}
|
||||
|
||||
withResourceInt := WithResource[*mockResource, int, any](
|
||||
func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
resource.acquired = true
|
||||
return resource
|
||||
}
|
||||
},
|
||||
func(r *mockResource) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
r.released = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Operation returning int
|
||||
opInt := withResourceInt(func(r *mockResource) ReaderIO[int] {
|
||||
return func(ctx context.Context) io.IO[int] {
|
||||
return func() int {
|
||||
return r.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
resultInt := opInt(context.Background())()
|
||||
assert.Equal(t, 42, resultInt)
|
||||
|
||||
// Reset resource state
|
||||
resource.acquired = false
|
||||
resource.released = false
|
||||
|
||||
// Create new WithResource for string type
|
||||
withResourceString := WithResource[*mockResource, string, any](
|
||||
func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
resource.acquired = true
|
||||
return resource
|
||||
}
|
||||
},
|
||||
func(r *mockResource) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
r.released = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Operation returning string
|
||||
opString := withResourceString(func(r *mockResource) ReaderIO[string] {
|
||||
return func(ctx context.Context) io.IO[string] {
|
||||
return func() string {
|
||||
return "value"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
resultString := opString(context.Background())()
|
||||
assert.Equal(t, "value", resultString)
|
||||
assert.True(t, resource.released)
|
||||
}
|
||||
|
||||
// TestWithResource_ContextPropagation tests that context is properly propagated
|
||||
func TestWithResource_ContextPropagation(t *testing.T) {
|
||||
type contextKey string
|
||||
const key contextKey = "test-key"
|
||||
|
||||
withResource := WithResource[string, string, any](
|
||||
func(ctx context.Context) io.IO[string] {
|
||||
return func() string {
|
||||
value := ctx.Value(key)
|
||||
if value != nil {
|
||||
return value.(string)
|
||||
}
|
||||
return "no-value"
|
||||
}
|
||||
},
|
||||
func(r string) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
operation := withResource(func(r string) ReaderIO[string] {
|
||||
return func(ctx context.Context) io.IO[string] {
|
||||
return func() string {
|
||||
return r + "-processed"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, "test-value")
|
||||
result := operation(ctx)()
|
||||
|
||||
assert.Equal(t, "test-value-processed", result)
|
||||
}
|
||||
|
||||
// TestWithResource_ErrorInRelease tests behavior when release function encounters an error
|
||||
func TestWithResource_ErrorInRelease(t *testing.T) {
|
||||
resource := &mockResource{id: 1}
|
||||
releaseError := errors.New("release failed")
|
||||
|
||||
withResource := WithResource[*mockResource, string, error](
|
||||
func(ctx context.Context) io.IO[*mockResource] {
|
||||
return func() *mockResource {
|
||||
resource.acquired = true
|
||||
return resource
|
||||
}
|
||||
},
|
||||
func(r *mockResource) ReaderIO[error] {
|
||||
return func(ctx context.Context) io.IO[error] {
|
||||
return func() error {
|
||||
r.released = true
|
||||
return releaseError
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
operation := withResource(func(r *mockResource) ReaderIO[string] {
|
||||
return func(ctx context.Context) io.IO[string] {
|
||||
return func() string {
|
||||
r.used = true
|
||||
return "success"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
result := operation(context.Background())()
|
||||
|
||||
// Operation should succeed even if release returns error
|
||||
assert.Equal(t, "success", result)
|
||||
assert.True(t, resource.acquired)
|
||||
assert.True(t, resource.used)
|
||||
assert.True(t, resource.released)
|
||||
}
|
||||
|
||||
// BenchmarkBracket benchmarks the Bracket function
|
||||
func BenchmarkBracket(b *testing.B) {
|
||||
acquire := func(ctx context.Context) io.IO[int] {
|
||||
return func() int {
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
use := func(n int) ReaderIO[int] {
|
||||
return func(ctx context.Context) io.IO[int] {
|
||||
return func() int {
|
||||
return n * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
release := func(n int, result int) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operation := Bracket(acquire, use, release)
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
operation(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkWithResource benchmarks the WithResource function
|
||||
func BenchmarkWithResource(b *testing.B) {
|
||||
withResource := WithResource[int, int, any](
|
||||
func(ctx context.Context) io.IO[int] {
|
||||
return func() int {
|
||||
return 42
|
||||
}
|
||||
},
|
||||
func(n int) ReaderIO[any] {
|
||||
return func(ctx context.Context) io.IO[any] {
|
||||
return func() any {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
operation := withResource(func(n int) ReaderIO[int] {
|
||||
return func(ctx context.Context) io.IO[int] {
|
||||
return func() int {
|
||||
return n * 2
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
operation(ctx)()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
|
||||
@@ -33,21 +36,24 @@ import (
|
||||
// The function f returns both a new context and a CancelFunc that should be called to release resources.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
// - A: The original result type produced by the ReaderIO
|
||||
// - B: The new output result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - f: Function to transform the input environment R into context.Context (contravariant)
|
||||
// - g: Function to transform the output value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
|
||||
// - A Kleisli arrow that takes a ReaderIO[A] and returns a function from R to B
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RIO.Kleisli[R, ReaderIO[A], B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
RIO.Map[R](g),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,14 +67,87 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[A]
|
||||
// - A Kleisli arrow that takes a ReaderIO[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIO.Kleisli[R, ReaderIO[A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the context using an IO effect before passing it to a ReaderIO computation.
|
||||
//
|
||||
// This is similar to Local, but the context transformation itself is wrapped in an IO effect,
|
||||
// allowing for side-effectful context transformations. The transformation function receives
|
||||
// the current context and returns an IO effect that produces a new context along with a
|
||||
// cancel function. The cancel function is automatically called when the computation completes
|
||||
// (via defer), ensuring proper cleanup of resources.
|
||||
//
|
||||
// This is useful for:
|
||||
// - Context transformations that require side effects (e.g., loading configuration)
|
||||
// - Lazy initialization of context values
|
||||
// - Context transformations that may fail or need to perform I/O
|
||||
// - Composing effectful context setup with computations
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type of the ReaderIO
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IO Kleisli arrow that transforms the context with side effects
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that runs the computation with the effectfully transformed context
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// "context"
|
||||
// G "github.com/IBM/fp-go/v2/io"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// )
|
||||
//
|
||||
// // Context transformation with side effects (e.g., loading config)
|
||||
// loadConfig := func(ctx context.Context) G.IO[ContextCancel] {
|
||||
// return func() ContextCancel {
|
||||
// // Simulate loading configuration
|
||||
// config := loadConfigFromFile()
|
||||
// newCtx := context.WithValue(ctx, "config", config)
|
||||
// return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// getValue := readerio.FromReader(func(ctx context.Context) string {
|
||||
// if cfg := ctx.Value("config"); cfg != nil {
|
||||
// return cfg.(string)
|
||||
// }
|
||||
// return "default"
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// getValue,
|
||||
// readerio.LocalIOK[string](loadConfig),
|
||||
// )
|
||||
// value := result(t.Context())() // Loads config and uses it
|
||||
//
|
||||
// Comparison with Local:
|
||||
// - Local: Takes a pure function that transforms the context
|
||||
// - LocalIOK: Takes an IO effect that transforms the context, allowing side effects
|
||||
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
|
||||
return func(r ReaderIO[A]) ReaderIO[A] {
|
||||
return func(ctx context.Context) IO[A] {
|
||||
p := f(ctx)
|
||||
return func() A {
|
||||
otherCancel, otherCtx := pair.Unpack(p())
|
||||
defer otherCancel()
|
||||
return r(otherCtx)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -38,9 +39,9 @@ func TestPromapBasic(t *testing.T) {
|
||||
}
|
||||
|
||||
// Transform context and result
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
@@ -63,9 +64,9 @@ func TestContramapBasic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
@@ -85,8 +86,9 @@ func TestLocalBasic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
addTimeout := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, time.Second)
|
||||
addTimeout := func(ctx context.Context) ContextCancel {
|
||||
newCtx, cancelFct := context.WithTimeout(ctx, time.Second)
|
||||
return pair.MakePair(cancelFct, newCtx)
|
||||
}
|
||||
|
||||
adapted := Local[bool](addTimeout)(getValue)
|
||||
@@ -95,3 +97,81 @@ func TestLocalBasic(t *testing.T) {
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOKBasic tests basic LocalIOK functionality
|
||||
func TestLocalIOKBasic(t *testing.T) {
|
||||
t.Run("context transformation with IO effect", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IO[string] {
|
||||
return func() string {
|
||||
if v := ctx.Value("key"); v != nil {
|
||||
return v.(string)
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
}
|
||||
|
||||
// Context transformation wrapped in IO effect
|
||||
addKeyIO := func(ctx context.Context) IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
// Simulate side effect (e.g., loading config)
|
||||
newCtx := context.WithValue(ctx, "key", "loaded-value")
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string](addKeyIO)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, "loaded-value", result)
|
||||
})
|
||||
|
||||
t.Run("cleanup function is called", func(t *testing.T) {
|
||||
cleanupCalled := false
|
||||
|
||||
getValue := func(ctx context.Context) IO[int] {
|
||||
return func() int {
|
||||
if v := ctx.Value("value"); v != nil {
|
||||
return v.(int)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
addValueIO := func(ctx context.Context) IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "value", 42)
|
||||
cleanup := context.CancelFunc(func() {
|
||||
cleanupCalled = true
|
||||
})
|
||||
return pair.MakePair(cleanup, newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[int](addValueIO)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
assert.True(t, cleanupCalled, "cleanup function should be called")
|
||||
})
|
||||
|
||||
t.Run("works with timeout context", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IO[bool] {
|
||||
return func() bool {
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
return hasDeadline
|
||||
}
|
||||
}
|
||||
|
||||
addTimeoutIO := func(ctx context.Context) IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx, cancelFct := context.WithTimeout(ctx, time.Second)
|
||||
return pair.MakePair(cancelFct, newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[bool](addTimeoutIO)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.True(t, result, "context should have deadline")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIO "github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
@@ -633,12 +634,15 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type of the ReaderIO
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms the context and returns a cancel function
|
||||
// - f: A function that transforms the input environment R into context.Context and returns a cancel function
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that runs the computation with the transformed context
|
||||
// - A Kleisli arrow that runs the computation with the transformed context
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -648,9 +652,9 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// type key int
|
||||
// const userKey key = 0
|
||||
//
|
||||
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// addUser := readerio.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||
// return newCtx, func() {} // No-op cancel
|
||||
// return pair.MakePair(func() {}, newCtx) // No-op cancel
|
||||
// })
|
||||
//
|
||||
// getUser := readerio.FromReader(func(ctx context.Context) string {
|
||||
@@ -669,19 +673,20 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
|
||||
// Timeout Example:
|
||||
//
|
||||
// // Add a 5-second timeout to a specific operation
|
||||
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 5*time.Second)
|
||||
// withTimeout := readerio.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// fetchData,
|
||||
// withTimeout,
|
||||
// )
|
||||
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return func(rr ReaderIO[A]) ReaderIO[A] {
|
||||
return func(ctx context.Context) IO[A] {
|
||||
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIO.Kleisli[R, ReaderIO[A], A] {
|
||||
return func(rr ReaderIO[A]) RIO.ReaderIO[R, A] {
|
||||
return func(r R) IO[A] {
|
||||
return func() A {
|
||||
otherCtx, otherCancel := f(ctx)
|
||||
otherCancel, otherCtx := pair.Unpack(f(r))
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)()
|
||||
}
|
||||
@@ -742,8 +747,9 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
|
||||
// )
|
||||
// data := result(t.Context())() // Returns Data{Value: "quick"}
|
||||
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(ctx, timeout)
|
||||
return Local[A](func(ctx context.Context) ContextCancel {
|
||||
newCtx, cancelFct := context.WithTimeout(ctx, timeout)
|
||||
return pair.MakePair(cancelFct, newCtx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -806,8 +812,9 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
// )
|
||||
// data := result(parentCtx)() // Will use parent's 1-hour deadline
|
||||
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
|
||||
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
return context.WithDeadline(ctx, deadline)
|
||||
return Local[A](func(ctx context.Context) ContextCancel {
|
||||
newCtx, cancelFct := context.WithDeadline(ctx, deadline)
|
||||
return pair.MakePair(cancelFct, newCtx)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
@@ -81,4 +82,15 @@ type (
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
Void = function.Void
|
||||
|
||||
// Pair represents a tuple of two values of types A and B.
|
||||
// It is used to group two related values together.
|
||||
Pair[A, B any] = pair.Pair[A, B]
|
||||
|
||||
// ContextCancel represents a pair of a cancel function and a context.
|
||||
// It is used in operations that create new contexts with cancellation capabilities.
|
||||
//
|
||||
// The first element is the CancelFunc that should be called to release resources.
|
||||
// The second element is the new Context that was created.
|
||||
ContextCancel = Pair[context.CancelFunc, context.Context]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -90,132 +91,7 @@ func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
|
||||
return F.Bind2nd(withLoggingContextValue, any(lctx))
|
||||
}
|
||||
|
||||
// LogEntryExitF creates a customizable operator that wraps a ReaderIOResult computation with entry/exit callbacks.
|
||||
//
|
||||
// This is a more flexible version of LogEntryExit that allows you to provide custom callbacks for
|
||||
// entry and exit events. The onEntry callback receives the current context and can return a modified
|
||||
// context (e.g., with additional logging information). The onExit callback receives the computation
|
||||
// result and can perform custom logging, metrics collection, or cleanup.
|
||||
//
|
||||
// The function uses the bracket pattern to ensure that:
|
||||
// - The onEntry callback is executed before the computation starts
|
||||
// - The computation runs with the context returned by onEntry
|
||||
// - The onExit callback is executed after the computation completes (success or failure)
|
||||
// - The original result is preserved and returned unchanged
|
||||
// - Cleanup happens even if the computation fails
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the ReaderIOResult
|
||||
// - ANY: The return type of the onExit callback (typically any)
|
||||
//
|
||||
// Parameters:
|
||||
// - onEntry: A ReaderIO that receives the current context and returns a (possibly modified) context.
|
||||
// This is executed before the computation starts. Use this for logging entry, adding context values,
|
||||
// starting timers, or initialization logic.
|
||||
// - onExit: A Kleisli function that receives the Result[A] and returns a ReaderIO[ANY].
|
||||
// This is executed after the computation completes, regardless of success or failure.
|
||||
// Use this for logging exit, recording metrics, cleanup, or finalization logic.
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that wraps the ReaderIOResult computation with the custom entry/exit callbacks
|
||||
//
|
||||
// Example with custom context modification:
|
||||
//
|
||||
// type RequestID string
|
||||
//
|
||||
// logOp := LogEntryExitF[User, any](
|
||||
// func(ctx context.Context) IO[context.Context] {
|
||||
// return func() context.Context {
|
||||
// reqID := RequestID(uuid.New().String())
|
||||
// log.Printf("[%s] Starting operation", reqID)
|
||||
// return context.WithValue(ctx, "requestID", reqID)
|
||||
// }
|
||||
// },
|
||||
// func(res Result[User]) ReaderIO[any] {
|
||||
// return func(ctx context.Context) IO[any] {
|
||||
// return func() any {
|
||||
// reqID := ctx.Value("requestID").(RequestID)
|
||||
// return F.Pipe1(
|
||||
// res,
|
||||
// result.Fold(
|
||||
// func(err error) any {
|
||||
// log.Printf("[%s] Operation failed: %v", reqID, err)
|
||||
// return nil
|
||||
// },
|
||||
// func(_ User) any {
|
||||
// log.Printf("[%s] Operation succeeded", reqID)
|
||||
// return nil
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// wrapped := logOp(fetchUser(123))
|
||||
//
|
||||
// Example with metrics collection:
|
||||
//
|
||||
// import "github.com/prometheus/client_golang/prometheus"
|
||||
//
|
||||
// metricsOp := LogEntryExitF[Response, any](
|
||||
// func(ctx context.Context) IO[context.Context] {
|
||||
// return func() context.Context {
|
||||
// requestCount.WithLabelValues("api_call", "started").Inc()
|
||||
// return context.WithValue(ctx, "startTime", time.Now())
|
||||
// }
|
||||
// },
|
||||
// func(res Result[Response]) ReaderIO[any] {
|
||||
// return func(ctx context.Context) IO[any] {
|
||||
// return func() any {
|
||||
// startTime := ctx.Value("startTime").(time.Time)
|
||||
// duration := time.Since(startTime).Seconds()
|
||||
//
|
||||
// return F.Pipe1(
|
||||
// res,
|
||||
// result.Fold(
|
||||
// func(err error) any {
|
||||
// requestCount.WithLabelValues("api_call", "error").Inc()
|
||||
// requestDuration.WithLabelValues("api_call", "error").Observe(duration)
|
||||
// return nil
|
||||
// },
|
||||
// func(_ Response) any {
|
||||
// requestCount.WithLabelValues("api_call", "success").Inc()
|
||||
// requestDuration.WithLabelValues("api_call", "success").Observe(duration)
|
||||
// return nil
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// Use Cases:
|
||||
// - Custom context modification: Adding request IDs, trace IDs, or other context values
|
||||
// - Structured logging: Integration with zap, logrus, or other structured loggers
|
||||
// - Metrics collection: Recording operation durations, success/failure rates
|
||||
// - Distributed tracing: OpenTelemetry, Jaeger integration
|
||||
// - Custom monitoring: Application-specific monitoring and alerting
|
||||
//
|
||||
// Note: LogEntryExit is implemented using LogEntryExitF with standard logging and context management.
|
||||
// Use LogEntryExitF when you need more control over the entry/exit behavior or context modification.
|
||||
func LogEntryExitF[A, ANY any](
|
||||
onEntry ReaderIO[context.Context],
|
||||
onExit readerio.Kleisli[Result[A], ANY],
|
||||
) Operator[A, A] {
|
||||
bracket := F.Bind13of3(readerio.Bracket[context.Context, Result[A], ANY])(onEntry, func(newCtx context.Context, res Result[A]) ReaderIO[ANY] {
|
||||
return readerio.FromIO(onExit(res)(newCtx)) // Get the exit callback for this result
|
||||
})
|
||||
|
||||
return func(src ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return bracket(F.Flow2(
|
||||
src,
|
||||
FromIOResult,
|
||||
))
|
||||
}
|
||||
}
|
||||
func noop() {}
|
||||
|
||||
// onEntry creates a ReaderIO that handles the entry logging for an operation.
|
||||
// It generates a unique logging ID, captures the start time, and logs the entry message.
|
||||
@@ -230,15 +106,15 @@ func LogEntryExitF[A, ANY any](
|
||||
// - A ReaderIO that prepares the context with logging information and logs the entry
|
||||
func onEntry(
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
cb Reader[context.Context, *slog.Logger],
|
||||
nameAttr slog.Attr,
|
||||
) ReaderIO[context.Context] {
|
||||
) ReaderIO[ContextCancel] {
|
||||
|
||||
return func(ctx context.Context) IO[context.Context] {
|
||||
return func(ctx context.Context) IO[ContextCancel] {
|
||||
// logger
|
||||
logger := cb(ctx)
|
||||
|
||||
return func() context.Context {
|
||||
return func() ContextCancel {
|
||||
// check if the logger is enabled
|
||||
if logger.Enabled(ctx, logLevel) {
|
||||
// Generate unique logging ID and capture start time
|
||||
@@ -258,19 +134,23 @@ func onEntry(
|
||||
})
|
||||
withLogger := logging.WithLogger(newLogger)
|
||||
|
||||
return withCtx(withLogger(ctx))
|
||||
return F.Pipe2(
|
||||
ctx,
|
||||
withLogger,
|
||||
pair.Map[context.CancelFunc](withCtx),
|
||||
)
|
||||
}
|
||||
// logging disabled
|
||||
withCtx := withLoggingContext(loggingContext{
|
||||
logger: logger,
|
||||
isEnabled: false,
|
||||
})
|
||||
return withCtx(ctx)
|
||||
return pair.MakePair[context.CancelFunc](noop, withCtx(ctx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onExitAny creates a Kleisli function that handles exit logging for an operation.
|
||||
// onExitVoid creates a Kleisli function that handles exit logging for an operation.
|
||||
// It logs either success or error based on the Result, including the operation duration.
|
||||
// Only logs if logging was enabled during entry (checked via loggingContext.isEnabled).
|
||||
//
|
||||
@@ -280,33 +160,33 @@ func onEntry(
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that logs the exit/error and returns nil
|
||||
func onExitAny(
|
||||
func onExitVoid(
|
||||
logLevel slog.Level,
|
||||
nameAttr slog.Attr,
|
||||
) readerio.Kleisli[Result[any], any] {
|
||||
return func(res Result[any]) ReaderIO[any] {
|
||||
return func(ctx context.Context) IO[any] {
|
||||
) readerio.Kleisli[Result[Void], Void] {
|
||||
return func(res Result[Void]) ReaderIO[Void] {
|
||||
return func(ctx context.Context) IO[Void] {
|
||||
value := getLoggingContext(ctx)
|
||||
|
||||
if value.isEnabled {
|
||||
|
||||
return func() any {
|
||||
return func() Void {
|
||||
// Retrieve logging information from context
|
||||
durationAttr := slog.Duration("duration", time.Since(value.startTime))
|
||||
|
||||
// Log error with ID and duration
|
||||
onError := func(err error) any {
|
||||
onError := func(err error) Void {
|
||||
value.logger.LogAttrs(ctx, logLevel, "[throwing]",
|
||||
nameAttr,
|
||||
durationAttr,
|
||||
slog.Any("error", err))
|
||||
return nil
|
||||
return F.VOID
|
||||
}
|
||||
|
||||
// Log success with ID and duration
|
||||
onSuccess := func(_ any) any {
|
||||
onSuccess := func(v Void) Void {
|
||||
value.logger.LogAttrs(ctx, logLevel, "[exiting ]", nameAttr, durationAttr)
|
||||
return nil
|
||||
return v
|
||||
}
|
||||
|
||||
return F.Pipe1(
|
||||
@@ -316,7 +196,7 @@ func onExitAny(
|
||||
}
|
||||
}
|
||||
// nothing to do
|
||||
return io.Of[any](nil)
|
||||
return io.Of(F.VOID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,13 +254,21 @@ func LogEntryExitWithCallback[A any](
|
||||
|
||||
nameAttr := slog.String("name", name)
|
||||
|
||||
return LogEntryExitF(
|
||||
entry := F.Pipe1(
|
||||
onEntry(logLevel, cb, nameAttr),
|
||||
F.Flow2(
|
||||
result.MapTo[A, any](nil),
|
||||
onExitAny(logLevel, nameAttr),
|
||||
),
|
||||
readerio.LocalIOK[Result[A]],
|
||||
)
|
||||
|
||||
exit := readerio.Tap(F.Flow2(
|
||||
result.MapTo[A](F.VOID),
|
||||
onExitVoid(logLevel, nameAttr),
|
||||
))
|
||||
|
||||
return F.Flow2(
|
||||
exit,
|
||||
entry,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// LogEntryExit creates an operator that logs the entry and exit of a ReaderIOResult computation with timing and correlation IDs.
|
||||
|
||||
415
v2/context/readerioresult/logging_comprehensive_test.go
Normal file
415
v2/context/readerioresult/logging_comprehensive_test.go
Normal file
@@ -0,0 +1,415 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTapSLogComprehensive_Success verifies TapSLog logs successful values
|
||||
func TestTapSLogComprehensive_Success(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
t.Run("logs integer success value", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
Of(42),
|
||||
TapSLog[int]("Integer value"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify result is correct
|
||||
assert.Equal(t, 84, F.Pipe1(res, getOrZero))
|
||||
|
||||
// Verify logging occurred
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Integer value", "Should log the message")
|
||||
assert.Contains(t, logOutput, "value=42", "Should log the success value")
|
||||
assert.NotContains(t, logOutput, "error", "Should not contain error keyword for success")
|
||||
})
|
||||
|
||||
t.Run("logs string success value", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
Of("hello world"),
|
||||
TapSLog[string]("String value"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify result is correct
|
||||
assert.True(t, F.Pipe1(res, isRight[string]))
|
||||
|
||||
// Verify logging occurred
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "String value")
|
||||
assert.Contains(t, logOutput, `value="hello world"`)
|
||||
})
|
||||
|
||||
t.Run("logs struct success value", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
pipeline := F.Pipe1(
|
||||
Of(user),
|
||||
TapSLog[User]("User struct"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify result is correct
|
||||
assert.True(t, F.Pipe1(res, isRight[User]))
|
||||
|
||||
// Verify logging occurred with struct fields
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "User struct")
|
||||
assert.Contains(t, logOutput, "ID:123")
|
||||
assert.Contains(t, logOutput, "Name:Alice")
|
||||
})
|
||||
|
||||
t.Run("logs multiple success values in pipeline", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
step1 := F.Pipe2(
|
||||
Of(10),
|
||||
TapSLog[int]("Initial value"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
step1,
|
||||
TapSLog[int]("After doubling"),
|
||||
Map(N.Add(5)),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify result is correct
|
||||
assert.Equal(t, 25, getOrZero(res))
|
||||
|
||||
// Verify both log entries
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Initial value")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
assert.Contains(t, logOutput, "After doubling")
|
||||
assert.Contains(t, logOutput, "value=20")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTapSLogComprehensive_Error verifies TapSLog behavior with errors
|
||||
func TestTapSLogComprehensive_Error(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
t.Run("logs error values", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
testErr := errors.New("test error")
|
||||
pipeline := F.Pipe2(
|
||||
Left[int](testErr),
|
||||
TapSLog[int]("Error case"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify error is preserved
|
||||
assert.True(t, F.Pipe1(res, isLeft[int]))
|
||||
|
||||
// Verify logging occurred for error
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Error case", "Should log the message")
|
||||
assert.Contains(t, logOutput, "error", "Should contain error keyword")
|
||||
assert.Contains(t, logOutput, "test error", "Should log the error message")
|
||||
assert.NotContains(t, logOutput, "value=", "Should not log 'value=' for errors")
|
||||
})
|
||||
|
||||
t.Run("preserves error through pipeline", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
originalErr := errors.New("original error")
|
||||
step1 := F.Pipe2(
|
||||
Left[int](originalErr),
|
||||
TapSLog[int]("First tap"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
step1,
|
||||
TapSLog[int]("Second tap"),
|
||||
Map(N.Add(5)),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify error is preserved
|
||||
assert.True(t, isLeft(res))
|
||||
|
||||
// Verify both taps logged the error
|
||||
logOutput := buf.String()
|
||||
errorCount := strings.Count(logOutput, "original error")
|
||||
assert.Equal(t, 2, errorCount, "Both TapSLog calls should log the error")
|
||||
assert.Contains(t, logOutput, "First tap")
|
||||
assert.Contains(t, logOutput, "Second tap")
|
||||
})
|
||||
|
||||
t.Run("logs error after successful operation", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
Of(10),
|
||||
TapSLog[int]("Before error"),
|
||||
Chain(func(n int) ReaderIOResult[int] {
|
||||
return Left[int](errors.New("chain error"))
|
||||
}),
|
||||
TapSLog[int]("After error"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify error is present
|
||||
assert.True(t, F.Pipe1(res, isLeft[int]))
|
||||
|
||||
// Verify both logs
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Before error")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
assert.Contains(t, logOutput, "After error")
|
||||
assert.Contains(t, logOutput, "chain error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTapSLogComprehensive_EdgeCases verifies TapSLog with edge cases
|
||||
func TestTapSLogComprehensive_EdgeCases(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
t.Run("logs zero value", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
Of(0),
|
||||
TapSLog[int]("Zero value"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, 0, F.Pipe1(res, getOrZero))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Zero value")
|
||||
assert.Contains(t, logOutput, "value=0")
|
||||
})
|
||||
|
||||
t.Run("logs empty string", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
Of(""),
|
||||
TapSLog[string]("Empty string"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.True(t, F.Pipe1(res, isRight[string]))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Empty string")
|
||||
assert.Contains(t, logOutput, `value=""`)
|
||||
})
|
||||
|
||||
t.Run("logs nil pointer", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
type Data struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
var nilData *Data
|
||||
pipeline := F.Pipe1(
|
||||
Of(nilData),
|
||||
TapSLog[*Data]("Nil pointer"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.True(t, F.Pipe1(res, isRight[*Data]))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Nil pointer")
|
||||
// Nil representation may vary, but should be logged
|
||||
assert.NotEmpty(t, logOutput)
|
||||
})
|
||||
|
||||
t.Run("respects logger level - disabled", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
// Create logger that only logs errors
|
||||
errorLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelError,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(errorLogger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
Of(42),
|
||||
TapSLog[int]("Should not log"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, 42, F.Pipe1(res, getOrZero))
|
||||
|
||||
// Should have no logs since level is ERROR
|
||||
logOutput := buf.String()
|
||||
assert.Empty(t, logOutput, "Should not log when level is disabled")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTapSLogComprehensive_Integration verifies TapSLog in realistic scenarios
|
||||
func TestTapSLogComprehensive_Integration(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
t.Run("complex pipeline with mixed success and error", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
// Simulate a data processing pipeline
|
||||
validatePositive := func(n int) ReaderIOResult[int] {
|
||||
if n > 0 {
|
||||
return Of(n)
|
||||
}
|
||||
return Left[int](errors.New("number must be positive"))
|
||||
}
|
||||
|
||||
step1 := F.Pipe3(
|
||||
Of(5),
|
||||
TapSLog[int]("Input received"),
|
||||
Map(N.Mul(2)),
|
||||
TapSLog[int]("After multiplication"),
|
||||
)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
step1,
|
||||
Chain(validatePositive),
|
||||
TapSLog[int]("After validation"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.Equal(t, 10, getOrZero(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Input received")
|
||||
assert.Contains(t, logOutput, "value=5")
|
||||
assert.Contains(t, logOutput, "After multiplication")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
assert.Contains(t, logOutput, "After validation")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
})
|
||||
|
||||
t.Run("error propagation with logging", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
|
||||
validatePositive := func(n int) ReaderIOResult[int] {
|
||||
if n > 0 {
|
||||
return Of(n)
|
||||
}
|
||||
return Left[int](errors.New("number must be positive"))
|
||||
}
|
||||
|
||||
step1 := F.Pipe3(
|
||||
Of(-5),
|
||||
TapSLog[int]("Input received"),
|
||||
Map(N.Mul(2)),
|
||||
TapSLog[int]("After multiplication"),
|
||||
)
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
step1,
|
||||
Chain(validatePositive),
|
||||
TapSLog[int]("After validation"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
assert.True(t, isLeft(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
// First two taps should log success
|
||||
assert.Contains(t, logOutput, "Input received")
|
||||
assert.Contains(t, logOutput, "value=-5")
|
||||
assert.Contains(t, logOutput, "After multiplication")
|
||||
assert.Contains(t, logOutput, "value=-10")
|
||||
// Last tap should log error
|
||||
assert.Contains(t, logOutput, "After validation")
|
||||
assert.Contains(t, logOutput, "number must be positive")
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions for tests
|
||||
|
||||
func getOrZero(res Result[int]) int {
|
||||
val, err := result.Unwrap(res)
|
||||
if err == nil {
|
||||
return val
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func isRight[A any](res Result[A]) bool {
|
||||
return result.IsRight(res)
|
||||
}
|
||||
|
||||
func isLeft[A any](res Result[A]) bool {
|
||||
return result.IsLeft(res)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -53,6 +54,11 @@ func TestLogEntryExitSuccess(t *testing.T) {
|
||||
assert.Contains(t, logOutput, "TestOperation")
|
||||
assert.Contains(t, logOutput, "ID=")
|
||||
assert.Contains(t, logOutput, "duration=")
|
||||
|
||||
// Verify entry log appears before exit log
|
||||
enteringIdx := strings.Index(logOutput, "[entering]")
|
||||
exitingIdx := strings.Index(logOutput, "[exiting ]")
|
||||
assert.Greater(t, exitingIdx, enteringIdx, "Exit log should appear after entry log")
|
||||
}
|
||||
|
||||
// TestLogEntryExitError tests error operation logging
|
||||
@@ -81,6 +87,11 @@ func TestLogEntryExitError(t *testing.T) {
|
||||
assert.Contains(t, logOutput, "test error")
|
||||
assert.Contains(t, logOutput, "ID=")
|
||||
assert.Contains(t, logOutput, "duration=")
|
||||
|
||||
// Verify entry log appears before error log
|
||||
enteringIdx := strings.Index(logOutput, "[entering]")
|
||||
throwingIdx := strings.Index(logOutput, "[throwing]")
|
||||
assert.Greater(t, throwingIdx, enteringIdx, "Error log should appear after entry log")
|
||||
}
|
||||
|
||||
// TestLogEntryExitNested tests nested operations with different IDs
|
||||
@@ -119,6 +130,48 @@ func TestLogEntryExitNested(t *testing.T) {
|
||||
exitCount := strings.Count(logOutput, "[exiting ]")
|
||||
assert.Equal(t, 2, enterCount, "Should have 2 entering logs")
|
||||
assert.Equal(t, 2, exitCount, "Should have 2 exiting logs")
|
||||
|
||||
// Verify log ordering: Each operation logs entry before exit
|
||||
// Note: Due to Chain semantics, OuterOp completes before InnerOp starts
|
||||
lines := strings.Split(logOutput, "\n")
|
||||
var logSequence []string
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "OuterOp") && strings.Contains(line, "[entering]") {
|
||||
logSequence = append(logSequence, "OuterOp-entering")
|
||||
} else if strings.Contains(line, "OuterOp") && strings.Contains(line, "[exiting ]") {
|
||||
logSequence = append(logSequence, "OuterOp-exiting")
|
||||
} else if strings.Contains(line, "InnerOp") && strings.Contains(line, "[entering]") {
|
||||
logSequence = append(logSequence, "InnerOp-entering")
|
||||
} else if strings.Contains(line, "InnerOp") && strings.Contains(line, "[exiting ]") {
|
||||
logSequence = append(logSequence, "InnerOp-exiting")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify each operation's entry comes before its exit
|
||||
assert.Equal(t, 4, len(logSequence), "Should have 4 log entries")
|
||||
|
||||
// Find indices
|
||||
outerEnterIdx := -1
|
||||
outerExitIdx := -1
|
||||
innerEnterIdx := -1
|
||||
innerExitIdx := -1
|
||||
|
||||
for i, log := range logSequence {
|
||||
switch log {
|
||||
case "OuterOp-entering":
|
||||
outerEnterIdx = i
|
||||
case "OuterOp-exiting":
|
||||
outerExitIdx = i
|
||||
case "InnerOp-entering":
|
||||
innerEnterIdx = i
|
||||
case "InnerOp-exiting":
|
||||
innerExitIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
// Verify entry before exit for each operation
|
||||
assert.Greater(t, outerExitIdx, outerEnterIdx, "OuterOp exit should come after OuterOp entry")
|
||||
assert.Greater(t, innerExitIdx, innerEnterIdx, "InnerOp exit should come after InnerOp entry")
|
||||
}
|
||||
|
||||
// TestLogEntryExitWithCallback tests custom log level and callback
|
||||
@@ -172,76 +225,6 @@ func TestLogEntryExitDisabled(t *testing.T) {
|
||||
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
|
||||
}
|
||||
|
||||
// TestLogEntryExitF tests custom entry/exit callbacks
|
||||
func TestLogEntryExitF(t *testing.T) {
|
||||
var entryCount, exitCount int
|
||||
|
||||
onEntry := func(ctx context.Context) IO[context.Context] {
|
||||
return func() context.Context {
|
||||
entryCount++
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
onExit := func(res Result[string]) ReaderIO[any] {
|
||||
return func(ctx context.Context) IO[any] {
|
||||
return func() any {
|
||||
exitCount++
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("test"),
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
|
||||
}
|
||||
|
||||
// TestLogEntryExitFWithError tests custom callbacks with error
|
||||
func TestLogEntryExitFWithError(t *testing.T) {
|
||||
var entryCount, exitCount int
|
||||
var capturedError error
|
||||
|
||||
onEntry := func(ctx context.Context) IO[context.Context] {
|
||||
return func() context.Context {
|
||||
entryCount++
|
||||
return ctx
|
||||
}
|
||||
}
|
||||
|
||||
onExit := func(res Result[string]) ReaderIO[any] {
|
||||
return func(ctx context.Context) IO[any] {
|
||||
return func() any {
|
||||
exitCount++
|
||||
if result.IsLeft(res) {
|
||||
_, capturedError = result.Unwrap(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testErr := errors.New("custom error")
|
||||
operation := F.Pipe1(
|
||||
Left[string](testErr),
|
||||
LogEntryExitF(onEntry, onExit),
|
||||
)
|
||||
|
||||
res := operation(t.Context())()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
|
||||
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
|
||||
assert.Equal(t, testErr, capturedError, "Should capture the error")
|
||||
}
|
||||
|
||||
// TestLoggingIDUniqueness tests that logging IDs are unique
|
||||
func TestLoggingIDUniqueness(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
@@ -287,7 +270,8 @@ func TestLogEntryExitWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
cancelFct, ctx := pair.Unpack(logging.WithLogger(contextLogger)(t.Context()))
|
||||
defer cancelFct()
|
||||
|
||||
operation := F.Pipe1(
|
||||
Of("context value"),
|
||||
@@ -546,7 +530,8 @@ func TestTapSLogWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
cancelFct, ctx := pair.Unpack(logging.WithLogger(contextLogger)(t.Context()))
|
||||
defer cancelFct()
|
||||
|
||||
operation := F.Pipe2(
|
||||
Of("test value"),
|
||||
@@ -660,3 +645,138 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
assert.Contains(t, logOutput, "warning error")
|
||||
assert.Contains(t, logOutput, "level=WARN")
|
||||
}
|
||||
|
||||
// TestTapSLogPreservesResult tests that TapSLog doesn't modify the result
|
||||
func TestTapSLogPreservesResult(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
// Test with success value
|
||||
successOp := F.Pipe2(
|
||||
Of(42),
|
||||
TapSLog[int]("Success value"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
successRes := successOp(t.Context())()
|
||||
assert.Equal(t, result.Of(84), successRes)
|
||||
|
||||
// Test with error value
|
||||
testErr := errors.New("test error")
|
||||
errorOp := F.Pipe2(
|
||||
Left[int](testErr),
|
||||
TapSLog[int]("Error value"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
errorRes := errorOp(t.Context())()
|
||||
assert.True(t, result.IsLeft(errorRes))
|
||||
|
||||
// Verify the error is preserved
|
||||
_, err := result.Unwrap(errorRes)
|
||||
assert.Equal(t, testErr, err)
|
||||
}
|
||||
|
||||
// TestTapSLogChainBehavior tests that TapSLog properly chains with other operations
|
||||
func TestTapSLogChainBehavior(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
// Create a pipeline with multiple TapSLog calls
|
||||
step1 := F.Pipe2(
|
||||
Of(1),
|
||||
TapSLog[int]("Step 1"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
step2 := F.Pipe2(
|
||||
step1,
|
||||
TapSLog[int]("Step 2"),
|
||||
Map(N.Mul(3)),
|
||||
)
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step2,
|
||||
TapSLog[int]("Step 3"),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
assert.Equal(t, result.Of(6), res)
|
||||
|
||||
logOutput := buf.String()
|
||||
|
||||
// Verify all steps were logged
|
||||
assert.Contains(t, logOutput, "Step 1")
|
||||
assert.Contains(t, logOutput, "value=1")
|
||||
assert.Contains(t, logOutput, "Step 2")
|
||||
assert.Contains(t, logOutput, "value=2")
|
||||
assert.Contains(t, logOutput, "Step 3")
|
||||
assert.Contains(t, logOutput, "value=6")
|
||||
}
|
||||
|
||||
// TestTapSLogWithNilValue tests TapSLog with nil pointer values
|
||||
func TestTapSLogWithNilValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
type Data struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
// Test with nil pointer
|
||||
var nilData *Data
|
||||
operation := F.Pipe1(
|
||||
Of(nilData),
|
||||
TapSLog[*Data]("Nil pointer value"),
|
||||
)
|
||||
|
||||
res := operation(t.Context())()
|
||||
assert.True(t, result.IsRight(res))
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Nil pointer value")
|
||||
// The exact representation of nil may vary, but it should be logged
|
||||
assert.NotEmpty(t, logOutput)
|
||||
}
|
||||
|
||||
// TestTapSLogLogsErrors verifies that TapSLog DOES log errors
|
||||
// TapSLog uses SLog internally, which logs both success values and errors
|
||||
func TestTapSLogLogsErrors(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
testErr := errors.New("test error message")
|
||||
pipeline := F.Pipe2(
|
||||
Left[int](testErr),
|
||||
TapSLog[int]("Error logging test"),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
res := pipeline(t.Context())()
|
||||
|
||||
// Verify the error is preserved
|
||||
assert.True(t, result.IsLeft(res))
|
||||
|
||||
// Verify logging occurred for the error
|
||||
logOutput := buf.String()
|
||||
assert.NotEmpty(t, logOutput, "TapSLog should log when the Result is an error")
|
||||
assert.Contains(t, logOutput, "Error logging test")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "test error message")
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
@@ -38,21 +39,24 @@ import (
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
// - A: The original success type produced by the ReaderIOResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - f: Function to transform the input environment R into context.Context (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[A] and returns a function from R to B
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context], g func(A) B) Operator[A, B] {
|
||||
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RIOR.Kleisli[R, ReaderIOResult[A], B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
RIOR.Map[R](g),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,18 +70,41 @@ func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, contex
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIOR.Kleisli[R, ReaderIOResult[A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
// ContramapIOK changes the context during the execution of a ReaderIOResult using an IO effect.
|
||||
// This is the contravariant functor operation with IO effects.
|
||||
//
|
||||
// ContramapIOK is an alias for LocalIOK and is useful for adapting a ReaderIOResult to work with
|
||||
// a modified context when the transformation itself requires side effects.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IO Kleisli arrow that transforms the context with side effects
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
|
||||
//
|
||||
// See Also:
|
||||
// - Contramap: For pure context transformations
|
||||
// - LocalIOK: The underlying implementation
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
func ContramapIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
|
||||
return LocalIOK[A](f)
|
||||
}
|
||||
@@ -189,8 +216,6 @@ func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A
|
||||
//
|
||||
// - Local: For pure context transformations
|
||||
// - LocalIOK: For context transformations with side effects that cannot fail
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOResultK[A any](f ioresult.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
|
||||
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOResult[A] {
|
||||
|
||||
@@ -77,6 +77,105 @@ func TestContramapBasic(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapIOK tests ContramapIOK functionality
|
||||
func TestContramapIOK(t *testing.T) {
|
||||
t.Run("transforms context with IO effect", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
if v := ctx.Value("requestID"); v != nil {
|
||||
return R.Of(v.(string))
|
||||
}
|
||||
return R.Of("no-id")
|
||||
}
|
||||
}
|
||||
|
||||
addRequestID := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
// Simulate generating a request ID via side effect
|
||||
requestID := "req-12345"
|
||||
newCtx := context.WithValue(ctx, "requestID", requestID)
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := ContramapIOK[string](addRequestID)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of("req-12345"), result)
|
||||
})
|
||||
|
||||
t.Run("preserves value type", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
if v := ctx.Value("counter"); v != nil {
|
||||
return R.Of(v.(int))
|
||||
}
|
||||
return R.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
addCounter := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "counter", 999)
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := ContramapIOK[int](addCounter)(getValue)
|
||||
result := adapted(t.Context())()
|
||||
|
||||
assert.Equal(t, R.Of(999), result)
|
||||
})
|
||||
|
||||
t.Run("calls cancel function", func(t *testing.T) {
|
||||
cancelCalled := false
|
||||
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("test")
|
||||
}
|
||||
}
|
||||
|
||||
addData := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "data", "value")
|
||||
cancelFunc := context.CancelFunc(func() {
|
||||
cancelCalled = true
|
||||
})
|
||||
return pair.MakePair(cancelFunc, newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
adapted := ContramapIOK[string](addData)(getValue)
|
||||
_ = adapted(t.Context())()
|
||||
|
||||
assert.True(t, cancelCalled, "cancel function should be called")
|
||||
})
|
||||
|
||||
t.Run("handles cancelled context", func(t *testing.T) {
|
||||
getValue := func(ctx context.Context) IOResult[string] {
|
||||
return func() R.Result[string] {
|
||||
return R.Of("should not reach here")
|
||||
}
|
||||
}
|
||||
|
||||
addData := func(ctx context.Context) io.IO[ContextCancel] {
|
||||
return func() ContextCancel {
|
||||
newCtx := context.WithValue(ctx, "data", "value")
|
||||
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
adapted := ContramapIOK[string](addData)(getValue)
|
||||
result := adapted(ctx)()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("adds value to context", func(t *testing.T) {
|
||||
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1011,12 +1010,15 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type of the ReaderIOResult
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms the context and returns a cancel function
|
||||
// - f: A function that transforms the input environment R into context.Context and returns a cancel function
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that runs the computation with the transformed context
|
||||
// - A Kleisli arrow that runs the computation with the transformed context
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -1026,9 +1028,9 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// type key int
|
||||
// const userKey key = 0
|
||||
//
|
||||
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// addUser := readerioresult.Local[string, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||
// return newCtx, func() {} // No-op cancel
|
||||
// return pair.MakePair(func() {}, newCtx) // No-op cancel
|
||||
// })
|
||||
//
|
||||
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
|
||||
@@ -1047,27 +1049,19 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
|
||||
// Timeout Example:
|
||||
//
|
||||
// // Add a 5-second timeout to a specific operation
|
||||
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// return context.WithTimeout(ctx, 5*time.Second)
|
||||
// withTimeout := readerioresult.Local[Data, context.Context](func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// fetchData,
|
||||
// withTimeout,
|
||||
// )
|
||||
func Local[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
|
||||
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOResult[A] {
|
||||
return func() Result[A] {
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[A](context.Cause(ctx))
|
||||
}
|
||||
otherCancel, otherCtx := pair.Unpack(f(ctx))
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)()
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RIOR.Kleisli[R, ReaderIOResult[A], A] {
|
||||
return readerio.Local[Result[A]](f)
|
||||
}
|
||||
|
||||
// WithTimeout adds a timeout to the context for a ReaderIOResult computation.
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
48
v2/context/readerreaderioresult/filter.go
Normal file
48
v2/context/readerreaderioresult/filter.go
Normal file
@@ -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"
|
||||
@@ -13,6 +14,17 @@ import (
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
@@ -102,6 +114,29 @@ func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReader
|
||||
return RRIOE.LocalIOEitherK[context.Context, A](f)
|
||||
}
|
||||
|
||||
// LocalResultK transforms the outer environment of a ReaderReaderIOResult using a Result-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through a pure computation that can fail before
|
||||
// passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// This is useful when the outer environment transformation is a pure computation that can fail,
|
||||
// such as parsing, validation, or data transformation that doesn't require IO effects.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The Result function f is executed with the R2 environment to produce Result[R1]
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Result Kleisli arrow that transforms R2 to R1 with pure computation that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalResultK[A, R1, R2 any](f result.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalEitherK[context.Context, A](f)
|
||||
@@ -162,6 +197,90 @@ func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(
|
||||
return RRIOE.LocalReaderIOEitherK[A](f)
|
||||
}
|
||||
|
||||
// LocalReaderK transforms the outer environment of a ReaderReaderIOResult using a Reader-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through a pure computation that depends on the inner context
|
||||
// before passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// This is useful when the outer environment transformation is a pure computation that requires access
|
||||
// to the inner context (e.g., context.Context) but cannot fail. Common use cases include:
|
||||
// - Extracting configuration from context values
|
||||
// - Computing derived environment values based on context
|
||||
// - Transforming environment based on context metadata
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The Reader function f is executed with the R2 outer environment and inner context to produce an R1 value
|
||||
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Reader Kleisli arrow that transforms R2 to R1 using the inner context
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// type ctxKey string
|
||||
// const configKey ctxKey = "config"
|
||||
//
|
||||
// // Extract config from context and transform environment
|
||||
// extractConfig := func(path string) reader.Reader[DetailedConfig] {
|
||||
// return func(ctx context.Context) DetailedConfig {
|
||||
// if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
|
||||
// return cfg
|
||||
// }
|
||||
// return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use the config
|
||||
// useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
|
||||
// return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
// return func() result.Result[string] {
|
||||
// return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Compose using LocalReaderK
|
||||
// adapted := LocalReaderK[string](extractConfig)(useConfig)
|
||||
// ctx := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
|
||||
// result := adapted("config.json")(ctx)() // Result: "api.example.com:443"
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderK[A, R1, R2 any](f reader.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderK[error, A](f)
|
||||
}
|
||||
|
||||
// LocalReaderReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderReaderIOResult-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through a computation that depends on both the outer environment
|
||||
// and the inner context, and can perform IO effects that may fail.
|
||||
//
|
||||
// This is the most powerful Local variant, useful when the outer environment transformation requires:
|
||||
// - Access to both the outer environment (R2) and inner context (context.Context)
|
||||
// - IO operations that can fail
|
||||
// - Complex transformations that need the full computational context
|
||||
//
|
||||
// The transformation happens in three stages:
|
||||
// 1. The ReaderReaderIOResult effect f is executed with the R2 outer environment and inner context
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderReaderIOResult Kleisli arrow that transforms R2 to R1 with full context-aware IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderReaderIOEitherK[A, R1, R2 any](f Kleisli[R2, R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderReaderIOEitherK[A](f)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
@@ -426,3 +427,226 @@ func TestLocalReaderIOResultK(t *testing.T) {
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalReaderK tests LocalReaderK functionality
|
||||
func TestLocalReaderK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic Reader transformation", func(t *testing.T) {
|
||||
// Reader that transforms string path to SimpleConfig using context
|
||||
loadConfig := func(path string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
// Could extract values from context here
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalReaderK
|
||||
adapted := LocalReaderK[string](loadConfig)(useConfig)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
})
|
||||
|
||||
t.Run("extract config from context", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const configKey ctxKey = "config"
|
||||
|
||||
// Reader that extracts config from context
|
||||
extractConfig := func(path string) reader.Reader[DetailedConfig] {
|
||||
return func(ctx context.Context) DetailedConfig {
|
||||
if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
|
||||
return cfg
|
||||
}
|
||||
// Default config if not in context
|
||||
return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](extractConfig)(useConfig)
|
||||
|
||||
// With context value
|
||||
ctxWithConfig := context.WithValue(ctx, configKey, DetailedConfig{Host: "api.example.com", Port: 443})
|
||||
res := adapted("ignored")(ctxWithConfig)()
|
||||
assert.Equal(t, result.Of("api.example.com:443"), res)
|
||||
|
||||
// Without context value (uses default)
|
||||
resDefault := adapted("ignored")(ctx)()
|
||||
assert.Equal(t, result.Of("localhost:8080"), resDefault)
|
||||
})
|
||||
|
||||
t.Run("context-aware transformation", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const multiplierKey ctxKey = "multiplier"
|
||||
|
||||
// Reader that uses context to compute environment
|
||||
computeValue := func(base int) reader.Reader[int] {
|
||||
return func(ctx context.Context) int {
|
||||
if mult, ok := ctx.Value(multiplierKey).(int); ok {
|
||||
return base * mult
|
||||
}
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
// Use the computed value
|
||||
formatValue := func(val int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Value: %d", val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](computeValue)(formatValue)
|
||||
|
||||
// With multiplier in context
|
||||
ctxWithMult := context.WithValue(ctx, multiplierKey, 10)
|
||||
res := adapted(5)(ctxWithMult)()
|
||||
assert.Equal(t, result.Of("Value: 50"), res)
|
||||
|
||||
// Without multiplier (uses base value)
|
||||
resBase := adapted(5)(ctx)()
|
||||
assert.Equal(t, result.Of("Value: 5"), resBase)
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalReaderK", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const prefixKey ctxKey = "prefix"
|
||||
|
||||
// First transformation: int -> string using context
|
||||
intToString := func(n int) reader.Reader[string] {
|
||||
return func(ctx context.Context) string {
|
||||
if prefix, ok := ctx.Value(prefixKey).(string); ok {
|
||||
return fmt.Sprintf("%s-%d", prefix, n)
|
||||
}
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: string -> SimpleConfig
|
||||
stringToConfig := func(s string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
return SimpleConfig{Port: len(s) * 100}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalReaderK[string](stringToConfig)(formatConfig)
|
||||
step2 := LocalReaderK[string](intToString)(step1)
|
||||
|
||||
// With prefix in context
|
||||
ctxWithPrefix := context.WithValue(ctx, prefixKey, "test")
|
||||
res := step2(42)(ctxWithPrefix)()
|
||||
// "test-42" has length 7, so port = 700
|
||||
assert.Equal(t, result.Of("Port: 700"), res)
|
||||
|
||||
// Without prefix
|
||||
resNoPrefix := step2(42)(ctx)()
|
||||
// "42" has length 2, so port = 200
|
||||
assert.Equal(t, result.Of("Port: 200"), resNoPrefix)
|
||||
})
|
||||
|
||||
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
|
||||
// Reader transformation (pure, cannot fail)
|
||||
loadConfig := func(path string) reader.Reader[SimpleConfig] {
|
||||
return func(ctx context.Context) SimpleConfig {
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Left[string](errors.New("operation failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](loadConfig)(failingOperation)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
// Error from the ReaderReaderIOResult should propagate
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("real-world: environment selection based on context", func(t *testing.T) {
|
||||
type Environment string
|
||||
const (
|
||||
Dev Environment = "dev"
|
||||
Prod Environment = "prod"
|
||||
)
|
||||
|
||||
type ctxKey string
|
||||
const envKey ctxKey = "environment"
|
||||
|
||||
type EnvConfig struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Reader that selects config based on context environment
|
||||
selectConfig := func(envName EnvConfig) reader.Reader[DetailedConfig] {
|
||||
return func(ctx context.Context) DetailedConfig {
|
||||
env := Dev
|
||||
if e, ok := ctx.Value(envKey).(Environment); ok {
|
||||
env = e
|
||||
}
|
||||
|
||||
switch env {
|
||||
case Prod:
|
||||
return DetailedConfig{Host: "api.production.com", Port: 443}
|
||||
default:
|
||||
return DetailedConfig{Host: "localhost", Port: 8080}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the selected config
|
||||
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Connecting to %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderK[string](selectConfig)(useConfig)
|
||||
|
||||
// Production environment
|
||||
ctxProd := context.WithValue(ctx, envKey, Prod)
|
||||
resProd := adapted(EnvConfig{Name: "app"})(ctxProd)()
|
||||
assert.Equal(t, result.Of("Connecting to api.production.com:443"), resProd)
|
||||
|
||||
// Development environment (default)
|
||||
resDev := adapted(EnvConfig{Name: "app"})(ctx)()
|
||||
assert.Equal(t, result.Of("Connecting to localhost:8080"), resDev)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -104,7 +105,8 @@ func TestSLogWithContextLogger(t *testing.T) {
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(t.Context())
|
||||
cancelFct, ctx := pair.Unpack(logging.WithLogger(contextLogger)(t.Context()))
|
||||
defer cancelFct()
|
||||
|
||||
res1 := result.Of("test value")
|
||||
logged := SLog[string]("Context logger test")(res1)(ctx)
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderResult.
|
||||
@@ -34,21 +36,24 @@ import (
|
||||
// The error type is fixed as error and remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
// - A: The original success type produced by the ReaderResult
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input context (contravariant)
|
||||
// - f: Function to transform the input environment R into context.Context (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
|
||||
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to B
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
|
||||
func Promap[R, A, B any](f pair.Kleisli[context.CancelFunc, R, context.Context], g func(A) B) RR.Kleisli[R, ReaderResult[A], B] {
|
||||
return function.Flow2(
|
||||
Local[A](f),
|
||||
Map(g),
|
||||
RR.Map[R](g),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,15 +67,18 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
|
||||
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
func Contramap[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RR.Kleisli[R, ReaderResult[A], A] {
|
||||
return Local[A](f)
|
||||
}
|
||||
|
||||
@@ -89,16 +97,19 @@ func Contramap[A any](f func(context.Context) (context.Context, context.CancelFu
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type (unchanged)
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the context, returning a new context and CancelFunc
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
|
||||
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||
return func(rr ReaderResult[A]) ReaderResult[A] {
|
||||
return func(ctx context.Context) Result[A] {
|
||||
otherCtx, otherCancel := f(ctx)
|
||||
// - A Kleisli arrow that takes a ReaderResult[A] and returns a function from R to A
|
||||
//
|
||||
// Note: When R is context.Context, this simplifies to an Operator[A, A]
|
||||
func Local[A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) RR.Kleisli[R, ReaderResult[A], A] {
|
||||
return func(rr ReaderResult[A]) RR.ReaderResult[R, A] {
|
||||
return func(r R) Result[A] {
|
||||
otherCancel, otherCtx := pair.Unpack(f(r))
|
||||
defer otherCancel()
|
||||
return rr(otherCtx)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -34,9 +35,9 @@ func TestPromapBasic(t *testing.T) {
|
||||
return R.Of(0)
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "key", 42)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
@@ -57,9 +58,9 @@ func TestContramapBasic(t *testing.T) {
|
||||
return R.Of(0)
|
||||
}
|
||||
|
||||
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "key", 100)
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
|
||||
adapted := Contramap[int](addKey)(getValue)
|
||||
@@ -79,9 +80,9 @@ func TestLocalBasic(t *testing.T) {
|
||||
return R.Of("unknown")
|
||||
}
|
||||
|
||||
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
addUser := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
newCtx := context.WithValue(ctx, "user", "Alice")
|
||||
return newCtx, func() {}
|
||||
return pair.MakePair[context.CancelFunc](func() {}, newCtx)
|
||||
}
|
||||
|
||||
adapted := Local[string](addUser)(getValue)
|
||||
|
||||
@@ -21,8 +21,9 @@ import (
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/statet"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
SRIOE "github.com/IBM/fp-go/v2/statereaderioeither"
|
||||
)
|
||||
|
||||
// Left creates a StateReaderIOResult that represents a failed computation with the given error.
|
||||
@@ -202,21 +203,42 @@ func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
|
||||
// Combinators
|
||||
|
||||
// Local runs a computation with a modified context.
|
||||
// The function f transforms the context before passing it to the computation.
|
||||
// The function f transforms the context before passing it to the computation,
|
||||
// returning both a new context and a CancelFunc that should be called to release resources.
|
||||
//
|
||||
// This is useful for:
|
||||
// - Adding values to the context
|
||||
// - Setting timeouts or deadlines
|
||||
// - Modifying context metadata
|
||||
//
|
||||
// The CancelFunc is automatically called after the computation completes to ensure proper cleanup.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - A: The result type
|
||||
// - R: The input environment type that f transforms into context.Context
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment R into context.Context, returning a new context and CancelFunc
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a StateReaderIOResult[S, A] and returns a StateReaderIOEither[S, R, error, A]
|
||||
//
|
||||
// Note: When R is context.Context, the return type simplifies to func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Modify context before running computation
|
||||
// withTimeout := statereaderioresult.Local[AppState](
|
||||
// func(ctx context.Context) context.Context {
|
||||
// ctx, _ = context.WithTimeout(ctx, 60*time.Second)
|
||||
// return ctx
|
||||
// }
|
||||
// // Add a timeout to a specific operation
|
||||
// withTimeout := statereaderioresult.Local[AppState, Data, context.Context](
|
||||
// func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
|
||||
// newCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
// return pair.MakePair(cancel, newCtx)
|
||||
// },
|
||||
// )
|
||||
// result := withTimeout(computation)
|
||||
func Local[S, A any](f func(context.Context) context.Context) func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
|
||||
return func(ma StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
|
||||
return function.Flow2(ma, RIOR.Local[Pair[S, A]](f))
|
||||
func Local[S, A, R any](f pair.Kleisli[context.CancelFunc, R, context.Context]) SRIOE.Kleisli[S, R, error, StateReaderIOResult[S, A], A] {
|
||||
return func(ma StateReaderIOResult[S, A]) SRIOE.StateReaderIOEither[S, R, error, A] {
|
||||
return function.Flow2(ma, RIORES.Local[Pair[S, A]](f))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
IOR "github.com/IBM/fp-go/v2/ioresult"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -264,8 +265,8 @@ func TestLocal(t *testing.T) {
|
||||
|
||||
// Modify context before running computation
|
||||
result := Local[testState, string](
|
||||
func(c context.Context) context.Context {
|
||||
return context.WithValue(c, "key", "value2")
|
||||
func(c context.Context) ContextCancel {
|
||||
return pair.MakePair[context.CancelFunc](func() {}, context.WithValue(c, "key", "value2"))
|
||||
},
|
||||
)(comp)
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
package statereaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
@@ -84,4 +86,11 @@ type (
|
||||
Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// ContextCancel represents a pair of a cancel function and a context.
|
||||
// It is used in operations that create new contexts with cancellation capabilities.
|
||||
//
|
||||
// The first element is the CancelFunc that should be called to release resources.
|
||||
// The second element is the new Context that was created.
|
||||
ContextCancel = Pair[context.CancelFunc, context.Context]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
296
v2/effect/filter.go
Normal file
296
v2/effect/filter.go
Normal file
@@ -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)
|
||||
}
|
||||
653
v2/effect/filter_test.go
Normal file
653
v2/effect/filter_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
86
v2/effect/profunctor.go
Normal file
86
v2/effect/profunctor.go
Normal file
@@ -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),
|
||||
)
|
||||
}
|
||||
373
v2/effect/profunctor_test.go
Normal file
373
v2/effect/profunctor_test.go
Normal file
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -111,15 +111,19 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// This is the functor map operation for endomorphisms.
|
||||
//
|
||||
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
|
||||
// - g is applied first to the input
|
||||
// - ma is applied first to the input
|
||||
// - f is applied to the result
|
||||
//
|
||||
// Note: unlike most other packages where MonadMap takes (fa, f) with the container
|
||||
// first, here f (the morphism) comes first to match the right-to-left composition
|
||||
// convention: MonadMap(f, ma) = f ∘ ma.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The function to map (outer function)
|
||||
// - g: The endomorphism to map over (inner function)
|
||||
// - f: The function to map (outer function, applied second)
|
||||
// - ma: The endomorphism to map over (inner function, applied first)
|
||||
//
|
||||
// Returns:
|
||||
// - A new endomorphism that applies g, then f
|
||||
// - A new endomorphism that applies ma, then f
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -127,8 +131,8 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// increment := N.Add(1)
|
||||
// mapped := endomorphism.MonadMap(double, increment)
|
||||
// // mapped(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
return MonadCompose(f, g)
|
||||
func MonadMap[A any](f, ma Endomorphism[A]) Endomorphism[A] {
|
||||
return MonadCompose(f, ma)
|
||||
}
|
||||
|
||||
// Compose returns a function that composes an endomorphism with another, executing right to left.
|
||||
@@ -386,3 +390,91 @@ func Join[A any](f Kleisli[A]) Endomorphism[A] {
|
||||
return f(a)(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Read captures a value and returns a function that applies endomorphisms to it.
|
||||
//
|
||||
// This function implements a "reader" pattern for endomorphisms. It takes a value
|
||||
// and returns a function that can apply any endomorphism to that captured value.
|
||||
// This is useful for creating reusable evaluation contexts where you want to apply
|
||||
// different transformations to the same initial value.
|
||||
//
|
||||
// The returned function has the signature func(Endomorphism[A]) A, which means
|
||||
// it takes an endomorphism and returns the result of applying that endomorphism
|
||||
// to the captured value.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the value being captured and transformed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - a: The value to capture for later transformation
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that applies endomorphisms to the captured value
|
||||
//
|
||||
// # Example - Basic Usage
|
||||
//
|
||||
// // Capture a value
|
||||
// applyTo5 := Read(5)
|
||||
//
|
||||
// // Apply different endomorphisms to the same value
|
||||
// doubled := applyTo5(N.Mul(2)) // 10
|
||||
// incremented := applyTo5(N.Add(1)) // 6
|
||||
// squared := applyTo5(func(x int) int { return x * x }) // 25
|
||||
//
|
||||
// # Example - Reusable Evaluation Context
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// Retries int
|
||||
// }
|
||||
//
|
||||
// baseConfig := Config{Timeout: 30, Retries: 3}
|
||||
// applyToBase := Read(baseConfig)
|
||||
//
|
||||
// // Apply different transformations to the same base config
|
||||
// withLongTimeout := applyToBase(func(c Config) Config {
|
||||
// c.Timeout = 60
|
||||
// return c
|
||||
// })
|
||||
//
|
||||
// withMoreRetries := applyToBase(func(c Config) Config {
|
||||
// c.Retries = 5
|
||||
// return c
|
||||
// })
|
||||
//
|
||||
// # Example - Testing Different Transformations
|
||||
//
|
||||
// // Useful for testing multiple transformations on the same input
|
||||
// testValue := "hello"
|
||||
// applyToTest := Read(testValue)
|
||||
//
|
||||
// upperCase := applyToTest(strings.ToUpper) // "HELLO"
|
||||
// withSuffix := applyToTest(func(s string) string {
|
||||
// return s + " world"
|
||||
// }) // "hello world"
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. **Testing**: Apply multiple transformations to the same test value
|
||||
// 2. **Configuration**: Create variations of a base configuration
|
||||
// 3. **Data Processing**: Evaluate different processing pipelines on the same data
|
||||
// 4. **Benchmarking**: Compare different endomorphisms on the same input
|
||||
// 5. **Functional Composition**: Build evaluation contexts for composed operations
|
||||
//
|
||||
// # Relationship to Other Functions
|
||||
//
|
||||
// Read is complementary to other endomorphism operations:
|
||||
// - Build applies an endomorphism to the zero value
|
||||
// - Read applies endomorphisms to a specific captured value
|
||||
// - Reduce applies multiple endomorphisms sequentially
|
||||
// - ConcatAll composes multiple endomorphisms
|
||||
//
|
||||
//go:inline
|
||||
func Read[A any](a A) func(Endomorphism[A]) A {
|
||||
return func(f Endomorphism[A]) A {
|
||||
return f(a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,3 +1071,226 @@ func TestReduceWithBuild(t *testing.T) {
|
||||
|
||||
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
|
||||
}
|
||||
|
||||
// TestRead tests the Read function
|
||||
func TestRead(t *testing.T) {
|
||||
t.Run("applies endomorphism to captured value", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
result := applyTo5(double)
|
||||
assert.Equal(t, 10, result, "Read should apply double to captured value 5")
|
||||
|
||||
result2 := applyTo5(increment)
|
||||
assert.Equal(t, 6, result2, "Read should apply increment to captured value 5")
|
||||
|
||||
result3 := applyTo5(square)
|
||||
assert.Equal(t, 25, result3, "Read should apply square to captured value 5")
|
||||
})
|
||||
|
||||
t.Run("captures value for reuse", func(t *testing.T) {
|
||||
applyTo10 := Read(10)
|
||||
|
||||
// Apply multiple different endomorphisms to the same captured value
|
||||
doubled := applyTo10(double)
|
||||
incremented := applyTo10(increment)
|
||||
negated := applyTo10(negate)
|
||||
|
||||
assert.Equal(t, 20, doubled, "Should double 10")
|
||||
assert.Equal(t, 11, incremented, "Should increment 10")
|
||||
assert.Equal(t, -10, negated, "Should negate 10")
|
||||
})
|
||||
|
||||
t.Run("works with identity", func(t *testing.T) {
|
||||
applyTo42 := Read(42)
|
||||
|
||||
result := applyTo42(Identity[int]())
|
||||
assert.Equal(t, 42, result, "Read with identity should return original value")
|
||||
})
|
||||
|
||||
t.Run("works with composed endomorphisms", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// Compose: double then increment (RIGHT-TO-LEFT)
|
||||
composed := MonadCompose(increment, double)
|
||||
result := applyTo5(composed)
|
||||
assert.Equal(t, 11, result, "Read should work with composed endomorphisms: (5 * 2) + 1 = 11")
|
||||
})
|
||||
|
||||
t.Run("works with chained endomorphisms", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// Chain: double then increment (LEFT-TO-RIGHT)
|
||||
chained := MonadChain(double, increment)
|
||||
result := applyTo5(chained)
|
||||
assert.Equal(t, 11, result, "Read should work with chained endomorphisms: (5 * 2) + 1 = 11")
|
||||
})
|
||||
|
||||
t.Run("works with ConcatAll", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// ConcatAll composes RIGHT-TO-LEFT
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
result := applyTo5(combined)
|
||||
// Execution: square(5) = 25, increment(25) = 26, double(26) = 52
|
||||
assert.Equal(t, 52, result, "Read should work with ConcatAll")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// Test with string
|
||||
applyToHello := Read("hello")
|
||||
|
||||
toUpper := func(s string) string { return s + " WORLD" }
|
||||
result := applyToHello(toUpper)
|
||||
assert.Equal(t, "hello WORLD", result, "Read should work with strings")
|
||||
|
||||
// Test with struct
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
applyToPoint := Read(Point{X: 3, Y: 4})
|
||||
|
||||
scaleX := func(p Point) Point {
|
||||
p.X *= 2
|
||||
return p
|
||||
}
|
||||
|
||||
result2 := applyToPoint(scaleX)
|
||||
assert.Equal(t, Point{X: 6, Y: 4}, result2, "Read should work with structs")
|
||||
})
|
||||
|
||||
t.Run("creates independent evaluation contexts", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
applyTo10 := Read(10)
|
||||
|
||||
// Same endomorphism, different contexts
|
||||
result5 := applyTo5(double)
|
||||
result10 := applyTo10(double)
|
||||
|
||||
assert.Equal(t, 10, result5, "First context should double 5")
|
||||
assert.Equal(t, 20, result10, "Second context should double 10")
|
||||
})
|
||||
|
||||
t.Run("useful for testing transformations", func(t *testing.T) {
|
||||
testValue := 100
|
||||
applyToTest := Read(testValue)
|
||||
|
||||
// Test multiple transformations on the same value
|
||||
transformations := []struct {
|
||||
name string
|
||||
endo Endomorphism[int]
|
||||
expected int
|
||||
}{
|
||||
{"double", double, 200},
|
||||
{"increment", increment, 101},
|
||||
{"negate", negate, -100},
|
||||
{"square", square, 10000},
|
||||
}
|
||||
|
||||
for _, tt := range transformations {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := applyToTest(tt.endo)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works with monoid operations", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// Use monoid to combine endomorphisms
|
||||
combined := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
|
||||
double,
|
||||
increment,
|
||||
})
|
||||
|
||||
result := applyTo5(combined)
|
||||
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
|
||||
assert.Equal(t, 12, result, "Read should work with monoid operations")
|
||||
})
|
||||
|
||||
t.Run("configuration example", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Timeout int
|
||||
Retries int
|
||||
}
|
||||
|
||||
baseConfig := Config{Timeout: 30, Retries: 3}
|
||||
applyToBase := Read(baseConfig)
|
||||
|
||||
withLongTimeout := func(c Config) Config {
|
||||
c.Timeout = 60
|
||||
return c
|
||||
}
|
||||
|
||||
withMoreRetries := func(c Config) Config {
|
||||
c.Retries = 5
|
||||
return c
|
||||
}
|
||||
|
||||
result1 := applyToBase(withLongTimeout)
|
||||
assert.Equal(t, Config{Timeout: 60, Retries: 3}, result1)
|
||||
|
||||
result2 := applyToBase(withMoreRetries)
|
||||
assert.Equal(t, Config{Timeout: 30, Retries: 5}, result2)
|
||||
|
||||
// Original is unchanged
|
||||
result3 := applyToBase(Identity[Config]())
|
||||
assert.Equal(t, baseConfig, result3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReadWithBuild tests the relationship between Read and Build
|
||||
func TestReadWithBuild(t *testing.T) {
|
||||
t.Run("Read applies to specific value, Build to zero value", func(t *testing.T) {
|
||||
endo := double
|
||||
|
||||
// Build applies to zero value
|
||||
builtResult := Build(endo)
|
||||
assert.Equal(t, 0, builtResult, "Build should apply to zero value: 0 * 2 = 0")
|
||||
|
||||
// Read applies to specific value
|
||||
readResult := Read(5)(endo)
|
||||
assert.Equal(t, 10, readResult, "Read should apply to captured value: 5 * 2 = 10")
|
||||
})
|
||||
|
||||
t.Run("Read can evaluate Build results", func(t *testing.T) {
|
||||
// Build an endomorphism
|
||||
builder := ConcatAll([]Endomorphism[int]{double, increment})
|
||||
|
||||
// Apply it to zero value
|
||||
builtValue := Build(builder)
|
||||
// RIGHT-TO-LEFT: increment(0) = 1, double(1) = 2
|
||||
assert.Equal(t, 2, builtValue)
|
||||
|
||||
// Now use Read to apply the same builder to a different value
|
||||
readValue := Read(5)(builder)
|
||||
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
|
||||
assert.Equal(t, 12, readValue)
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkRead benchmarks the Read function
|
||||
func BenchmarkRead(b *testing.B) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
b.Run("simple endomorphism", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = applyTo5(double)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("composed endomorphism", func(b *testing.B) {
|
||||
composed := MonadCompose(double, increment)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = applyTo5(composed)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ConcatAll endomorphism", func(b *testing.B) {
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = applyTo5(combined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,8 +144,8 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||
// combined := M.ConcatAll(monoid)(double, increment, square)
|
||||
// result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
|
||||
// combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
|
||||
// result := combined(5) // double(increment(square(5))) = double(increment(25)) = double(26) = 52
|
||||
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
|
||||
return M.MakeMonoid(MonadCompose[A], Identity[A]())
|
||||
}
|
||||
|
||||
@@ -41,20 +41,22 @@ type (
|
||||
// It's a function from A to Endomorphism[A], used for composing endomorphic operations.
|
||||
Kleisli[A any] = func(A) Endomorphism[A]
|
||||
|
||||
// Operator represents a transformation from one endomorphism to another.
|
||||
// Operator represents a higher-order transformation on endomorphisms of the same type.
|
||||
//
|
||||
// An Operator takes an endomorphism on type A and produces an endomorphism on type B.
|
||||
// This is useful for lifting operations or transforming endomorphisms in a generic way.
|
||||
// An Operator takes an endomorphism on type A and produces another endomorphism on type A.
|
||||
// Since Operator[A] = Endomorphism[Endomorphism[A]] = func(func(A)A) func(A)A,
|
||||
// both the input and output endomorphisms operate on the same type A.
|
||||
//
|
||||
// This is the return type of curried operations such as Compose, Map, and Chain.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // An operator that converts an int endomorphism to a string endomorphism
|
||||
// intToString := func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[string] {
|
||||
// return func(s string) string {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// result := f(n)
|
||||
// return strconv.Itoa(result)
|
||||
// }
|
||||
// // An operator that applies any endomorphism twice
|
||||
// var applyTwice endomorphism.Operator[int] = func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[int] {
|
||||
// return func(x int) int { return f(f(x)) }
|
||||
// }
|
||||
// double := N.Mul(2)
|
||||
// result := applyTwice(double) // double ∘ double
|
||||
// // result(5) = double(double(5)) = double(10) = 20
|
||||
Operator[A any] = Endomorphism[Endomorphism[A]]
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
15
v2/internal/filterable/types.go
Normal file
15
v2/internal/filterable/types.go
Normal file
@@ -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.
|
||||
|
||||
27
v2/internal/witherable/filter.go
Normal file
27
v2/internal/witherable/filter.go
Normal file
@@ -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,
|
||||
)
|
||||
}
|
||||
1
v2/internal/witherable/partition.go
Normal file
1
v2/internal/witherable/partition.go
Normal file
@@ -0,0 +1 @@
|
||||
package witherable
|
||||
7
v2/internal/witherable/type.go
Normal file
7
v2/internal/witherable/type.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package witherable
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
)
|
||||
195
v2/iooption/array_test.go
Normal file
195
v2/iooption/array_test.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iooption
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTraverseArray_Success(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := TraverseArray(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{2, 4, 6, 8, 10}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_WithNone(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
if n > 0 {
|
||||
return Of(n * 2)
|
||||
}
|
||||
return None[int]()
|
||||
}
|
||||
|
||||
input := []int{1, 2, -3, 4}
|
||||
result := TraverseArray(f)(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
input := []int{}
|
||||
result := TraverseArray(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_Success(t *testing.T) {
|
||||
f := func(idx, n int) IOOption[int] {
|
||||
return Of(n + idx)
|
||||
}
|
||||
|
||||
input := []int{10, 20, 30}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{10, 21, 32}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_WithNone(t *testing.T) {
|
||||
f := func(idx, n int) IOOption[int] {
|
||||
if idx < 2 {
|
||||
return Of(n + idx)
|
||||
}
|
||||
return None[int]()
|
||||
}
|
||||
|
||||
input := []int{10, 20, 30}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
|
||||
f := func(idx, n int) IOOption[int] {
|
||||
return Of(n + idx)
|
||||
}
|
||||
|
||||
input := []int{}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{}), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_AllSome(t *testing.T) {
|
||||
input := []IOOption[int]{
|
||||
Of(1),
|
||||
Of(2),
|
||||
Of(3),
|
||||
}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{1, 2, 3}), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_WithNone(t *testing.T) {
|
||||
input := []IOOption[int]{
|
||||
Of(1),
|
||||
None[int](),
|
||||
Of(3),
|
||||
}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_Empty(t *testing.T) {
|
||||
input := []IOOption[int]{}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{}), result)
|
||||
}
|
||||
|
||||
func TestSequenceArray_AllNone(t *testing.T) {
|
||||
input := []IOOption[int]{
|
||||
None[int](),
|
||||
None[int](),
|
||||
None[int](),
|
||||
}
|
||||
|
||||
result := SequenceArray(input)()
|
||||
|
||||
assert.Equal(t, O.None[[]int](), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_Composition(t *testing.T) {
|
||||
// Test composing traverse with other operations
|
||||
f := func(n int) IOOption[int] {
|
||||
if n%2 == 0 {
|
||||
return Of(n / 2)
|
||||
}
|
||||
return None[int]()
|
||||
}
|
||||
|
||||
input := []int{2, 4, 6, 8}
|
||||
result := F.Pipe1(
|
||||
input,
|
||||
TraverseArray(f),
|
||||
)()
|
||||
|
||||
assert.Equal(t, O.Some([]int{1, 2, 3, 4}), result)
|
||||
}
|
||||
|
||||
func TestTraverseArray_WithMap(t *testing.T) {
|
||||
// Test traverse followed by map
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
TraverseArray(f),
|
||||
Map(func(arr []int) int {
|
||||
sum := 0
|
||||
for _, v := range arr {
|
||||
sum += v
|
||||
}
|
||||
return sum
|
||||
}),
|
||||
)()
|
||||
|
||||
assert.Equal(t, O.Some(12), result) // (1*2 + 2*2 + 3*2) = 12
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex_UseIndex(t *testing.T) {
|
||||
// Test that index is properly used
|
||||
f := func(idx, n int) IOOption[string] {
|
||||
return Of(fmt.Sprintf("%d", idx*n*2))
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
result := TraverseArrayWithIndex(f)(input)()
|
||||
|
||||
assert.Equal(t, O.Some([]string{"0", "4", "12"}), result)
|
||||
}
|
||||
|
||||
|
||||
433
v2/iooption/iooption_comprehensive_test.go
Normal file
433
v2/iooption/iooption_comprehensive_test.go
Normal file
@@ -0,0 +1,433 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iooption
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
I "github.com/IBM/fp-go/v2/io"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
result := Of(42)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
func TestSome(t *testing.T) {
|
||||
result := Some("test")()
|
||||
assert.Equal(t, O.Some("test"), result)
|
||||
}
|
||||
|
||||
func TestNone(t *testing.T) {
|
||||
result := None[int]()()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
}
|
||||
|
||||
func TestMonadOf(t *testing.T) {
|
||||
result := MonadOf(100)()
|
||||
assert.Equal(t, O.Some(100), result)
|
||||
}
|
||||
|
||||
func TestFromOptionComprehensive(t *testing.T) {
|
||||
t.Run("from Some", func(t *testing.T) {
|
||||
result := FromOption(O.Some(42))()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("from None", func(t *testing.T) {
|
||||
result := FromOption(O.None[int]())()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ioValue := I.Of(42)
|
||||
result := FromIO(ioValue)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("map over Some", func(t *testing.T) {
|
||||
result := MonadMap(Of(5), utils.Double)()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("map over None", func(t *testing.T) {
|
||||
result := MonadMap(None[int](), utils.Double)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("chain Some to Some", func(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
result := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("chain Some to None", func(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return None[int]()
|
||||
}
|
||||
result := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("chain None", func(t *testing.T) {
|
||||
f := func(n int) IOOption[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
result := MonadChain(None[int](), f)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
f := func(n int) IOOption[string] {
|
||||
if n > 0 {
|
||||
return Of("positive")
|
||||
}
|
||||
return None[string]()
|
||||
}
|
||||
|
||||
t.Run("chain positive", func(t *testing.T) {
|
||||
result := F.Pipe1(Of(5), Chain(f))()
|
||||
assert.Equal(t, O.Some("positive"), result)
|
||||
})
|
||||
|
||||
t.Run("chain negative", func(t *testing.T) {
|
||||
result := F.Pipe1(Of(-5), Chain(f))()
|
||||
assert.Equal(t, O.None[string](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("apply Some function to Some value", func(t *testing.T) {
|
||||
mab := Of(utils.Double)
|
||||
ma := Of(5)
|
||||
result := MonadAp(mab, ma)()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("apply None function", func(t *testing.T) {
|
||||
mab := None[func(int) int]()
|
||||
ma := Of(5)
|
||||
result := MonadAp(mab, ma)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("apply to None value", func(t *testing.T) {
|
||||
mab := Of(utils.Double)
|
||||
ma := None[int]()
|
||||
result := MonadAp(mab, ma)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
ma := Of(5)
|
||||
result := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
ma := Of(5)
|
||||
result := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
ma := Of(5)
|
||||
result := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
t.Run("flatten Some(Some)", func(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
result := Flatten(nested)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("flatten Some(None)", func(t *testing.T) {
|
||||
nested := Of(None[int]())
|
||||
result := Flatten(nested)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("flatten None", func(t *testing.T) {
|
||||
nested := None[IOOption[int]]()
|
||||
result := Flatten(nested)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOptionize0(t *testing.T) {
|
||||
f := func() (int, bool) {
|
||||
return 42, true
|
||||
}
|
||||
result := Optionize0(f)()()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
|
||||
f2 := func() (int, bool) {
|
||||
return 0, false
|
||||
}
|
||||
result2 := Optionize0(f2)()()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestOptionize2(t *testing.T) {
|
||||
f := func(a, b int) (int, bool) {
|
||||
if b != 0 {
|
||||
return a / b, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
result := Optionize2(f)(10, 2)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
|
||||
result2 := Optionize2(f)(10, 0)()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestOptionize3(t *testing.T) {
|
||||
f := func(a, b, c int) (int, bool) {
|
||||
if c != 0 {
|
||||
return (a + b) / c, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
result := Optionize3(f)(10, 5, 3)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
|
||||
result2 := Optionize3(f)(10, 5, 0)()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestOptionize4(t *testing.T) {
|
||||
f := func(a, b, c, d int) (int, bool) {
|
||||
if d != 0 {
|
||||
return (a + b + c) / d, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
result := Optionize4(f)(10, 5, 3, 2)()
|
||||
assert.Equal(t, O.Some(9), result)
|
||||
|
||||
result2 := Optionize4(f)(10, 5, 3, 0)()
|
||||
assert.Equal(t, O.None[int](), result2)
|
||||
}
|
||||
|
||||
func TestMemoize(t *testing.T) {
|
||||
callCount := 0
|
||||
ioOpt := func() Option[int] {
|
||||
callCount++
|
||||
return O.Some(42)
|
||||
}
|
||||
|
||||
memoized := Memoize(ioOpt)
|
||||
|
||||
// First call
|
||||
result1 := memoized()
|
||||
assert.Equal(t, O.Some(42), result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Second call should use cached value
|
||||
result2 := memoized()
|
||||
assert.Equal(t, O.Some(42), result2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
onNone := I.Of("none")
|
||||
onSome := func(n int) I.IO[string] {
|
||||
return I.Of(fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
t.Run("fold Some", func(t *testing.T) {
|
||||
result := Fold(onNone, onSome)(Of(42))()
|
||||
assert.Equal(t, "42", result)
|
||||
})
|
||||
|
||||
t.Run("fold None", func(t *testing.T) {
|
||||
result := Fold(onNone, onSome)(None[int]())()
|
||||
assert.Equal(t, "none", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefer(t *testing.T) {
|
||||
callCount := 0
|
||||
gen := func() IOOption[int] {
|
||||
callCount++
|
||||
return Of(42)
|
||||
}
|
||||
|
||||
deferred := Defer(gen)
|
||||
|
||||
// Each call should invoke the generator
|
||||
result1 := deferred()
|
||||
assert.Equal(t, O.Some(42), result1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
result2 := deferred()
|
||||
assert.Equal(t, O.Some(42), result2)
|
||||
assert.Equal(t, 2, callCount)
|
||||
}
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
t.Run("from Right", func(t *testing.T) {
|
||||
either := ET.Right[string](42)
|
||||
result := FromEither(either)()
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("from Left", func(t *testing.T) {
|
||||
either := ET.Left[int]("error")
|
||||
result := FromEither(either)()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
t.Run("first is Some", func(t *testing.T) {
|
||||
result := MonadAlt(Of(1), Of(2))()
|
||||
assert.Equal(t, O.Some(1), result)
|
||||
})
|
||||
|
||||
t.Run("first is None, second is Some", func(t *testing.T) {
|
||||
result := MonadAlt(None[int](), Of(2))()
|
||||
assert.Equal(t, O.Some(2), result)
|
||||
})
|
||||
|
||||
t.Run("both are None", func(t *testing.T) {
|
||||
result := MonadAlt(None[int](), None[int]())()
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlt(t *testing.T) {
|
||||
t.Run("first is Some", func(t *testing.T) {
|
||||
result := F.Pipe1(Of(1), Alt(Of(2)))()
|
||||
assert.Equal(t, O.Some(1), result)
|
||||
})
|
||||
|
||||
t.Run("first is None", func(t *testing.T) {
|
||||
result := F.Pipe1(None[int](), Alt(Of(2)))()
|
||||
assert.Equal(t, O.Some(2), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) IOOption[string] {
|
||||
sideEffect = n * 2
|
||||
return Of("side effect")
|
||||
}
|
||||
|
||||
result := MonadChainFirst(Of(5), f)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) IOOption[string] {
|
||||
sideEffect = n * 2
|
||||
return Of("side effect")
|
||||
}
|
||||
|
||||
result := F.Pipe1(Of(5), ChainFirst(f))()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestMonadChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) I.IO[string] {
|
||||
return func() string {
|
||||
sideEffect = n * 2
|
||||
return "side effect"
|
||||
}
|
||||
}
|
||||
|
||||
result := MonadChainFirstIOK(Of(5), f)()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestChainFirstIOK(t *testing.T) {
|
||||
sideEffect := 0
|
||||
f := func(n int) I.IO[string] {
|
||||
return func() string {
|
||||
sideEffect = n * 2
|
||||
return "side effect"
|
||||
}
|
||||
}
|
||||
|
||||
result := F.Pipe1(Of(5), ChainFirstIOK(f))()
|
||||
assert.Equal(t, O.Some(5), result)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
}
|
||||
|
||||
func TestDelay(t *testing.T) {
|
||||
start := time.Now()
|
||||
delay := 50 * time.Millisecond
|
||||
|
||||
result := F.Pipe1(Of(42), Delay[int](delay))()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
assert.True(t, elapsed >= delay, "Expected delay of at least %v, got %v", delay, elapsed)
|
||||
}
|
||||
|
||||
func TestAfter(t *testing.T) {
|
||||
timestamp := time.Now().Add(50 * time.Millisecond)
|
||||
|
||||
result := F.Pipe1(Of(42), After[int](timestamp))()
|
||||
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
assert.True(t, time.Now().After(timestamp) || time.Now().Equal(timestamp))
|
||||
}
|
||||
|
||||
func TestMonadChainIOK(t *testing.T) {
|
||||
f := func(n int) I.IO[string] {
|
||||
return I.Of(fmt.Sprintf("%d", n))
|
||||
}
|
||||
|
||||
t.Run("chain Some", func(t *testing.T) {
|
||||
result := MonadChainIOK(Of(42), f)()
|
||||
assert.Equal(t, O.Some("42"), result)
|
||||
})
|
||||
|
||||
t.Run("chain None", func(t *testing.T) {
|
||||
result := MonadChainIOK(None[int](), f)()
|
||||
assert.Equal(t, O.None[string](), result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
241
v2/ioresult/bracket_test.go
Normal file
241
v2/ioresult/bracket_test.go
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBracket_Success(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
used = true
|
||||
return result.Of("success")
|
||||
}
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Of(F.VOID)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, used, "Resource should be used")
|
||||
assert.True(t, released, "Resource should be released")
|
||||
assert.Equal(t, result.Of("success"), res)
|
||||
}
|
||||
|
||||
func TestBracket_UseFailure(t *testing.T) {
|
||||
acquired := false
|
||||
released := false
|
||||
releaseResult := result.Result[string]{}
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
useErr := errors.New("use error")
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](useErr)
|
||||
}
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
releaseResult = res
|
||||
return result.Of(F.VOID)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, released, "Resource should be released even on use failure")
|
||||
assert.Equal(t, result.Left[string](useErr), res)
|
||||
assert.Equal(t, result.Left[string](useErr), releaseResult)
|
||||
}
|
||||
|
||||
func TestBracket_AcquireFailure(t *testing.T) {
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquireErr := errors.New("acquire error")
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Left[int](acquireErr)
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
used = true
|
||||
return result.Of("success")
|
||||
}
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Of(F.VOID)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.False(t, used, "Use should not be called if acquire fails")
|
||||
assert.False(t, released, "Release should not be called if acquire fails")
|
||||
assert.Equal(t, result.Left[string](acquireErr), res)
|
||||
}
|
||||
|
||||
func TestBracket_ReleaseFailure(t *testing.T) {
|
||||
acquired := false
|
||||
used := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
used = true
|
||||
return result.Of("success")
|
||||
}
|
||||
}
|
||||
|
||||
releaseErr := errors.New("release error")
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Left[F.Void](releaseErr)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, used, "Resource should be used")
|
||||
assert.True(t, released, "Release should be attempted")
|
||||
// When release fails, the release error is returned
|
||||
assert.Equal(t, result.Left[string](releaseErr), res)
|
||||
}
|
||||
|
||||
func TestBracket_BothUseAndReleaseFail(t *testing.T) {
|
||||
acquired := false
|
||||
released := false
|
||||
|
||||
acquire := func() IOResult[int] {
|
||||
return func() Result[int] {
|
||||
acquired = true
|
||||
return result.Of(42)
|
||||
}
|
||||
}()
|
||||
|
||||
useErr := errors.New("use error")
|
||||
use := func(n int) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](useErr)
|
||||
}
|
||||
}
|
||||
|
||||
releaseErr := errors.New("release error")
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
return func() Result[F.Void] {
|
||||
released = true
|
||||
return result.Left[F.Void](releaseErr)
|
||||
}
|
||||
}
|
||||
|
||||
res := Bracket(acquire, use, release)()
|
||||
|
||||
assert.True(t, acquired, "Resource should be acquired")
|
||||
assert.True(t, released, "Release should be attempted")
|
||||
// When both fail, the release error is returned
|
||||
assert.Equal(t, result.Left[string](releaseErr), res)
|
||||
}
|
||||
|
||||
func TestBracket_ResourceValue(t *testing.T) {
|
||||
// Test that the acquired resource value is passed correctly
|
||||
var usedValue int
|
||||
var releasedValue int
|
||||
|
||||
acquire := Of(100)
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
usedValue = n
|
||||
return Of("result")
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
releasedValue = n
|
||||
return Of(F.VOID)
|
||||
}
|
||||
|
||||
Bracket(acquire, use, release)()
|
||||
|
||||
assert.Equal(t, 100, usedValue, "Use should receive acquired value")
|
||||
assert.Equal(t, 100, releasedValue, "Release should receive acquired value")
|
||||
}
|
||||
|
||||
func TestBracket_ResultValue(t *testing.T) {
|
||||
// Test that the use result is passed to release
|
||||
var releaseReceivedResult Result[string]
|
||||
|
||||
acquire := Of(42)
|
||||
|
||||
use := func(n int) IOResult[string] {
|
||||
return Of("test result")
|
||||
}
|
||||
|
||||
release := func(n int, res Result[string]) IOResult[F.Void] {
|
||||
releaseReceivedResult = res
|
||||
return Of(F.VOID)
|
||||
}
|
||||
|
||||
Bracket(acquire, use, release)()
|
||||
|
||||
assert.Equal(t, result.Of("test result"), releaseReceivedResult)
|
||||
}
|
||||
|
||||
|
||||
581
v2/ioresult/ioresult_comprehensive_test.go
Normal file
581
v2/ioresult/ioresult_comprehensive_test.go
Normal file
@@ -0,0 +1,581 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioresult
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
ET "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLeft(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := Left[int](err)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
}
|
||||
|
||||
func TestRight(t *testing.T) {
|
||||
res := Right(42)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
res := Of(42)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestMonadOf(t *testing.T) {
|
||||
res := MonadOf(42)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestLeftIO(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := LeftIO[int](io.Of(err))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
}
|
||||
|
||||
func TestRightIO(t *testing.T) {
|
||||
res := RightIO(io.Of(42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestFromEither(t *testing.T) {
|
||||
t.Run("from Right", func(t *testing.T) {
|
||||
either := result.Of(42)
|
||||
res := FromEither(either)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("from Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
either := result.Left[int](err)
|
||||
res := FromEither(either)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResult(t *testing.T) {
|
||||
t.Run("from success", func(t *testing.T) {
|
||||
res := FromResult(result.Of(42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("from error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := FromResult(result.Left[int](err))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromEitherI(t *testing.T) {
|
||||
t.Run("with nil error", func(t *testing.T) {
|
||||
res := FromEitherI(42, nil)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := FromEitherI(0, err)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromResultI(t *testing.T) {
|
||||
t.Run("with nil error", func(t *testing.T) {
|
||||
res := FromResultI(42, nil)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("with error", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := FromResultI(0, err)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromOption_Success(t *testing.T) {
|
||||
onNone := func() error {
|
||||
return errors.New("none")
|
||||
}
|
||||
|
||||
t.Run("from Some", func(t *testing.T) {
|
||||
res := FromOption[int](onNone)(O.Some(42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("from None", func(t *testing.T) {
|
||||
res := FromOption[int](onNone)(O.None[int]())()
|
||||
assert.Equal(t, result.Left[int](errors.New("none")), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIO(t *testing.T) {
|
||||
ioValue := io.Of(42)
|
||||
res := FromIO(ioValue)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestFromLazy(t *testing.T) {
|
||||
lazy := func() int { return 42 }
|
||||
res := FromLazy(lazy)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("map over Right", func(t *testing.T) {
|
||||
res := MonadMap(Of(5), utils.Double)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("map over Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := MonadMap(Left[int](err), utils.Double)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap_Comprehensive(t *testing.T) {
|
||||
double := func(n int) int { return n * 2 }
|
||||
|
||||
t.Run("map Right", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(5), Map(double))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("map Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := F.Pipe1(Left[int](err), Map(double))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("mapTo Right", func(t *testing.T) {
|
||||
res := MonadMapTo(Of(5), "constant")()
|
||||
assert.Equal(t, result.Of("constant"), res)
|
||||
})
|
||||
|
||||
t.Run("mapTo Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := MonadMapTo(Left[int](err), "constant")()
|
||||
assert.Equal(t, result.Left[string](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
res := F.Pipe1(Of(5), MapTo[int]("constant"))()
|
||||
assert.Equal(t, result.Of("constant"), res)
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
f := func(n int) IOResult[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
t.Run("chain Right to Right", func(t *testing.T) {
|
||||
res := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("chain Right to Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
f := func(n int) IOResult[int] {
|
||||
return Left[int](err)
|
||||
}
|
||||
res := MonadChain(Of(5), f)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("chain Left", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
res := MonadChain(Left[int](err), f)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain_Comprehensive(t *testing.T) {
|
||||
f := func(n int) IOResult[string] {
|
||||
if n > 0 {
|
||||
return Of(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return Left[string](errors.New("negative"))
|
||||
}
|
||||
|
||||
t.Run("chain positive", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(5), Chain(f))()
|
||||
assert.Equal(t, result.Of("5"), res)
|
||||
})
|
||||
|
||||
t.Run("chain negative", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(-5), Chain(f))()
|
||||
assert.Equal(t, result.Left[string](errors.New("negative")), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainEitherK(t *testing.T) {
|
||||
f := func(n int) result.Result[int] {
|
||||
if n > 0 {
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
return result.Left[int](errors.New("non-positive"))
|
||||
}
|
||||
|
||||
t.Run("chain to success", func(t *testing.T) {
|
||||
res := MonadChainEitherK(Of(5), f)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("chain to error", func(t *testing.T) {
|
||||
res := MonadChainEitherK(Of(-5), f)()
|
||||
assert.Equal(t, result.Left[int](errors.New("non-positive")), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainResultK(t *testing.T) {
|
||||
f := func(n int) result.Result[int] {
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
|
||||
res := MonadChainResultK(Of(5), f)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestChainResultK(t *testing.T) {
|
||||
f := func(n int) result.Result[int] {
|
||||
return result.Of(n * 2)
|
||||
}
|
||||
|
||||
res := F.Pipe1(Of(5), ChainResultK(f))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestMonadAp_Comprehensive(t *testing.T) {
|
||||
t.Run("apply Right function to Right value", func(t *testing.T) {
|
||||
mab := Of(utils.Double)
|
||||
ma := Of(5)
|
||||
res := MonadAp(mab, ma)()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
})
|
||||
|
||||
t.Run("apply Left function", func(t *testing.T) {
|
||||
err := errors.New("function error")
|
||||
mab := Left[func(int) int](err)
|
||||
ma := Of(5)
|
||||
res := MonadAp(mab, ma)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("apply to Left value", func(t *testing.T) {
|
||||
err := errors.New("value error")
|
||||
mab := Of(utils.Double)
|
||||
ma := Left[int](err)
|
||||
res := MonadAp(mab, ma)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp_Comprehensive(t *testing.T) {
|
||||
ma := Of(5)
|
||||
res := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestApPar(t *testing.T) {
|
||||
ma := Of(5)
|
||||
res := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestApSeq(t *testing.T) {
|
||||
ma := Of(5)
|
||||
res := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
|
||||
assert.Equal(t, result.Of(10), res)
|
||||
}
|
||||
|
||||
func TestFlatten_Comprehensive(t *testing.T) {
|
||||
t.Run("flatten Right(Right)", func(t *testing.T) {
|
||||
nested := Of(Of(42))
|
||||
res := Flatten(nested)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("flatten Right(Left)", func(t *testing.T) {
|
||||
err := errors.New("inner error")
|
||||
nested := Of(Left[int](err))
|
||||
res := Flatten(nested)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("flatten Left", func(t *testing.T) {
|
||||
err := errors.New("outer error")
|
||||
nested := Left[IOResult[int]](err)
|
||||
res := Flatten(nested)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTryCatch(t *testing.T) {
|
||||
t.Run("successful function", func(t *testing.T) {
|
||||
f := func() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
res := TryCatch(f, F.Identity[error])()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("failing function", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
f := func() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
res := TryCatch(f, F.Identity[error])()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("with error transformation", func(t *testing.T) {
|
||||
err := errors.New("original")
|
||||
f := func() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
onThrow := func(e error) error {
|
||||
return fmt.Errorf("wrapped: %w", e)
|
||||
}
|
||||
res := TryCatch(f, onThrow)()
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTryCatchError_Comprehensive(t *testing.T) {
|
||||
t.Run("successful function", func(t *testing.T) {
|
||||
f := func() (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
res := TryCatchError(f)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("failing function", func(t *testing.T) {
|
||||
err := errors.New("test error")
|
||||
f := func() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
res := TryCatchError(f)()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMemoize_Comprehensive(t *testing.T) {
|
||||
callCount := 0
|
||||
ioRes := func() Result[int] {
|
||||
callCount++
|
||||
return result.Of(42)
|
||||
}
|
||||
|
||||
memoized := Memoize(ioRes)
|
||||
|
||||
// First call
|
||||
res1 := memoized()
|
||||
assert.Equal(t, result.Of(42), res1)
|
||||
assert.Equal(t, 1, callCount)
|
||||
|
||||
// Second call should use cached value
|
||||
res2 := memoized()
|
||||
assert.Equal(t, result.Of(42), res2)
|
||||
assert.Equal(t, 1, callCount)
|
||||
}
|
||||
|
||||
func TestMonadMapLeft(t *testing.T) {
|
||||
t.Run("map Left error", func(t *testing.T) {
|
||||
err := errors.New("original")
|
||||
f := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
res := MonadMapLeft(Left[int](err), f)()
|
||||
// Result is IOEither[string, int], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("map Right unchanged", func(t *testing.T) {
|
||||
f := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
res := MonadMapLeft(Of(42), f)()
|
||||
// MapLeft changes the error type, so result is IOEither[string, int]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMapLeft_Comprehensive(t *testing.T) {
|
||||
f := func(e error) string {
|
||||
return fmt.Sprintf("wrapped: %s", e.Error())
|
||||
}
|
||||
|
||||
t.Run("map Left", func(t *testing.T) {
|
||||
err := errors.New("original")
|
||||
res := F.Pipe1(Left[int](err), MapLeft[int](f))()
|
||||
// Result is IOEither[string, int], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
|
||||
t.Run("map Right unchanged", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(42), MapLeft[int](f))()
|
||||
// MapLeft changes the error type, so result is IOEither[string, int]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadBiMap(t *testing.T) {
|
||||
leftF := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
rightF := func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
t.Run("bimap Right", func(t *testing.T) {
|
||||
res := MonadBiMap(Of(42), leftF, rightF)()
|
||||
// BiMap changes both types, so result is IOEither[string, string]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
|
||||
})
|
||||
|
||||
t.Run("bimap Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := MonadBiMap(Left[int](err), leftF, rightF)()
|
||||
// Result is IOEither[string, string], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBiMap_Comprehensive(t *testing.T) {
|
||||
leftF := func(e error) string {
|
||||
return e.Error()
|
||||
}
|
||||
rightF := func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
|
||||
t.Run("bimap Right", func(t *testing.T) {
|
||||
res := F.Pipe1(Of(42), BiMap(leftF, rightF))()
|
||||
// BiMap changes both types, so result is IOEither[string, string]
|
||||
assert.True(t, ET.IsRight(res))
|
||||
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
|
||||
})
|
||||
|
||||
t.Run("bimap Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := F.Pipe1(Left[int](err), BiMap(leftF, rightF))()
|
||||
// Result is IOEither[string, string], check it's a left
|
||||
assert.True(t, ET.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFold_Comprehensive(t *testing.T) {
|
||||
onLeft := func(e error) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("error: %s", e.Error()))
|
||||
}
|
||||
onRight := func(n int) io.IO[string] {
|
||||
return io.Of(fmt.Sprintf("value: %d", n))
|
||||
}
|
||||
|
||||
t.Run("fold Right", func(t *testing.T) {
|
||||
res := Fold(onLeft, onRight)(Of(42))()
|
||||
assert.Equal(t, "value: 42", res)
|
||||
})
|
||||
|
||||
t.Run("fold Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := Fold(onLeft, onRight)(Left[int](err))()
|
||||
assert.Equal(t, "error: test", res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOrElse_Comprehensive(t *testing.T) {
|
||||
onLeft := func(e error) io.IO[int] {
|
||||
return io.Of(0)
|
||||
}
|
||||
|
||||
t.Run("get Right value", func(t *testing.T) {
|
||||
res := GetOrElse(onLeft)(Of(42))()
|
||||
assert.Equal(t, 42, res)
|
||||
})
|
||||
|
||||
t.Run("get default on Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := GetOrElse(onLeft)(Left[int](err))()
|
||||
assert.Equal(t, 0, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOrElseOf(t *testing.T) {
|
||||
onLeft := func(e error) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
t.Run("get Right value", func(t *testing.T) {
|
||||
res := GetOrElseOf(onLeft)(Of(42))()
|
||||
assert.Equal(t, 42, res)
|
||||
})
|
||||
|
||||
t.Run("get default on Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := GetOrElseOf(onLeft)(Left[int](err))()
|
||||
assert.Equal(t, 0, res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainTo(t *testing.T) {
|
||||
t.Run("chain Right to Right", func(t *testing.T) {
|
||||
res := MonadChainTo(Of(1), Of(2))()
|
||||
assert.Equal(t, result.Of(2), res)
|
||||
})
|
||||
|
||||
t.Run("chain Right to Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := MonadChainTo(Of(1), Left[int](err))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
|
||||
t.Run("chain Left", func(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
res := MonadChainTo(Left[int](err), Of(2))()
|
||||
assert.Equal(t, result.Left[int](err), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainLazyK(t *testing.T) {
|
||||
f := func(n int) Lazy[string] {
|
||||
return func() string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
res := F.Pipe1(Of(42), ChainLazyK(f))()
|
||||
assert.Equal(t, result.Of("42"), res)
|
||||
}
|
||||
|
||||
|
||||
195
v2/iterator/iter/async.go
Normal file
195
v2/iterator/iter/async.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Async 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 := Async(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 := Async(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 := Async(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 Async[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
return func(yield func(T) bool) {
|
||||
ch := make(chan T, N.Max(bufSize, 0))
|
||||
done := make(chan Void)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for v := range input {
|
||||
select {
|
||||
case ch <- v:
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer close(done)
|
||||
for v := range ch {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Async2 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 := Async2(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 := Async2(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 Async2[K, V any](input Seq2[K, V], bufSize int) Seq2[K, V] {
|
||||
return FromSeqPair(Async(ToSeqPair(input), bufSize))
|
||||
}
|
||||
905
v2/iterator/iter/async_test.go
Normal file
905
v2/iterator/iter/async_test.go
Normal file
@@ -0,0 +1,905 @@
|
||||
// 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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
async := Async(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty with zero buffer", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(seq, 3)
|
||||
async2 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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 := Async2(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:
|
||||
@@ -473,6 +497,12 @@ func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
|
||||
// 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:
|
||||
@@ -489,6 +519,14 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
// 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 +615,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 +856,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 +884,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 +905,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 +927,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 +1074,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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"log"
|
||||
"log/slog"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// LoggingCallbacks creates a pair of logging callback functions from the provided loggers.
|
||||
@@ -128,6 +130,7 @@ var loggerInContextKey loggerInContextType
|
||||
// logger.Info("Processing request")
|
||||
// }
|
||||
func GetLoggerFromContext(ctx context.Context) *slog.Logger {
|
||||
// using idomatic style to avoid import cycle
|
||||
value, ok := ctx.Value(loggerInContextKey).(*slog.Logger)
|
||||
if !ok {
|
||||
return globalLogger.Load()
|
||||
@@ -135,9 +138,11 @@ func GetLoggerFromContext(ctx context.Context) *slog.Logger {
|
||||
return value
|
||||
}
|
||||
|
||||
// WithLogger returns an endomorphism that adds a logger to a context.
|
||||
// An endomorphism is a function that takes a value and returns a value of the same type.
|
||||
// This function creates a context transformation that embeds the provided logger.
|
||||
func noop() {}
|
||||
|
||||
// WithLogger returns a Kleisli arrow that adds a logger to a context.
|
||||
// A Kleisli arrow transforms a context into a ContextCancel pair containing
|
||||
// a no-op cancel function and the new context with the embedded logger.
|
||||
//
|
||||
// This is particularly useful in functional programming patterns where you want to
|
||||
// compose context transformations, or when working with middleware that needs to
|
||||
@@ -147,7 +152,7 @@ func GetLoggerFromContext(ctx context.Context) *slog.Logger {
|
||||
// - l: The *slog.Logger to embed in the context
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism[context.Context] function that adds the logger to a context
|
||||
// - A Kleisli arrow (function from context.Context to ContextCancel) that adds the logger to a context
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -156,13 +161,14 @@ func GetLoggerFromContext(ctx context.Context) *slog.Logger {
|
||||
//
|
||||
// // Apply it to a context
|
||||
// ctx := context.Background()
|
||||
// ctxWithLogger := addLogger(ctx)
|
||||
// result := addLogger(ctx)
|
||||
// ctxWithLogger := pair.Second(result)
|
||||
//
|
||||
// // Retrieve the logger later
|
||||
// logger := GetLoggerFromContext(ctxWithLogger)
|
||||
// logger.Info("Using context logger")
|
||||
func WithLogger(l *slog.Logger) Endomorphism[context.Context] {
|
||||
return func(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, loggerInContextKey, l)
|
||||
func WithLogger(l *slog.Logger) pair.Kleisli[context.CancelFunc, context.Context, context.Context] {
|
||||
return func(ctx context.Context) ContextCancel {
|
||||
return pair.MakePair[context.CancelFunc](noop, context.WithValue(ctx, loggerInContextKey, l))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@ package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
@@ -288,3 +291,355 @@ func BenchmarkLoggingCallbacks_Logging(b *testing.B) {
|
||||
infoLog("benchmark message %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetLogger_Success tests setting a new global logger and verifying it returns the old one.
|
||||
func TestSetLogger_Success(t *testing.T) {
|
||||
// Save original logger to restore later
|
||||
originalLogger := GetLogger()
|
||||
defer SetLogger(originalLogger)
|
||||
|
||||
// Create a new logger
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
newLogger := slog.New(handler)
|
||||
|
||||
// Set the new logger
|
||||
oldLogger := SetLogger(newLogger)
|
||||
|
||||
// Verify old logger was returned
|
||||
if oldLogger == nil {
|
||||
t.Error("Expected SetLogger to return the previous logger")
|
||||
}
|
||||
|
||||
// Verify new logger is now active
|
||||
currentLogger := GetLogger()
|
||||
if currentLogger != newLogger {
|
||||
t.Error("Expected GetLogger to return the newly set logger")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetLogger_Multiple tests setting logger multiple times.
|
||||
func TestSetLogger_Multiple(t *testing.T) {
|
||||
// Save original logger to restore later
|
||||
originalLogger := GetLogger()
|
||||
defer SetLogger(originalLogger)
|
||||
|
||||
// Create three loggers
|
||||
logger1 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
logger2 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
logger3 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
|
||||
// Set first logger
|
||||
old1 := SetLogger(logger1)
|
||||
if GetLogger() != logger1 {
|
||||
t.Error("Expected logger1 to be active")
|
||||
}
|
||||
|
||||
// Set second logger
|
||||
old2 := SetLogger(logger2)
|
||||
if old2 != logger1 {
|
||||
t.Error("Expected SetLogger to return logger1")
|
||||
}
|
||||
if GetLogger() != logger2 {
|
||||
t.Error("Expected logger2 to be active")
|
||||
}
|
||||
|
||||
// Set third logger
|
||||
old3 := SetLogger(logger3)
|
||||
if old3 != logger2 {
|
||||
t.Error("Expected SetLogger to return logger2")
|
||||
}
|
||||
if GetLogger() != logger3 {
|
||||
t.Error("Expected logger3 to be active")
|
||||
}
|
||||
|
||||
// Restore to original
|
||||
restored := SetLogger(old1)
|
||||
if restored != logger3 {
|
||||
t.Error("Expected SetLogger to return logger3")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetLogger_Default tests that GetLogger returns a valid logger by default.
|
||||
func TestGetLogger_Default(t *testing.T) {
|
||||
logger := GetLogger()
|
||||
|
||||
if logger == nil {
|
||||
t.Error("Expected GetLogger to return a non-nil logger")
|
||||
}
|
||||
|
||||
// Verify it's usable
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
testLogger := slog.New(handler)
|
||||
|
||||
oldLogger := SetLogger(testLogger)
|
||||
defer SetLogger(oldLogger)
|
||||
|
||||
GetLogger().Info("test message")
|
||||
if !strings.Contains(buf.String(), "test message") {
|
||||
t.Errorf("Expected logger to log message, got: %s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetLogger_AfterSet tests that GetLogger returns the logger set by SetLogger.
|
||||
func TestGetLogger_AfterSet(t *testing.T) {
|
||||
originalLogger := GetLogger()
|
||||
defer SetLogger(originalLogger)
|
||||
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
customLogger := slog.New(handler)
|
||||
|
||||
SetLogger(customLogger)
|
||||
|
||||
retrievedLogger := GetLogger()
|
||||
if retrievedLogger != customLogger {
|
||||
t.Error("Expected GetLogger to return the custom logger")
|
||||
}
|
||||
|
||||
// Verify it's the same instance by logging
|
||||
retrievedLogger.Info("test")
|
||||
if !strings.Contains(buf.String(), "test") {
|
||||
t.Error("Expected retrieved logger to be the same instance")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetLoggerFromContext_WithLogger tests retrieving a logger from context.
|
||||
func TestGetLoggerFromContext_WithLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
contextLogger := slog.New(handler)
|
||||
|
||||
// Create context with logger using WithLogger
|
||||
ctx := context.Background()
|
||||
kleisli := WithLogger(contextLogger)
|
||||
result := kleisli(ctx)
|
||||
ctxWithLogger := pair.Second(result)
|
||||
|
||||
// Retrieve logger from context
|
||||
retrievedLogger := GetLoggerFromContext(ctxWithLogger)
|
||||
|
||||
if retrievedLogger != contextLogger {
|
||||
t.Error("Expected to retrieve the context logger")
|
||||
}
|
||||
|
||||
// Verify it's the same instance by logging
|
||||
retrievedLogger.Info("context test")
|
||||
if !strings.Contains(buf.String(), "context test") {
|
||||
t.Error("Expected retrieved logger to be the same instance")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetLoggerFromContext_WithoutLogger tests that it returns global logger when context has no logger.
|
||||
func TestGetLoggerFromContext_WithoutLogger(t *testing.T) {
|
||||
originalLogger := GetLogger()
|
||||
defer SetLogger(originalLogger)
|
||||
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
globalLogger := slog.New(handler)
|
||||
SetLogger(globalLogger)
|
||||
|
||||
// Create context without logger
|
||||
ctx := context.Background()
|
||||
|
||||
// Should return global logger
|
||||
retrievedLogger := GetLoggerFromContext(ctx)
|
||||
|
||||
if retrievedLogger != globalLogger {
|
||||
t.Error("Expected to retrieve the global logger when context has no logger")
|
||||
}
|
||||
|
||||
// Verify it's the same instance
|
||||
retrievedLogger.Info("global test")
|
||||
if !strings.Contains(buf.String(), "global test") {
|
||||
t.Error("Expected retrieved logger to be the global logger")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetLoggerFromContext_NilContext tests behavior with nil context value.
|
||||
func TestGetLoggerFromContext_NilContext(t *testing.T) {
|
||||
originalLogger := GetLogger()
|
||||
defer SetLogger(originalLogger)
|
||||
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
globalLogger := slog.New(handler)
|
||||
SetLogger(globalLogger)
|
||||
|
||||
// Create context with wrong type value
|
||||
ctx := context.WithValue(context.Background(), loggerInContextKey, "not a logger")
|
||||
|
||||
// Should return global logger when type assertion fails
|
||||
retrievedLogger := GetLoggerFromContext(ctx)
|
||||
|
||||
if retrievedLogger != globalLogger {
|
||||
t.Error("Expected to retrieve the global logger when context value is wrong type")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithLogger_CreatesContextWithLogger tests that WithLogger adds logger to context.
|
||||
func TestWithLogger_CreatesContextWithLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
testLogger := slog.New(handler)
|
||||
|
||||
// Create Kleisli arrow
|
||||
kleisli := WithLogger(testLogger)
|
||||
|
||||
// Apply to context
|
||||
ctx := context.Background()
|
||||
result := kleisli(ctx)
|
||||
|
||||
// Verify result is a ContextCancel pair
|
||||
cancelFunc := pair.First(result)
|
||||
newCtx := pair.Second(result)
|
||||
|
||||
if cancelFunc == nil {
|
||||
t.Error("Expected cancel function to be non-nil")
|
||||
}
|
||||
|
||||
if newCtx == nil {
|
||||
t.Error("Expected new context to be non-nil")
|
||||
}
|
||||
|
||||
// Verify logger is in context
|
||||
retrievedLogger := GetLoggerFromContext(newCtx)
|
||||
if retrievedLogger != testLogger {
|
||||
t.Error("Expected logger to be in the new context")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithLogger_CancelFuncIsNoop tests that the cancel function is a no-op.
|
||||
func TestWithLogger_CancelFuncIsNoop(t *testing.T) {
|
||||
testLogger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
kleisli := WithLogger(testLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
result := kleisli(ctx)
|
||||
cancelFunc := pair.First(result)
|
||||
|
||||
// Calling cancel should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("Cancel function panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
cancelFunc()
|
||||
}
|
||||
|
||||
// TestWithLogger_PreservesOriginalContext tests that original context is not modified.
|
||||
func TestWithLogger_PreservesOriginalContext(t *testing.T) {
|
||||
originalLogger := GetLogger()
|
||||
defer SetLogger(originalLogger)
|
||||
|
||||
var buf bytes.Buffer
|
||||
handler := slog.NewTextHandler(&buf, nil)
|
||||
globalLogger := slog.New(handler)
|
||||
SetLogger(globalLogger)
|
||||
|
||||
testLogger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
kleisli := WithLogger(testLogger)
|
||||
|
||||
// Original context without logger
|
||||
originalCtx := context.Background()
|
||||
|
||||
// Apply transformation
|
||||
result := kleisli(originalCtx)
|
||||
newCtx := pair.Second(result)
|
||||
|
||||
// Original context should still return global logger
|
||||
originalCtxLogger := GetLoggerFromContext(originalCtx)
|
||||
if originalCtxLogger != globalLogger {
|
||||
t.Error("Expected original context to still use global logger")
|
||||
}
|
||||
|
||||
// New context should have the test logger
|
||||
newCtxLogger := GetLoggerFromContext(newCtx)
|
||||
if newCtxLogger != testLogger {
|
||||
t.Error("Expected new context to have the test logger")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithLogger_Composition tests composing multiple WithLogger calls.
|
||||
func TestWithLogger_Composition(t *testing.T) {
|
||||
logger1 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
logger2 := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
|
||||
kleisli1 := WithLogger(logger1)
|
||||
kleisli2 := WithLogger(logger2)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Apply first transformation
|
||||
result1 := kleisli1(ctx)
|
||||
ctx1 := pair.Second(result1)
|
||||
|
||||
// Verify first logger
|
||||
if GetLoggerFromContext(ctx1) != logger1 {
|
||||
t.Error("Expected first logger in context after first transformation")
|
||||
}
|
||||
|
||||
// Apply second transformation (should override)
|
||||
result2 := kleisli2(ctx1)
|
||||
ctx2 := pair.Second(result2)
|
||||
|
||||
// Verify second logger (should override first)
|
||||
if GetLoggerFromContext(ctx2) != logger2 {
|
||||
t.Error("Expected second logger to override first logger")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkSetLogger benchmarks setting the global logger.
|
||||
func BenchmarkSetLogger(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
SetLogger(logger)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetLogger benchmarks getting the global logger.
|
||||
func BenchmarkGetLogger(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetLogger()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetLoggerFromContext_WithLogger benchmarks retrieving logger from context.
|
||||
func BenchmarkGetLoggerFromContext_WithLogger(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
kleisli := WithLogger(logger)
|
||||
ctx := pair.Second(kleisli(context.Background()))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetLoggerFromContext(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetLoggerFromContext_WithoutLogger benchmarks retrieving global logger from context.
|
||||
func BenchmarkGetLoggerFromContext_WithoutLogger(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetLoggerFromContext(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkWithLogger benchmarks creating context with logger.
|
||||
func BenchmarkWithLogger(b *testing.B) {
|
||||
logger := slog.New(slog.NewTextHandler(&bytes.Buffer{}, nil))
|
||||
kleisli := WithLogger(logger)
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
kleisli(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -39,4 +42,15 @@ type (
|
||||
// ctx := context.Background()
|
||||
// newCtx := addLogger(ctx) // Both ctx and newCtx are context.Context
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Pair represents a tuple of two values of types A and B.
|
||||
// It is used to group two related values together.
|
||||
Pair[A, B any] = pair.Pair[A, B]
|
||||
|
||||
// ContextCancel represents a pair of a cancel function and a context.
|
||||
// It is used in operations that create new contexts with cancellation capabilities.
|
||||
//
|
||||
// The first element is the CancelFunc that should be called to release resources.
|
||||
// The second element is the new Context that was created.
|
||||
ContextCancel = Pair[context.CancelFunc, context.Context]
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,13 +20,89 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// Do creates the initial empty codec to be used as the starting point for
|
||||
// do-notation style codec construction.
|
||||
//
|
||||
// This is the entry point for building up a struct codec field-by-field using
|
||||
// the applicative and monadic sequencing operators ApSL, ApSO, and Bind.
|
||||
// It wraps Empty and lifts a lazily-evaluated default Pair[O, A] into a
|
||||
// Type[A, O, I] that ignores its input and always succeeds with the default value.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type for decoding (what the codec reads from)
|
||||
// - A: The target struct type being built up (what the codec decodes to)
|
||||
// - O: The output type for encoding (what the codec writes to)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - e: A Lazy[Pair[O, A]] providing the initial default values:
|
||||
// - pair.Head(e()): The default encoded output O (e.g. an empty monoid value)
|
||||
// - pair.Tail(e()): The initial zero value of the struct A (e.g. MyStruct{})
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Type[A, O, I] that always decodes to the default A and encodes to the
|
||||
// default O, regardless of input. This is then transformed by chaining
|
||||
// ApSL, ApSO, or Bind operators to add fields one by one.
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Building a struct codec using do-notation style:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/lazy"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// "github.com/IBM/fp-go/v2/pair"
|
||||
// S "github.com/IBM/fp-go/v2/string"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// nameLens := lens.MakeLens(
|
||||
// func(p Person) string { return p.Name },
|
||||
// func(p Person, name string) Person { p.Name = name; return p },
|
||||
// )
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, age int) Person { p.Age = age; return p },
|
||||
// )
|
||||
//
|
||||
// personCodec := F.Pipe2(
|
||||
// codec.Do[any, Person, string](lazy.Of(pair.MakePair("", Person{}))),
|
||||
// codec.ApSL(S.Monoid, nameLens, codec.String()),
|
||||
// codec.ApSL(S.Monoid, ageLens, codec.Int()),
|
||||
// )
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Do is typically the first call in a codec pipeline, followed by ApSL, ApSO, or Bind
|
||||
// - The lazy pair should use the monoid's empty value for O and the zero value for A
|
||||
// - For convenience, use Struct to create the initial codec for named struct types
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Empty: The underlying codec constructor that Do delegates to
|
||||
// - ApSL: Applicative sequencing for required struct fields via Lens
|
||||
// - ApSO: Applicative sequencing for optional struct fields via Optional
|
||||
// - Bind: Monadic sequencing for context-dependent field codecs
|
||||
//
|
||||
//go:inline
|
||||
func Do[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
|
||||
return Empty[I](e)
|
||||
}
|
||||
|
||||
// ApSL creates an applicative sequencing operator for codecs using a lens.
|
||||
//
|
||||
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
|
||||
@@ -403,48 +479,26 @@ func ApSO[S, T, O, I any](
|
||||
// See also:
|
||||
// - ApSL: Applicative sequencing with a fixed lens codec (error accumulation)
|
||||
// - Kleisli: The function type from S to Type[T, O, I]
|
||||
// - decode.Bind: The underlying decode-level bind combinator
|
||||
// - validate.Bind: The underlying validate-level bind combinator
|
||||
func Bind[S, T, O, I any](
|
||||
m Monoid[O],
|
||||
l Lens[S, T],
|
||||
f Kleisli[S, T, O, I],
|
||||
) Operator[S, S, O, I] {
|
||||
name := fmt.Sprintf("Bind[%s]", l)
|
||||
val := reader.Curry(Type[T, O, I].Validate)
|
||||
rm := reader.ApplicativeMonoid[S](m)
|
||||
val := F.Curry2(Type[T, O, I].Validate)
|
||||
|
||||
return func(t Type[S, O, I]) Type[S, O, I] {
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
t.Is,
|
||||
func(i I) Decode[validate.Context, S] {
|
||||
|
||||
bind := decode.Bind(
|
||||
l.Set,
|
||||
F.Flow2(
|
||||
f,
|
||||
val(i),
|
||||
),
|
||||
)
|
||||
|
||||
return F.Pipe2(
|
||||
i,
|
||||
t.Validate,
|
||||
bind,
|
||||
)
|
||||
},
|
||||
F.Pipe1(
|
||||
t.Validate,
|
||||
validate.Bind(l.Set, F.Flow2(f, val)),
|
||||
),
|
||||
func(s S) O {
|
||||
fa := f(s)
|
||||
|
||||
encConcat := F.Pipe1(
|
||||
F.Flow2(
|
||||
l.Get,
|
||||
fa.Encode,
|
||||
),
|
||||
semigroup.AppendTo(rm),
|
||||
)
|
||||
|
||||
return encConcat(t.Encode)(s)
|
||||
return m.Concat(t.Encode(s), f(s).Encode(l.Get(s)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -343,6 +343,61 @@ func Int64FromString() Type[int64, string, string] {
|
||||
)
|
||||
}
|
||||
|
||||
// BoolFromString creates a bidirectional codec for parsing boolean values from strings.
|
||||
// This codec converts string representations of booleans to bool values and vice versa.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string to a bool using strconv.ParseBool
|
||||
// - Encodes: Converts a bool to its string representation using strconv.FormatBool
|
||||
// - Validates: Ensures the string contains a valid boolean value
|
||||
//
|
||||
// The codec accepts the following string values (case-insensitive):
|
||||
// - true: "1", "t", "T", "true", "TRUE", "True"
|
||||
// - false: "0", "f", "F", "false", "FALSE", "False"
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[bool, string, string] codec that handles bool/string conversions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// boolCodec := BoolFromString()
|
||||
//
|
||||
// // Decode valid boolean strings
|
||||
// validation := boolCodec.Decode("true")
|
||||
// // validation is Right(true)
|
||||
//
|
||||
// validation := boolCodec.Decode("1")
|
||||
// // validation is Right(true)
|
||||
//
|
||||
// validation := boolCodec.Decode("false")
|
||||
// // validation is Right(false)
|
||||
//
|
||||
// validation := boolCodec.Decode("0")
|
||||
// // validation is Right(false)
|
||||
//
|
||||
// // Encode a boolean to string
|
||||
// str := boolCodec.Encode(true)
|
||||
// // str is "true"
|
||||
//
|
||||
// str := boolCodec.Encode(false)
|
||||
// // str is "false"
|
||||
//
|
||||
// // Invalid boolean string fails validation
|
||||
// validation := boolCodec.Decode("yes")
|
||||
// // validation is Left(ValidationError{...})
|
||||
//
|
||||
// // Case variations are accepted
|
||||
// validation := boolCodec.Decode("TRUE")
|
||||
// // validation is Right(true)
|
||||
func BoolFromString() Type[bool, string, string] {
|
||||
return MakeType(
|
||||
"BoolFromString",
|
||||
Is[bool](),
|
||||
validateFromParser(strconv.ParseBool),
|
||||
strconv.FormatBool,
|
||||
)
|
||||
}
|
||||
|
||||
func decodeJSON[T any](dec json.Unmarshaler) ReaderResult[[]byte, T] {
|
||||
return func(b []byte) Result[T] {
|
||||
var t T
|
||||
|
||||
@@ -461,6 +461,233 @@ func TestInt64FromString_Name(t *testing.T) {
|
||||
assert.Equal(t, "Int64FromString", Int64FromString().Name())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BoolFromString
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestBoolFromString_Decode_Success(t *testing.T) {
|
||||
t.Run("decodes 'true' string", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("true")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'false' string", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("false")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes '1' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("1")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes '0' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("0")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 't' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("t")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'f' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("f")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'T' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("T")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'F' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("F")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'TRUE' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("TRUE")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'FALSE' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("FALSE")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'True' as true", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("True")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes 'False' as false", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("False")
|
||||
assert.Equal(t, validation.Success(false), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_Decode_Failure(t *testing.T) {
|
||||
t.Run("fails on 'yes'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("yes")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on 'no'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("no")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on empty string", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on numeric string other than 0 or 1", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("2")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on arbitrary text", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("not a boolean")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on whitespace", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode(" ")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails on 'true' with leading/trailing spaces", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode(" true ")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_Encode(t *testing.T) {
|
||||
t.Run("encodes true to 'true'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
assert.Equal(t, "true", c.Encode(true))
|
||||
})
|
||||
|
||||
t.Run("encodes false to 'false'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
assert.Equal(t, "false", c.Encode(false))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode 'true' then encode", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("true")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return false }, func(b bool) bool { return b })
|
||||
assert.Equal(t, "true", c.Encode(b))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode 'false' then encode", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("false")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return true }, func(b bool) bool { return b })
|
||||
assert.Equal(t, "false", c.Encode(b))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode '1' encodes as 'true'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("1")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return false }, func(b bool) bool { return b })
|
||||
// Note: strconv.FormatBool always returns "true" or "false", not "1" or "0"
|
||||
assert.Equal(t, "true", c.Encode(b))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode '0' encodes as 'false'", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
result := c.Decode("0")
|
||||
require.True(t, either.IsRight(result))
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return true }, func(b bool) bool { return b })
|
||||
assert.Equal(t, "false", c.Encode(b))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_EdgeCases(t *testing.T) {
|
||||
t.Run("case sensitivity variations", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
cases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"True", true},
|
||||
{"TRUE", true},
|
||||
{"false", false},
|
||||
{"False", false},
|
||||
{"FALSE", false},
|
||||
{"t", true},
|
||||
{"T", true},
|
||||
{"f", false},
|
||||
{"F", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
result := c.Decode(tc.input)
|
||||
require.True(t, either.IsRight(result), "expected success for %s", tc.input)
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return !tc.expected }, func(b bool) bool { return b })
|
||||
assert.Equal(t, tc.expected, b, "input: %s", tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBoolFromString_Name(t *testing.T) {
|
||||
assert.Equal(t, "BoolFromString", BoolFromString().Name())
|
||||
}
|
||||
|
||||
func TestBoolFromString_Integration(t *testing.T) {
|
||||
t.Run("decodes and encodes multiple boolean values", func(t *testing.T) {
|
||||
c := BoolFromString()
|
||||
cases := []struct {
|
||||
str string
|
||||
val bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"false", false},
|
||||
{"1", true},
|
||||
{"0", false},
|
||||
{"T", true},
|
||||
{"F", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
result := c.Decode(tc.str)
|
||||
require.True(t, either.IsRight(result), "expected success for %s", tc.str)
|
||||
b := either.MonadFold(result, func(validation.Errors) bool { return !tc.val }, func(b bool) bool { return b })
|
||||
assert.Equal(t, tc.val, b)
|
||||
// Note: encoding always produces "true" or "false", not the original input
|
||||
if tc.val {
|
||||
assert.Equal(t, "true", c.Encode(b))
|
||||
} else {
|
||||
assert.Equal(t, "false", c.Encode(b))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MarshalJSON
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
169
v2/optics/codec/iso.go
Normal file
169
v2/optics/codec/iso.go
Normal file
@@ -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,
|
||||
)
|
||||
}
|
||||
504
v2/optics/codec/iso_test.go
Normal file
504
v2/optics/codec/iso_test.go
Normal file
@@ -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]
|
||||
)
|
||||
|
||||
@@ -259,5 +259,42 @@ type (
|
||||
// result := LetL(lens, double)(Success(21)) // Success(42)
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Lazy represents a lazily-evaluated value of type A.
|
||||
// This is an alias for lazy.Lazy[A], which defers computation until the value is needed.
|
||||
//
|
||||
// In the validation context, Lazy is used to defer expensive validation operations
|
||||
// or to break circular dependencies in validation logic.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lazyValidation := lazy.Of(func() Validation[int] {
|
||||
// // Expensive validation logic here
|
||||
// return Success(42)
|
||||
// })
|
||||
// // Validation is not executed until lazyValidation() is called
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// ErrorsProvider is an interface for types that can provide a collection of errors.
|
||||
// This interface allows validation errors to be extracted from various error types
|
||||
// in a uniform way, supporting error aggregation and reporting.
|
||||
//
|
||||
// Types implementing this interface can be unwrapped to access their underlying
|
||||
// error collection, enabling consistent error handling across different error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type MyErrors struct {
|
||||
// errs []error
|
||||
// }
|
||||
//
|
||||
// func (e *MyErrors) Errors() []error {
|
||||
// return e.errs
|
||||
// }
|
||||
//
|
||||
// // Usage
|
||||
// var provider ErrorsProvider = &MyErrors{errs: []error{...}}
|
||||
// allErrors := provider.Errors()
|
||||
ErrorsProvider interface {
|
||||
Errors() []error
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package validation
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
@@ -12,6 +13,11 @@ import (
|
||||
// Returns a generic error message indicating this is a validation error.
|
||||
// For detailed error information, use String() or Format() methods.
|
||||
|
||||
// toError converts the validation error to the error interface
|
||||
func toError(v *ValidationError) error {
|
||||
return v
|
||||
}
|
||||
|
||||
// Error implements the error interface for ValidationError.
|
||||
// Returns a generic error message.
|
||||
func (v *ValidationError) Error() string {
|
||||
@@ -34,44 +40,45 @@ func (v *ValidationError) String() string {
|
||||
// It includes the context path, message, and optionally the cause error.
|
||||
// Supports verbs: %s, %v, %+v (with additional details)
|
||||
func (v *ValidationError) Format(s fmt.State, verb rune) {
|
||||
// Build the context path
|
||||
path := ""
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path += "."
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path += entry.Key
|
||||
} else {
|
||||
path += entry.Type
|
||||
}
|
||||
}
|
||||
var result strings.Builder
|
||||
|
||||
// Start with the path if available
|
||||
result := ""
|
||||
if path != "" {
|
||||
result = fmt.Sprintf("at %s: ", path)
|
||||
// Build the context path
|
||||
if len(v.Context) > 0 {
|
||||
var path strings.Builder
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path.WriteString(".")
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path.WriteString(entry.Key)
|
||||
} else {
|
||||
path.WriteString(entry.Type)
|
||||
}
|
||||
}
|
||||
result.WriteString("at ")
|
||||
result.WriteString(path.String())
|
||||
result.WriteString(": ")
|
||||
}
|
||||
|
||||
// Add the message
|
||||
result += v.Messsage
|
||||
result.WriteString(v.Messsage)
|
||||
|
||||
// Add the cause if present
|
||||
if v.Cause != nil {
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
// Verbose format with detailed cause
|
||||
result += fmt.Sprintf("\n caused by: %+v", v.Cause)
|
||||
fmt.Fprintf(&result, "\n caused by: %+v", v.Cause)
|
||||
} else {
|
||||
result += fmt.Sprintf(" (caused by: %v)", v.Cause)
|
||||
fmt.Fprintf(&result, " (caused by: %v)", v.Cause)
|
||||
}
|
||||
}
|
||||
|
||||
// Add value information for verbose format
|
||||
if s.Flag('+') && verb == 'v' {
|
||||
result += fmt.Sprintf("\n value: %#v", v.Value)
|
||||
fmt.Fprintf(&result, "\n value: %#v", v.Value)
|
||||
}
|
||||
|
||||
fmt.Fprint(s, result)
|
||||
fmt.Fprint(s, result.String())
|
||||
}
|
||||
|
||||
// LogValue implements the slog.LogValuer interface for ValidationError.
|
||||
@@ -94,18 +101,18 @@ func (v *ValidationError) LogValue() slog.Value {
|
||||
|
||||
// Add context path if available
|
||||
if len(v.Context) > 0 {
|
||||
path := ""
|
||||
var path strings.Builder
|
||||
for i, entry := range v.Context {
|
||||
if i > 0 {
|
||||
path += "."
|
||||
path.WriteString(".")
|
||||
}
|
||||
if entry.Key != "" {
|
||||
path += entry.Key
|
||||
path.WriteString(entry.Key)
|
||||
} else {
|
||||
path += entry.Type
|
||||
path.WriteString(entry.Type)
|
||||
}
|
||||
}
|
||||
attrs = append(attrs, slog.String("path", path))
|
||||
attrs = append(attrs, slog.String("path", path.String()))
|
||||
}
|
||||
|
||||
// Add cause if present
|
||||
@@ -119,13 +126,14 @@ func (v *ValidationError) LogValue() slog.Value {
|
||||
// Error implements the error interface for ValidationErrors.
|
||||
// Returns a generic error message indicating validation errors occurred.
|
||||
func (ve *validationErrors) Error() string {
|
||||
if len(ve.errors) == 0 {
|
||||
switch len(ve.errors) {
|
||||
case 0:
|
||||
return "ValidationErrors: no errors"
|
||||
}
|
||||
if len(ve.errors) == 1 {
|
||||
case 1:
|
||||
return "ValidationErrors: 1 error"
|
||||
default:
|
||||
return fmt.Sprintf("ValidationErrors: %d errors", len(ve.errors))
|
||||
}
|
||||
return fmt.Sprintf("ValidationErrors: %d errors", len(ve.errors))
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying cause error if present.
|
||||
@@ -134,6 +142,33 @@ func (ve *validationErrors) Unwrap() error {
|
||||
return ve.cause
|
||||
}
|
||||
|
||||
// Errors implements the ErrorsProvider interface for validationErrors.
|
||||
// It converts the internal collection of ValidationError pointers to a slice of error interfaces.
|
||||
// This method enables uniform error extraction from validation error collections.
|
||||
//
|
||||
// The returned slice contains the same errors as the internal errors field,
|
||||
// but typed as error interface values for compatibility with standard Go error handling.
|
||||
//
|
||||
// Returns:
|
||||
// - A slice of error interfaces, one for each ValidationError in the collection
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ve := &validationErrors{
|
||||
// errors: Errors{
|
||||
// &ValidationError{Messsage: "invalid email"},
|
||||
// &ValidationError{Messsage: "age must be positive"},
|
||||
// },
|
||||
// }
|
||||
// errs := ve.Errors()
|
||||
// // errs is []error with 2 elements, each implementing the error interface
|
||||
// for _, err := range errs {
|
||||
// fmt.Println(err.Error()) // "ValidationError"
|
||||
// }
|
||||
func (ve *validationErrors) Errors() []error {
|
||||
return A.MonadMap(ve.errors, toError)
|
||||
}
|
||||
|
||||
// String returns a simple string representation of all validation errors.
|
||||
// Each error is listed on a separate line with its index.
|
||||
func (ve *validationErrors) String() string {
|
||||
@@ -141,16 +176,17 @@ func (ve *validationErrors) String() string {
|
||||
return "ValidationErrors: no errors"
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("ValidationErrors (%d):\n", len(ve.errors))
|
||||
var result strings.Builder
|
||||
fmt.Fprintf(&result, "ValidationErrors (%d):\n", len(ve.errors))
|
||||
for i, err := range ve.errors {
|
||||
result += fmt.Sprintf(" [%d] %s\n", i, err.String())
|
||||
fmt.Fprintf(&result, " [%d] %s\n", i, err.String())
|
||||
}
|
||||
|
||||
if ve.cause != nil {
|
||||
result += fmt.Sprintf(" caused by: %v\n", ve.cause)
|
||||
fmt.Fprintf(&result, " caused by: %v\n", ve.cause)
|
||||
}
|
||||
|
||||
return result
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for custom formatting of ValidationErrors.
|
||||
|
||||
@@ -846,3 +846,142 @@ func TestLogValuerInterface(t *testing.T) {
|
||||
var _ slog.LogValuer = (*validationErrors)(nil)
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidationErrors_Errors tests the Errors() method implementation
|
||||
func TestValidationErrors_Errors(t *testing.T) {
|
||||
t.Run("returns empty slice for no errors", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
assert.Empty(t, errs)
|
||||
assert.NotNil(t, errs)
|
||||
})
|
||||
|
||||
t.Run("converts single ValidationError to error interface", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Value: "test", Messsage: "invalid value"},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 1)
|
||||
assert.Equal(t, "ValidationError", errs[0].Error())
|
||||
})
|
||||
|
||||
t.Run("converts multiple ValidationErrors to error interfaces", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
&ValidationError{Value: "test3", Messsage: "error 3"},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 3)
|
||||
for _, err := range errs {
|
||||
assert.Equal(t, "ValidationError", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves error details in converted errors", func(t *testing.T) {
|
||||
originalErr := &ValidationError{
|
||||
Value: "abc",
|
||||
Context: []ContextEntry{{Key: "field"}},
|
||||
Messsage: "invalid format",
|
||||
Cause: errors.New("parse error"),
|
||||
}
|
||||
ve := &validationErrors{
|
||||
errors: Errors{originalErr},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 1)
|
||||
|
||||
// Verify the error can be type-asserted back to ValidationError
|
||||
validationErr, ok := errs[0].(*ValidationError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "abc", validationErr.Value)
|
||||
assert.Equal(t, "invalid format", validationErr.Messsage)
|
||||
assert.NotNil(t, validationErr.Cause)
|
||||
assert.Len(t, validationErr.Context, 1)
|
||||
})
|
||||
|
||||
t.Run("implements ErrorsProvider interface", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Messsage: "error 1"},
|
||||
&ValidationError{Messsage: "error 2"},
|
||||
},
|
||||
}
|
||||
|
||||
// Verify it implements ErrorsProvider
|
||||
var provider ErrorsProvider = ve
|
||||
errs := provider.Errors()
|
||||
assert.Len(t, errs, 2)
|
||||
})
|
||||
|
||||
t.Run("returned errors are usable with standard error handling", func(t *testing.T) {
|
||||
cause := errors.New("underlying error")
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{
|
||||
Value: "test",
|
||||
Messsage: "validation failed",
|
||||
Cause: cause,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 1)
|
||||
|
||||
// Test with errors.Is
|
||||
assert.True(t, errors.Is(errs[0], cause))
|
||||
|
||||
// Test with errors.As
|
||||
var validationErr *ValidationError
|
||||
assert.True(t, errors.As(errs[0], &validationErr))
|
||||
assert.Equal(t, "validation failed", validationErr.Messsage)
|
||||
})
|
||||
|
||||
t.Run("does not modify original errors slice", func(t *testing.T) {
|
||||
originalErrors := Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
}
|
||||
ve := &validationErrors{
|
||||
errors: originalErrors,
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 2)
|
||||
|
||||
// Original should be unchanged
|
||||
assert.Len(t, ve.errors, 2)
|
||||
assert.Equal(t, originalErrors, ve.errors)
|
||||
})
|
||||
|
||||
t.Run("each error in slice is independent", func(t *testing.T) {
|
||||
ve := &validationErrors{
|
||||
errors: Errors{
|
||||
&ValidationError{Value: "test1", Messsage: "error 1"},
|
||||
&ValidationError{Value: "test2", Messsage: "error 2"},
|
||||
},
|
||||
}
|
||||
|
||||
errs := ve.Errors()
|
||||
require.Len(t, errs, 2)
|
||||
|
||||
// Verify each error is distinct
|
||||
err1, ok1 := errs[0].(*ValidationError)
|
||||
err2, ok2 := errs[1].(*ValidationError)
|
||||
require.True(t, ok1)
|
||||
require.True(t, ok2)
|
||||
assert.NotEqual(t, err1.Messsage, err2.Messsage)
|
||||
assert.NotEqual(t, err1.Value, err2.Value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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