1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-29 10:36:04 +02:00

Compare commits

..

22 Commits

Author SHA1 Message Date
Dr. Carsten Leue
df07599a9e fix: some docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:40:45 +01:00
Dr. Carsten Leue
30ad0e4dd8 doc: add validation docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:26:53 +01:00
Dr. Carsten Leue
2374d7f1e4 fix: support unexported fields for lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:18:44 +01:00
Dr. Carsten Leue
eafc008798 fix: doc for lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:00:11 +01:00
Dr. Carsten Leue
46bf065e34 fix: migrate CLI to github.com/urfave/v3
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 12:56:23 +01:00
renovate[bot]
b4e303423b chore(deps): update actions/checkout action to v6.0.2 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 12:39:53 +01:00
renovate[bot]
7afc098f58 fix(deps): update go dependencies (major) (#144)
* fix(deps): update go dependencies

* fix: fix renovate config

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:28:56 +01:00
Dr. Carsten Leue
617e43de19 fix: improve codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:12:40 +01:00
Dr. Carsten Leue
0f7a6c0589 fix: prism doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-22 13:57:07 +01:00
Dr. Carsten Leue
e7f78e1a33 fix: more codecs and cleanup of type hints
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-21 09:23:48 +01:00
Dr. Carsten Leue
6505ab1791 fix: improve Retry implementation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-20 09:50:20 +01:00
Dr. Carsten Leue
cfa48985ec fix: WithLocal
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 18:36:02 +01:00
Dr. Carsten Leue
677523b70f fix: add nested reader transformer
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 16:34:39 +01:00
Dr. Carsten Leue
8243242cf1 doc: explain use of ReaderIOResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 10:10:43 +01:00
Dr. Carsten Leue
9021a8e274 fix: simplify tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-18 18:33:15 +01:00
Dr. Carsten Leue
f3128e887b fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-18 17:43:57 +01:00
Dr. Carsten Leue
4583694211 fix: more type magic
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-18 00:21:48 +01:00
Dr. Carsten Leue
b87c20d139 fix: better reader abstraction
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-17 22:36:26 +01:00
Dr. Carsten Leue
9fd5b90138 fix: add codec and implement some helpers along the way
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-16 23:21:10 +01:00
Dr. Carsten Leue
cdc2041d8e fix: implement ReadIO consistently
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 13:07:19 +01:00
Dr. Carsten Leue
777fff9a5a fix: implement ReadIO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 12:24:46 +01:00
Carsten Leue
8acea9043f fix: refactor circuitbreaker (#152)
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 11:36:32 +01:00
256 changed files with 44228 additions and 1705 deletions

View File

@@ -28,11 +28,11 @@ jobs:
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
@@ -66,11 +66,11 @@ jobs:
matrix:
go-version: ['1.24.x', '1.25.x']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
@@ -126,17 +126,17 @@ jobs:
steps:
# full checkout for semantic-release
- name: Full checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ env.LATEST_GO_VERSION }}
cache: true # Enable Go module caching

16
go.sum
View File

@@ -1,7 +1,3 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -10,20 +6,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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=

View File

@@ -22,7 +22,8 @@
"matchDepTypes": [
"golang"
],
"enabled": false
"enabled": false,
"description": "Disable updates to the go directive in go.mod files - the directive identifies the minimum compatible Go version and should stay as small as possible for maximum compatibility"
},
{
"matchUpdateTypes": [

1
v2/.bobignore Normal file
View File

@@ -0,0 +1 @@
reflect\reflect.go

View File

@@ -584,5 +584,7 @@ func process(input string) types.Result[types.Option[int]] {
For more information, see:
- [README.md](./README.md) - Overview and quick start
- [FUNCTIONAL_IO.md](./FUNCTIONAL_IO.md) - Functional I/O patterns with Context and Reader
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison between standard and idiomatic packages
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
- [Samples](./samples/) - Practical examples

829
v2/FUNCTIONAL_IO.md Normal file
View File

@@ -0,0 +1,829 @@
# Functional I/O in Go: Context, Errors, and the Reader Pattern
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult` and `idiomatic/context/readerresult` packages.
## Table of Contents
- [Why Context in I/O Operations](#why-context-in-io-operations)
- [The Error-Value Tuple Pattern](#the-error-value-tuple-pattern)
- [Functional Approach: Reader Pattern](#functional-approach-reader-pattern)
- [Benefits of the Functional Approach](#benefits-of-the-functional-approach)
- [Side-by-Side Comparison](#side-by-side-comparison)
- [Advanced Patterns](#advanced-patterns)
- [When to Use Each Approach](#when-to-use-each-approach)
## Why Context in I/O Operations
In idiomatic Go, I/O operations conventionally take a `context.Context` as their first parameter:
```go
func QueryDatabase(ctx context.Context, query string) (Result, error)
func MakeHTTPRequest(ctx context.Context, url string) (*http.Response, error)
func ReadFile(ctx context.Context, path string) ([]byte, error)
```
### The Purpose of Context
The `context.Context` parameter serves several critical purposes:
1. **Cancellation Propagation**: Operations can be cancelled when the context is cancelled
2. **Deadline Management**: Operations respect timeouts and deadlines
3. **Request-Scoped Values**: Carry request metadata (trace IDs, user info, etc.)
4. **Resource Cleanup**: Signal to release resources when work is no longer needed
### Why Context Matters for I/O
I/O operations are inherently **effectful** - they interact with the outside world:
- Reading from disk, network, or database
- Writing to external systems
- Generating random numbers
- Reading the current time
These operations can:
- **Take time**: Network calls may be slow
- **Fail**: Connections drop, files don't exist
- **Block**: Waiting for external resources
- **Need cancellation**: User navigates away, request times out
Context provides a standard mechanism to control these operations across your entire application.
## The Error-Value Tuple Pattern
### Why Operations Must Return Errors
In Go, I/O operations return `(value, error)` tuples because:
1. **Context can be cancelled**: Even if the operation would succeed, cancellation must be represented
2. **External systems fail**: Networks fail, files are missing, permissions are denied
3. **Resources are exhausted**: Out of memory, disk full, connection pool exhausted
4. **Timeouts occur**: Operations exceed their deadline
**There cannot be I/O operations without error handling** because the context itself introduces a failure mode (cancellation) that must be represented in the return type.
### Traditional Go Pattern
```go
func ProcessUser(ctx context.Context, userID int) (User, error) {
// Check context before starting
if err := ctx.Err(); err != nil {
return User{}, err
}
// Fetch user from database
user, err := db.QueryUser(ctx, userID)
if err != nil {
return User{}, fmt.Errorf("query user: %w", err)
}
// Validate user
if user.Age < 18 {
return User{}, errors.New("user too young")
}
// Fetch user's posts
posts, err := db.QueryPosts(ctx, user.ID)
if err != nil {
return User{}, fmt.Errorf("query posts: %w", err)
}
user.Posts = posts
return user, nil
}
```
**Characteristics:**
- Explicit error checking at each step
- Manual error wrapping and propagation
- Context checked manually
- Imperative control flow
- Error handling mixed with business logic
## Functional Approach: Reader Pattern
### The Core Insight
In functional programming, we separate **what to compute** from **how to execute it**. Instead of functions that perform I/O directly, we create functions that **return descriptions of I/O operations**.
### Key Type: ReaderIOResult
```go
// A function that takes a context and returns a value or error
type ReaderIOResult[A any] = func(context.Context) (A, error)
```
This type represents:
- **Reader**: Depends on an environment (context.Context)
- **IO**: Performs side effects (I/O operations)
- **Result**: Can fail with an error
### Why This Is Better
The functional approach **carries the I/O aspect as the return value, not on the input**:
```go
// Traditional: I/O is implicit in the function execution
func fetchUser(ctx context.Context, id int) (User, error) {
// Performs I/O immediately
}
// Functional: I/O is explicit in the return type
func fetchUser(id int) ReaderIOResult[User] {
// Returns a description of I/O, doesn't execute yet
return func(ctx context.Context) (User, error) {
// I/O happens here when the function is called
}
}
```
**Key difference**: The functional version is a **curried function** where:
1. Business parameters come first: `fetchUser(id)`
2. Context comes last: `fetchUser(id)(ctx)`
3. The intermediate result is composable: `ReaderIOResult[User]`
## Benefits of the Functional Approach
### 1. Separation of Pure and Impure Code
```go
// Pure computation - no I/O, no context needed
func validateAge(user User) (User, error) {
if user.Age < 18 {
return User{}, errors.New("user too young")
}
return user, nil
}
// Impure I/O operation - needs context
func fetchUser(id int) ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
return db.QueryUser(ctx, id)
}
}
// Compose them - pure logic lifted into ReaderIOResult
pipeline := F.Pipe2(
fetchUser(42), // ReaderIOResult[User]
readerioresult.ChainEitherK(validateAge), // Lift pure function
)
// Execute when ready
user, err := pipeline(ctx)
```
**Benefits:**
- Pure functions are easier to test (no mocking needed)
- Pure functions are easier to reason about (no side effects)
- Clear boundary between logic and I/O
- Can test business logic independently
### 2. Composability
Functions compose naturally without manual error checking:
```go
// Traditional approach - manual error handling
func ProcessUserTraditional(ctx context.Context, userID int) (UserWithPosts, error) {
user, err := fetchUser(ctx, userID)
if err != nil {
return UserWithPosts{}, err
}
validated, err := validateUser(user)
if err != nil {
return UserWithPosts{}, err
}
posts, err := fetchPosts(ctx, validated.ID)
if err != nil {
return UserWithPosts{}, err
}
return enrichUser(validated, posts), nil
}
// Functional approach - automatic error propagation
func ProcessUserFunctional(userID int) ReaderIOResult[UserWithPosts] {
return F.Pipe3(
fetchUser(userID),
readerioresult.ChainEitherK(validateUser),
readerioresult.Chain(func(user User) ReaderIOResult[UserWithPosts] {
return F.Pipe2(
fetchPosts(user.ID),
readerioresult.Map(func(posts []Post) UserWithPosts {
return enrichUser(user, posts)
}),
)
}),
)
}
```
**Benefits:**
- No manual error checking
- Automatic short-circuiting on first error
- Clear data flow
- Easier to refactor and extend
### 3. Testability
```go
// Mock I/O operations by providing test implementations
func TestProcessUser(t *testing.T) {
// Create a mock that returns test data
mockFetchUser := func(id int) ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
return User{ID: id, Age: 25}, nil
}
}
// Test with mock - no database needed
result, err := mockFetchUser(42)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 25, result.Age)
}
```
### 4. Lazy Evaluation
Operations are not executed until you provide the context:
```go
// Build the pipeline - no I/O happens yet
pipeline := F.Pipe3(
fetchUser(42),
readerioresult.Map(enrichUser),
readerioresult.Chain(saveUser),
)
// I/O only happens when we call it with a context
user, err := pipeline(ctx)
```
**Benefits:**
- Build complex operations as pure data structures
- Defer execution until needed
- Reuse pipelines with different contexts
- Test pipelines without executing I/O
### 5. Context Propagation
Context is automatically threaded through all operations:
```go
// Traditional - must pass context explicitly everywhere
func Process(ctx context.Context) error {
user, err := fetchUser(ctx, 42)
if err != nil {
return err
}
posts, err := fetchPosts(ctx, user.ID)
if err != nil {
return err
}
return savePosts(ctx, posts)
}
// Functional - context provided once at execution
func Process() ReaderIOResult[any] {
return F.Pipe2(
fetchUser(42),
readerioresult.Chain(func(user User) ReaderIOResult[any] {
return F.Pipe2(
fetchPosts(user.ID),
readerioresult.Chain(savePosts),
)
}),
)
}
// Context provided once
err := readerioresult.Fold(
func(err error) error { return err },
func(any) error { return nil },
)(Process())(ctx)
```
## Side-by-Side Comparison
### Example: User Service with Database Operations
#### Traditional Go Style
```go
package traditional
import (
"context"
"database/sql"
"fmt"
)
type User struct {
ID int
Name string
Email string
Age int
}
type UserService struct {
db *sql.DB
}
// Fetch user from database
func (s *UserService) GetUser(ctx context.Context, id int) (User, error) {
var user User
// Check context
if err := ctx.Err(); err != nil {
return User{}, err
}
// Query database
row := s.db.QueryRowContext(ctx,
"SELECT id, name, email, age FROM users WHERE id = ?", id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
if err != nil {
return User{}, fmt.Errorf("scan user: %w", err)
}
return user, nil
}
// Validate user
func (s *UserService) ValidateUser(ctx context.Context, user User) (User, error) {
if user.Age < 18 {
return User{}, fmt.Errorf("user %d is too young", user.ID)
}
if user.Email == "" {
return User{}, fmt.Errorf("user %d has no email", user.ID)
}
return user, nil
}
// Update user email
func (s *UserService) UpdateEmail(ctx context.Context, id int, email string) (User, error) {
// Check context
if err := ctx.Err(); err != nil {
return User{}, err
}
// Update database
_, err := s.db.ExecContext(ctx,
"UPDATE users SET email = ? WHERE id = ?", email, id)
if err != nil {
return User{}, fmt.Errorf("update email: %w", err)
}
// Fetch updated user
return s.GetUser(ctx, id)
}
// Process user: fetch, validate, update email
func (s *UserService) ProcessUser(ctx context.Context, id int, newEmail string) (User, error) {
// Fetch user
user, err := s.GetUser(ctx, id)
if err != nil {
return User{}, fmt.Errorf("get user: %w", err)
}
// Validate user
validated, err := s.ValidateUser(ctx, user)
if err != nil {
return User{}, fmt.Errorf("validate user: %w", err)
}
// Update email
updated, err := s.UpdateEmail(ctx, validated.ID, newEmail)
if err != nil {
return User{}, fmt.Errorf("update email: %w", err)
}
return updated, nil
}
```
**Characteristics:**
- ✗ Manual error checking at every step
- ✗ Context passed explicitly to every function
- ✗ Error wrapping is manual and verbose
- ✗ Business logic mixed with error handling
- ✗ Hard to test without database
- ✗ Difficult to compose operations
- ✓ Familiar to Go developers
- ✓ Explicit control flow
#### Functional Go Style (context/readerioresult)
```go
package functional
import (
"context"
"database/sql"
"fmt"
F "github.com/IBM/fp-go/v2/function"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
)
type User struct {
ID int
Name string
Email string
Age int
}
type UserService struct {
db *sql.DB
}
// Fetch user from database - returns a ReaderIOResult
func (s *UserService) GetUser(id int) RIO.ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
var user User
row := s.db.QueryRowContext(ctx,
"SELECT id, name, email, age FROM users WHERE id = ?", id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
if err != nil {
return User{}, fmt.Errorf("scan user: %w", err)
}
return user, nil
}
}
// Validate user - pure function (no I/O, no context)
func ValidateUser(user User) (User, error) {
if user.Age < 18 {
return User{}, fmt.Errorf("user %d is too young", user.ID)
}
if user.Email == "" {
return User{}, fmt.Errorf("user %d has no email", user.ID)
}
return user, nil
}
// Update user email - returns a ReaderIOResult
func (s *UserService) UpdateEmail(id int, email string) RIO.ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
_, err := s.db.ExecContext(ctx,
"UPDATE users SET email = ? WHERE id = ?", email, id)
if err != nil {
return User{}, fmt.Errorf("update email: %w", err)
}
// Chain to GetUser
return s.GetUser(id)(ctx)
}
}
// Process user: fetch, validate, update email - composable pipeline
func (s *UserService) ProcessUser(id int, newEmail string) RIO.ReaderIOResult[User] {
return F.Pipe3(
s.GetUser(id), // Fetch user
RIO.ChainEitherK(ValidateUser), // Validate (pure function)
RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
return s.UpdateEmail(user.ID, newEmail) // Update email
}),
)
}
// Alternative: Using Do-notation for more complex flows
func (s *UserService) ProcessUserDo(id int, newEmail string) RIO.ReaderIOResult[User] {
return RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
// Validate is pure, lift it into ReaderIOResult
validated, err := ValidateUser(user)
if err != nil {
return RIO.Left[User](err)
}
// Update with validated user
return s.UpdateEmail(validated.ID, newEmail)
})(s.GetUser(id))
}
```
**Characteristics:**
- ✓ Automatic error propagation (no manual checking)
- ✓ Context threaded automatically
- ✓ Pure functions separated from I/O
- ✓ Business logic clear and composable
- ✓ Easy to test (mock ReaderIOResult)
- ✓ Operations compose naturally
- ✓ Lazy evaluation (build pipeline, execute later)
- ✗ Requires understanding of functional patterns
- ✗ Less familiar to traditional Go developers
#### Idiomatic Functional Style (idiomatic/context/readerresult)
For even better performance with the same functional benefits:
```go
package idiomatic
import (
"context"
"database/sql"
"fmt"
F "github.com/IBM/fp-go/v2/function"
RR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
)
type User struct {
ID int
Name string
Email string
Age int
}
type UserService struct {
db *sql.DB
}
// ReaderResult is just: func(context.Context) (A, error)
// Same as ReaderIOResult but using native Go tuples
func (s *UserService) GetUser(id int) RR.ReaderResult[User] {
return func(ctx context.Context) (User, error) {
var user User
row := s.db.QueryRowContext(ctx,
"SELECT id, name, email, age FROM users WHERE id = ?", id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
return user, err // Native tuple return
}
}
// Pure validation - returns native (User, error) tuple
func ValidateUser(user User) (User, error) {
if user.Age < 18 {
return User{}, fmt.Errorf("user %d is too young", user.ID)
}
if user.Email == "" {
return User{}, fmt.Errorf("user %d has no email", user.ID)
}
return user, nil
}
func (s *UserService) UpdateEmail(id int, email string) RR.ReaderResult[User] {
return func(ctx context.Context) (User, error) {
_, err := s.db.ExecContext(ctx,
"UPDATE users SET email = ? WHERE id = ?", email, id)
if err != nil {
return User{}, err
}
return s.GetUser(id)(ctx)
}
}
// Composable pipeline with native tuples
func (s *UserService) ProcessUser(id int, newEmail string) RR.ReaderResult[User] {
return F.Pipe3(
s.GetUser(id),
RR.ChainEitherK(ValidateUser), // Lift pure function
RR.Chain(func(user User) RR.ReaderResult[User] {
return s.UpdateEmail(user.ID, newEmail)
}),
)
}
```
**Characteristics:**
- ✓ All benefits of functional approach
-**2-10x better performance** (native tuples)
-**Zero allocations** for many operations
- ✓ More familiar to Go developers (uses (value, error))
- ✓ Seamless integration with existing Go code
- ✓ Same composability as ReaderIOResult
### Usage Comparison
```go
// Traditional
func HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
service := &UserService{db: db}
user, err := service.ProcessUser(ctx, 42, "new@email.com")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
// Functional (both styles)
func HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
service := &UserService{db: db}
// Build the pipeline (no execution yet)
pipeline := service.ProcessUser(42, "new@email.com")
// Execute with context
user, err := pipeline(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
// Or using Fold for cleaner error handling
func HandleRequestFold(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
service := &UserService{db: db}
RR.Fold(
func(err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
},
func(user User) {
json.NewEncoder(w).Encode(user)
},
)(service.ProcessUser(42, "new@email.com"))(ctx)
}
```
## Advanced Patterns
### Resource Management with Bracket
```go
// Traditional
func ProcessFile(ctx context.Context, path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return "", err
}
return string(data), nil
}
// Functional - guaranteed cleanup even on panic
func ProcessFile(path string) RIO.ReaderIOResult[string] {
return RIO.Bracket(
// Acquire resource
func(ctx context.Context) (*os.File, error) {
return os.Open(path)
},
// Release resource (always called)
func(file *os.File, err error) RIO.ReaderIOResult[any] {
return func(ctx context.Context) (any, error) {
return nil, file.Close()
}
},
// Use resource
func(file *os.File) RIO.ReaderIOResult[string] {
return func(ctx context.Context) (string, error) {
data, err := io.ReadAll(file)
return string(data), err
}
},
)
}
```
### Parallel Execution
```go
// Traditional - manual goroutines and sync
func FetchMultipleUsers(ctx context.Context, ids []int) ([]User, error) {
var wg sync.WaitGroup
users := make([]User, len(ids))
errs := make([]error, len(ids))
for i, id := range ids {
wg.Add(1)
go func(i, id int) {
defer wg.Done()
users[i], errs[i] = fetchUser(ctx, id)
}(i, id)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return nil, err
}
}
return users, nil
}
// Functional - automatic parallelization
func FetchMultipleUsers(ids []int) RIO.ReaderIOResult[[]User] {
operations := A.Map(func(id int) RIO.ReaderIOResult[User] {
return fetchUser(id)
})(ids)
return RIO.TraverseArrayPar(F.Identity[RIO.ReaderIOResult[User]])(operations)
}
```
### Retry Logic
```go
// Traditional
func FetchWithRetry(ctx context.Context, url string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
if ctx.Err() != nil {
return nil, ctx.Err()
}
resp, err := http.Get(url)
if err == nil {
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
lastErr = err
time.Sleep(time.Second * time.Duration(i+1))
}
return nil, lastErr
}
// Functional
func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
operation := func(ctx context.Context) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
return RIO.Retry(
maxRetries,
func(attempt int) time.Duration {
return time.Second * time.Duration(attempt)
},
)(operation)
}
```
## When to Use Each Approach
### Use Traditional Go Style When:
1. **Team familiarity**: Team is not familiar with functional programming
2. **Simple operations**: Single I/O operation with straightforward error handling
3. **Existing codebase**: Large codebase already using traditional patterns
4. **Learning curve**: Want to minimize onboarding time
5. **Explicit control**: Need very explicit control flow
### Use Functional Style (ReaderIOResult) When:
1. **Complex pipelines**: Multiple I/O operations that need composition
2. **Testability**: Need to test business logic separately from I/O
3. **Reusability**: Want to build reusable operation pipelines
4. **Error handling**: Want automatic error propagation
5. **Resource management**: Need guaranteed cleanup (Bracket)
6. **Parallel execution**: Need to parallelize operations easily
7. **Type safety**: Want the type system to track I/O effects
### Use Idiomatic Functional Style (idiomatic/context/readerresult) When:
1. **All functional benefits**: Want functional patterns with Go idioms
2. **Performance critical**: Need 2-10x better performance
3. **Zero allocations**: Memory efficiency is important
4. **Go integration**: Want seamless integration with existing Go code
5. **Production services**: Building high-throughput services
6. **Best of both worlds**: Want functional composition with Go's native patterns
## Summary
The functional approach to I/O in Go offers significant advantages:
1. **Separation of Concerns**: Pure logic separated from I/O effects
2. **Composability**: Operations compose naturally without manual error checking
3. **Testability**: Easy to test without mocking I/O
4. **Type Safety**: I/O effects visible in the type system
5. **Lazy Evaluation**: Build pipelines, execute when ready
6. **Context Propagation**: Automatic threading of context
7. **Performance**: Idiomatic version offers 2-10x speedup
The key insight is that **I/O operations return descriptions of effects** (ReaderIOResult) rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
For production Go services, the **idiomatic/context/readerresult** package provides the best balance: full functional programming capabilities with native Go performance and familiar error handling patterns.
## Further Reading
- [DESIGN.md](./DESIGN.md) - Design principles and patterns
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison
- [idiomatic/doc.go](./idiomatic/doc.go) - Idiomatic package overview
- [context/readerioresult](./context/readerioresult/) - ReaderIOResult package
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package

View File

@@ -447,6 +447,8 @@ func process() IOResult[string] {
## 📚 Documentation
- **[Design Decisions](./DESIGN.md)** - Key design principles and patterns explained
- **[Functional I/O in Go](./FUNCTIONAL_IO.md)** - Understanding Context, errors, and the Reader pattern for I/O operations
- **[Idiomatic vs Standard Packages](./IDIOMATIC_COMPARISON.md)** - Performance comparison and when to use each approach
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
- **[Code Samples](./samples/)** - Practical examples and use cases
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
@@ -463,7 +465,7 @@ func process() IOResult[string] {
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
#### Idiomatic Packages (Tuple-based, High Performance)
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples

View File

@@ -190,6 +190,11 @@ func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
return G.MonadReduce(fa, f, initial)
}
//go:inline
func MonadReduceWithIndex[A, B any](fa []A, f func(int, B, A) B, initial B) B {
return G.MonadReduceWithIndex(fa, f, initial)
}
// Reduce folds an array from left to right, applying a function to accumulate a result.
//
// Example:
@@ -514,6 +519,83 @@ func Push[A any](a A) Operator[A, A] {
return G.Push[Operator[A, A]](a)
}
// Concat concatenates two arrays, appending the provided array to the end of the input array.
// This is a curried function that takes an array to append and returns a function that
// takes the base array and returns the concatenated result.
//
// The function creates a new array containing all elements from the base array followed
// by all elements from the appended array. Neither input array is modified.
//
// Type Parameters:
// - A: The type of elements in the arrays
//
// Parameters:
// - as: The array to append to the end of the base array
//
// Returns:
// - A function that takes a base array and returns a new array with `as` appended to its end
//
// Behavior:
// - Creates a new array with length equal to the sum of both input arrays
// - Copies all elements from the base array first
// - Appends all elements from the `as` array at the end
// - Returns the base array unchanged if `as` is empty
// - Returns `as` unchanged if the base array is empty
// - Does not modify either input array
//
// Example:
//
// base := []int{1, 2, 3}
// toAppend := []int{4, 5, 6}
// result := array.Concat(toAppend)(base)
// // result: []int{1, 2, 3, 4, 5, 6}
// // base: []int{1, 2, 3} (unchanged)
// // toAppend: []int{4, 5, 6} (unchanged)
//
// Example with empty arrays:
//
// base := []int{1, 2, 3}
// empty := []int{}
// result := array.Concat(empty)(base)
// // result: []int{1, 2, 3}
//
// Example with strings:
//
// words1 := []string{"hello", "world"}
// words2 := []string{"foo", "bar"}
// result := array.Concat(words2)(words1)
// // result: []string{"hello", "world", "foo", "bar"}
//
// Example with functional composition:
//
// numbers := []int{1, 2, 3}
// result := F.Pipe2(
// numbers,
// array.Map(N.Mul(2)),
// array.Concat([]int{10, 20}),
// )
// // result: []int{2, 4, 6, 10, 20}
//
// Use cases:
// - Combining multiple arrays into one
// - Building arrays incrementally
// - Implementing array-based data structures (queues, buffers)
// - Merging results from multiple operations
// - Creating array pipelines with functional composition
//
// Performance:
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
// - Space complexity: O(n + m) for the new array
// - Optimized to avoid allocation when one array is empty
//
// Note: This function is immutable - it creates a new array rather than modifying
// the input arrays. For appending a single element, consider using Append or Push.
//
//go:inline
func Concat[A any](as []A) Operator[A, A] {
return F.Bind2nd(array.Concat[[]A, A], as)
}
// MonadFlap applies a value to an array of functions, producing an array of results.
// This is the monadic version that takes both parameters.
//

View File

@@ -764,3 +764,341 @@ func TestExtendUseCases(t *testing.T) {
assert.Equal(t, expected, result)
})
}
// TestConcat tests the Concat function
func TestConcat(t *testing.T) {
t.Run("Concat two non-empty arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
result := Concat(toAppend)(base)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result)
})
t.Run("Concat with empty array to append", func(t *testing.T) {
base := []int{1, 2, 3}
empty := []int{}
result := Concat(empty)(base)
assert.Equal(t, base, result)
})
t.Run("Concat to empty base array", func(t *testing.T) {
empty := []int{}
toAppend := []int{1, 2, 3}
result := Concat(toAppend)(empty)
assert.Equal(t, toAppend, result)
})
t.Run("Concat two empty arrays", func(t *testing.T) {
empty1 := []int{}
empty2 := []int{}
result := Concat(empty2)(empty1)
assert.Equal(t, []int{}, result)
})
t.Run("Concat strings", func(t *testing.T) {
words1 := []string{"hello", "world"}
words2 := []string{"foo", "bar"}
result := Concat(words2)(words1)
expected := []string{"hello", "world", "foo", "bar"}
assert.Equal(t, expected, result)
})
t.Run("Concat single element arrays", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
result := Concat(arr2)(arr1)
expected := []int{1, 2}
assert.Equal(t, expected, result)
})
t.Run("Does not modify original arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
baseCopy := []int{1, 2, 3}
toAppendCopy := []int{4, 5, 6}
_ = Concat(toAppend)(base)
assert.Equal(t, baseCopy, base)
assert.Equal(t, toAppendCopy, toAppend)
})
t.Run("Concat with floats", func(t *testing.T) {
arr1 := []float64{1.1, 2.2}
arr2 := []float64{3.3, 4.4}
result := Concat(arr2)(arr1)
expected := []float64{1.1, 2.2, 3.3, 4.4}
assert.Equal(t, expected, result)
})
t.Run("Concat with structs", func(t *testing.T) {
type Person struct {
Name string
Age int
}
arr1 := []Person{{"Alice", 30}, {"Bob", 25}}
arr2 := []Person{{"Charlie", 35}}
result := Concat(arr2)(arr1)
expected := []Person{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}
assert.Equal(t, expected, result)
})
t.Run("Concat large arrays", func(t *testing.T) {
arr1 := MakeBy(500, F.Identity[int])
arr2 := MakeBy(500, func(i int) int { return i + 500 })
result := Concat(arr2)(arr1)
assert.Equal(t, 1000, len(result))
assert.Equal(t, 0, result[0])
assert.Equal(t, 499, result[499])
assert.Equal(t, 500, result[500])
assert.Equal(t, 999, result[999])
})
t.Run("Concat multiple times", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
arr3 := []int{3}
result := F.Pipe2(
arr1,
Concat(arr2),
Concat(arr3),
)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
}
// TestConcatComposition tests Concat with other array operations
func TestConcatComposition(t *testing.T) {
t.Run("Concat after Map", func(t *testing.T) {
numbers := []int{1, 2, 3}
result := F.Pipe2(
numbers,
Map(N.Mul(2)),
Concat([]int{10, 20}),
)
expected := []int{2, 4, 6, 10, 20}
assert.Equal(t, expected, result)
})
t.Run("Map after Concat", func(t *testing.T) {
arr1 := []int{1, 2}
arr2 := []int{3, 4}
result := F.Pipe2(
arr1,
Concat(arr2),
Map(N.Mul(2)),
)
expected := []int{2, 4, 6, 8}
assert.Equal(t, expected, result)
})
t.Run("Concat with Filter", func(t *testing.T) {
arr1 := []int{1, 2, 3, 4}
arr2 := []int{5, 6, 7, 8}
result := F.Pipe2(
arr1,
Concat(arr2),
Filter(func(n int) bool { return n%2 == 0 }),
)
expected := []int{2, 4, 6, 8}
assert.Equal(t, expected, result)
})
t.Run("Concat with Reduce", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := F.Pipe2(
arr1,
Concat(arr2),
Reduce(func(acc, x int) int { return acc + x }, 0),
)
expected := 21 // 1+2+3+4+5+6
assert.Equal(t, expected, result)
})
t.Run("Concat with Reverse", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := F.Pipe2(
arr1,
Concat(arr2),
Reverse[int],
)
expected := []int{6, 5, 4, 3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Concat with Flatten", func(t *testing.T) {
arr1 := [][]int{{1, 2}, {3, 4}}
arr2 := [][]int{{5, 6}}
result := F.Pipe2(
arr1,
Concat(arr2),
Flatten[int],
)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result)
})
t.Run("Multiple Concat operations", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
arr3 := []int{3}
arr4 := []int{4}
result := Concat(arr4)(Concat(arr3)(Concat(arr2)(arr1)))
expected := []int{1, 2, 3, 4}
assert.Equal(t, expected, result)
})
}
// TestConcatUseCases demonstrates practical use cases for Concat
func TestConcatUseCases(t *testing.T) {
t.Run("Building array incrementally", func(t *testing.T) {
header := []string{"Name", "Age"}
data := []string{"Alice", "30"}
footer := []string{"Total: 1"}
result := F.Pipe2(
header,
Concat(data),
Concat(footer),
)
expected := []string{"Name", "Age", "Alice", "30", "Total: 1"}
assert.Equal(t, expected, result)
})
t.Run("Merging results from multiple operations", func(t *testing.T) {
evens := Filter(func(n int) bool { return n%2 == 0 })([]int{1, 2, 3, 4, 5, 6})
odds := Filter(func(n int) bool { return n%2 != 0 })([]int{1, 2, 3, 4, 5, 6})
result := Concat(odds)(evens)
expected := []int{2, 4, 6, 1, 3, 5}
assert.Equal(t, expected, result)
})
t.Run("Combining prefix and suffix", func(t *testing.T) {
prefix := []string{"Mr.", "Dr."}
names := []string{"Smith", "Jones"}
addPrefix := func(name string) []string {
return Map(func(p string) string { return p + " " + name })(prefix)
}
result := F.Pipe2(
names,
Chain(addPrefix),
F.Identity[[]string],
)
expected := []string{"Mr. Smith", "Dr. Smith", "Mr. Jones", "Dr. Jones"}
assert.Equal(t, expected, result)
})
t.Run("Queue-like behavior", func(t *testing.T) {
queue := []int{1, 2, 3}
newItems := []int{4, 5}
// Add items to end of queue
updatedQueue := Concat(newItems)(queue)
assert.Equal(t, []int{1, 2, 3, 4, 5}, updatedQueue)
assert.Equal(t, 1, updatedQueue[0]) // Front of queue
assert.Equal(t, 5, updatedQueue[len(updatedQueue)-1]) // Back of queue
})
t.Run("Combining configuration arrays", func(t *testing.T) {
defaultConfig := []string{"--verbose", "--color"}
userConfig := []string{"--output=file.txt", "--format=json"}
finalConfig := Concat(userConfig)(defaultConfig)
expected := []string{"--verbose", "--color", "--output=file.txt", "--format=json"}
assert.Equal(t, expected, finalConfig)
})
}
// TestConcatProperties tests mathematical properties of Concat
func TestConcatProperties(t *testing.T) {
t.Run("Associativity: (a + b) + c == a + (b + c)", func(t *testing.T) {
a := []int{1, 2}
b := []int{3, 4}
c := []int{5, 6}
// (a + b) + c
left := Concat(c)(Concat(b)(a))
// a + (b + c)
right := Concat(Concat(c)(b))(a)
assert.Equal(t, left, right)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, left)
})
t.Run("Identity: a + [] == a and [] + a == a", func(t *testing.T) {
arr := []int{1, 2, 3}
empty := []int{}
// Right identity
rightResult := Concat(empty)(arr)
assert.Equal(t, arr, rightResult)
// Left identity
leftResult := Concat(arr)(empty)
assert.Equal(t, arr, leftResult)
})
t.Run("Length property: len(a + b) == len(a) + len(b)", func(t *testing.T) {
testCases := []struct {
arr1 []int
arr2 []int
}{
{[]int{1, 2, 3}, []int{4, 5}},
{[]int{1}, []int{2, 3, 4, 5}},
{[]int{}, []int{1, 2, 3}},
{[]int{1, 2, 3}, []int{}},
{MakeBy(100, F.Identity[int]), MakeBy(50, F.Identity[int])},
}
for _, tc := range testCases {
result := Concat(tc.arr2)(tc.arr1)
expectedLen := len(tc.arr1) + len(tc.arr2)
assert.Equal(t, expectedLen, len(result))
}
})
t.Run("Order preservation: elements maintain their relative order", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := Concat(arr2)(arr1)
// Check arr1 elements are in order
assert.Equal(t, 1, result[0])
assert.Equal(t, 2, result[1])
assert.Equal(t, 3, result[2])
// Check arr2 elements are in order after arr1
assert.Equal(t, 4, result[3])
assert.Equal(t, 5, result[4])
assert.Equal(t, 6, result[5])
})
t.Run("Immutability: original arrays are not modified", func(t *testing.T) {
original1 := []int{1, 2, 3}
original2 := []int{4, 5, 6}
copy1 := []int{1, 2, 3}
copy2 := []int{4, 5, 6}
_ = Concat(original2)(original1)
assert.Equal(t, copy1, original1)
assert.Equal(t, copy2, original2)
})
}

View File

@@ -764,14 +764,14 @@ func TestFoldMap(t *testing.T) {
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := From(1, 2, 3, 4)
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
result := FoldMap[int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
})
t.Run("FoldMap with string concatenation", func(t *testing.T) {
concatSemigroup := STR.Semigroup
arr := From(1, 2, 3)
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
result := FoldMap[int](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
assert.Equal(t, "123", result)
})
}

View File

@@ -16,7 +16,11 @@
package array
import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
"github.com/IBM/fp-go/v2/internal/traversable"
)
// Traverse maps each element of an array to an effect (HKT), then collects the results
@@ -55,9 +59,9 @@ import (
//
//go:inline
func Traverse[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap functor.MapType[[]B, func(B) []B, HKTRB, HKTAB],
fap apply.ApType[HKTB, HKTRB, HKTAB],
f func(A) HKTB) func([]A) HKTRB {
return array.Traverse[[]A](fof, fmap, fap, f)
@@ -71,7 +75,7 @@ func Traverse[A, B, HKTB, HKTAB, HKTRB any](
//
//go:inline
func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
@@ -83,7 +87,7 @@ func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
//go:inline
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
@@ -93,7 +97,7 @@ func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
//go:inline
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
@@ -102,3 +106,22 @@ func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
}
func MakeTraverseType[A, B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B any]() traversable.TraverseType[A, B, []A, []B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B] {
return func(
// ap
fof_b pointed.OfType[[]B, HKT_F_T_B],
fmap_b functor.MapType[[]B, func(B) []B, HKT_F_T_B, HKT_F_B_T_B],
fap_b apply.ApType[HKT_F_B, HKT_F_T_B, HKT_F_B_T_B],
) func(func(A) HKT_F_B) func([]A) HKT_F_T_B {
return func(f func(A) HKT_F_B) func([]A) HKT_F_T_B {
return Traverse(
fof_b,
fmap_b,
fap_b,
f,
)
}
}
}

View File

@@ -1,7 +1,81 @@
// Copyright (c) 2024 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 builder provides a generic Builder pattern interface for constructing
// complex objects with validation.
//
// The Builder pattern is useful when:
// - Object construction requires multiple steps
// - Construction may fail with validation errors
// - You want to separate construction logic from the object itself
//
// Example usage:
//
// type PersonBuilder struct {
// name string
// age int
// }
//
// func (b PersonBuilder) Build() result.Result[Person] {
// if b.name == "" {
// return result.Error[Person](errors.New("name is required"))
// }
// if b.age < 0 {
// return result.Error[Person](errors.New("age must be non-negative"))
// }
// return result.Of(Person{Name: b.name, Age: b.age})
// }
package builder
type (
// Builder is a generic interface for the Builder pattern that constructs
// objects of type T with validation.
//
// The Build method returns a Result[T] which can be either:
// - Success: containing the constructed object of type T
// - Error: containing an error if validation or construction fails
//
// This allows builders to perform validation and return meaningful errors
// during the construction process, making it explicit that object creation
// may fail.
//
// Type Parameters:
// - T: The type of object being built
//
// Example:
//
// type ConfigBuilder struct {
// host string
// port int
// }
//
// func (b ConfigBuilder) Build() result.Result[Config] {
// if b.host == "" {
// return result.Error[Config](errors.New("host is required"))
// }
// if b.port <= 0 || b.port > 65535 {
// return result.Error[Config](errors.New("invalid port"))
// }
// return result.Of(Config{Host: b.host, Port: b.port})
// }
Builder[T any] interface {
// Build constructs and validates an object of type T.
//
// Returns:
// - Result[T]: A Result containing either the successfully built object
// or an error if validation or construction fails.
Build() Result[T]
}
)

374
v2/builder/builder_test.go Normal file
View File

@@ -0,0 +1,374 @@
// Copyright (c) 2024 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 builder
import (
"errors"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// Test types for demonstration
type Person struct {
Name string
Age int
}
type PersonBuilder struct {
name string
age int
}
func (b PersonBuilder) WithName(name string) PersonBuilder {
b.name = name
return b
}
func (b PersonBuilder) WithAge(age int) PersonBuilder {
b.age = age
return b
}
func (b PersonBuilder) Build() Result[Person] {
if b.name == "" {
return result.Left[Person](errors.New("name is required"))
}
if b.age < 0 {
return result.Left[Person](errors.New("age must be non-negative"))
}
if b.age > 150 {
return result.Left[Person](errors.New("age must be realistic"))
}
return result.Of(Person{Name: b.name, Age: b.age})
}
func NewPersonBuilder(p Person) PersonBuilder {
return PersonBuilder{name: p.Name, age: p.Age}
}
// Config example for additional test coverage
type Config struct {
Host string
Port int
}
type ConfigBuilder struct {
host string
port int
}
func (b ConfigBuilder) WithHost(host string) ConfigBuilder {
b.host = host
return b
}
func (b ConfigBuilder) WithPort(port int) ConfigBuilder {
b.port = port
return b
}
func (b ConfigBuilder) Build() Result[Config] {
if b.host == "" {
return result.Left[Config](errors.New("host is required"))
}
if b.port <= 0 || b.port > 65535 {
return result.Left[Config](errors.New("port must be between 1 and 65535"))
}
return result.Of(Config{Host: b.host, Port: b.port})
}
func NewConfigBuilder(c Config) ConfigBuilder {
return ConfigBuilder{host: c.Host, port: c.Port}
}
// Tests for Builder interface
func TestBuilder_SuccessfulBuild(t *testing.T) {
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
res := builder.Build()
assert.True(t, result.IsRight(res), "Build should succeed")
person := result.ToOption(res)
assert.True(t, O.IsSome(person), "Result should contain a person")
p := O.GetOrElse(func() Person { return Person{} })(person)
assert.Equal(t, "Alice", p.Name)
assert.Equal(t, 30, p.Age)
}
func TestBuilder_ValidationFailure_MissingName(t *testing.T) {
builder := PersonBuilder{}.WithAge(30)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when name is missing")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "name is required", err.Error())
}
func TestBuilder_ValidationFailure_NegativeAge(t *testing.T) {
builder := PersonBuilder{}.
WithName("Bob").
WithAge(-5)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when age is negative")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "age must be non-negative", err.Error())
}
func TestBuilder_ValidationFailure_UnrealisticAge(t *testing.T) {
builder := PersonBuilder{}.
WithName("Charlie").
WithAge(200)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when age is unrealistic")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "age must be realistic", err.Error())
}
func TestBuilder_ConfigSuccessfulBuild(t *testing.T) {
builder := ConfigBuilder{}.
WithHost("localhost").
WithPort(8080)
res := builder.Build()
assert.True(t, result.IsRight(res), "Build should succeed")
config := result.ToOption(res)
assert.True(t, O.IsSome(config), "Result should contain a config")
c := O.GetOrElse(func() Config { return Config{} })(config)
assert.Equal(t, "localhost", c.Host)
assert.Equal(t, 8080, c.Port)
}
func TestBuilder_ConfigValidationFailure_MissingHost(t *testing.T) {
builder := ConfigBuilder{}.WithPort(8080)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when host is missing")
err := result.Fold(
func(e error) error { return e },
func(Config) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "host is required", err.Error())
}
func TestBuilder_ConfigValidationFailure_InvalidPort(t *testing.T) {
tests := []struct {
name string
port int
}{
{"zero port", 0},
{"negative port", -1},
{"port too large", 70000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := ConfigBuilder{}.
WithHost("localhost").
WithPort(tt.port)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail for invalid port")
err := result.Fold(
func(e error) error { return e },
func(Config) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "port must be between 1 and 65535", err.Error())
})
}
}
// Tests for BuilderPrism
func TestBuilderPrism_GetOption_ValidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
personOpt := prism.GetOption(builder)
assert.True(t, O.IsSome(personOpt), "GetOption should return Some for valid builder")
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
}
func TestBuilderPrism_GetOption_InvalidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.WithAge(30) // Missing name
personOpt := prism.GetOption(builder)
assert.True(t, O.IsNone(personOpt), "GetOption should return None for invalid builder")
}
func TestBuilderPrism_ReverseGet(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
person := Person{Name: "Bob", Age: 25}
builder := prism.ReverseGet(person)
assert.Equal(t, "Bob", builder.name)
assert.Equal(t, 25, builder.age)
// Verify the builder can build the same person
res := builder.Build()
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
rebuilt := O.GetOrElse(func() Person { return Person{} })(result.ToOption(res))
assert.Equal(t, person, rebuilt)
}
func TestBuilderPrism_RoundTrip_ValidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
originalBuilder := PersonBuilder{}.
WithName("Charlie").
WithAge(35)
// Extract person from builder
personOpt := prism.GetOption(originalBuilder)
assert.True(t, O.IsSome(personOpt), "Should extract person from valid builder")
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
// Reconstruct builder from person
rebuiltBuilder := prism.ReverseGet(person)
// Verify the rebuilt builder produces the same person
rebuiltRes := rebuiltBuilder.Build()
assert.True(t, result.IsRight(rebuiltRes), "Rebuilt builder should be valid")
rebuiltPerson := O.GetOrElse(func() Person { return Person{} })(result.ToOption(rebuiltRes))
assert.Equal(t, person, rebuiltPerson)
}
func TestBuilderPrism_ConfigPrism(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
builder := ConfigBuilder{}.
WithHost("example.com").
WithPort(443)
configOpt := prism.GetOption(builder)
assert.True(t, O.IsSome(configOpt), "GetOption should return Some for valid config builder")
config := O.GetOrElse(func() Config { return Config{} })(configOpt)
assert.Equal(t, "example.com", config.Host)
assert.Equal(t, 443, config.Port)
}
func TestBuilderPrism_ConfigPrism_InvalidBuilder(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
builder := ConfigBuilder{}.WithPort(8080) // Missing host
configOpt := prism.GetOption(builder)
assert.True(t, O.IsNone(configOpt), "GetOption should return None for invalid config builder")
}
func TestBuilderPrism_ConfigPrism_ReverseGet(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
config := Config{Host: "api.example.com", Port: 9000}
builder := prism.ReverseGet(config)
assert.Equal(t, "api.example.com", builder.host)
assert.Equal(t, 9000, builder.port)
// Verify the builder can build the same config
res := builder.Build()
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
rebuilt := O.GetOrElse(func() Config { return Config{} })(result.ToOption(res))
assert.Equal(t, config, rebuilt)
}
// Benchmark tests
func BenchmarkBuilder_SuccessfulBuild(b *testing.B) {
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.Build()
}
}
func BenchmarkBuilder_FailedBuild(b *testing.B) {
builder := PersonBuilder{}.WithAge(30) // Missing name
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.Build()
}
}
func BenchmarkBuilderPrism_GetOption(b *testing.B) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prism.GetOption(builder)
}
}
func BenchmarkBuilderPrism_ReverseGet(b *testing.B) {
prism := BuilderPrism(NewPersonBuilder)
person := Person{Name: "Bob", Age: 25}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prism.ReverseGet(person)
}
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2024 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 builder
import (
@@ -6,7 +21,61 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// BuilderPrism createa a [Prism] that converts between a builder and its type
// BuilderPrism creates a [Prism] that converts between a builder and its built type.
//
// A Prism is an optic that focuses on a case of a sum type, providing bidirectional
// conversion with the possibility of failure. This function creates a prism that:
// - Extracts: Attempts to build the object from the builder (may fail)
// - Constructs: Creates a builder from a valid object (always succeeds)
//
// The extraction direction (builder -> object) uses the Build method and converts
// the Result to an Option, where errors become None. The construction direction
// (object -> builder) uses the provided creator function.
//
// Type Parameters:
// - T: The type of the object being built
// - B: The builder type that implements Builder[T]
//
// Parameters:
// - creator: A function that creates a builder from a valid object of type T.
// This function should initialize the builder with all fields from the object.
//
// Returns:
// - Prism[B, T]: A prism that can convert between the builder and the built type.
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// type PersonBuilder struct {
// name string
// age int
// }
//
// func (b PersonBuilder) Build() result.Result[Person] {
// if b.name == "" {
// return result.Error[Person](errors.New("name required"))
// }
// return result.Of(Person{Name: b.name, Age: b.age})
// }
//
// func NewPersonBuilder(p Person) PersonBuilder {
// return PersonBuilder{name: p.Name, age: p.Age}
// }
//
// // Create a prism for PersonBuilder
// prism := BuilderPrism(NewPersonBuilder)
//
// // Use the prism to extract a Person from a valid builder
// builder := PersonBuilder{name: "Alice", age: 30}
// person := prism.GetOption(builder) // Some(Person{Name: "Alice", Age: 30})
//
// // Use the prism to create a builder from a Person
// p := Person{Name: "Bob", Age: 25}
// b := prism.ReverseGet(p) // PersonBuilder{name: "Bob", age: 25}
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
}

View File

@@ -4,7 +4,6 @@ import (
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
"github.com/IBM/fp-go/v2/io"
@@ -14,6 +13,7 @@ import (
"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/readerio"
"github.com/IBM/fp-go/v2/retry"
)
@@ -241,125 +241,155 @@ func isResetTimeExceeded(ct time.Time) option.Kleisli[openState, openState] {
})
}
// handleSuccessOnClosed handles a successful request when the circuit breaker is in closed state.
// It updates the closed state by recording the success and returns an IO operation that
// modifies the breaker state.
// handleSuccessOnClosed creates a Reader that handles successful requests when the circuit is closed.
// This function is used to update the circuit breaker state after a successful operation completes
// while the circuit is in the closed state.
//
// This function is part of the circuit breaker's state management for the closed state.
// When a request succeeds in closed state:
// 1. The current time is obtained
// 2. The addSuccess function is called with the current time to update the ClosedState
// 3. The updated ClosedState is wrapped in a Right (closed) BreakerState
// 4. The breaker state is modified with the new state
// The function takes a Reader that adds a success record to the ClosedState and lifts it to work
// with BreakerState by mapping over the Right (closed) side of the Either type. This ensures that
// success tracking only affects the closed state and leaves any open state unchanged.
//
// Parameters:
// - currentTime: An IO operation that provides the current time
// - addSuccess: A Reader that takes a time and returns an endomorphism for ClosedState,
// typically resetting failure counters or history
// - addSuccess: A Reader that takes the current time and returns an Endomorphism that updates
// the ClosedState by recording a successful operation. This typically increments a success
// counter or updates a success history.
//
// Returns:
// - An io.Kleisli that takes another io.Kleisli and chains them together.
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
// This allows composing the success handling with other state modifications.
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
// an endomorphism that updates the BreakerState by applying the success update to the closed
// state (if closed) or leaving the state unchanged (if open).
//
// Thread Safety: This function creates IO operations that will atomically modify the
// IORef[BreakerState] when executed. The state modifications are thread-safe.
//
// Type signature:
//
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
// Thread Safety: This is a pure function that creates new state instances. The returned
// endomorphism is safe for concurrent use as it does not mutate its input.
//
// Usage Context:
// - Called when a request succeeds while the circuit is closed
// - Resets failure tracking (counter or history) in the ClosedState
// - Keeps the circuit in closed state
// - Called after a successful request completes while the circuit is closed
// - Updates success metrics/counters in the ClosedState
// - Does not affect the circuit state if it's already open
// - Part of the normal operation flow when the circuit breaker is functioning properly
func handleSuccessOnClosed(
currentTime IO[time.Time],
addSuccess Reader[time.Time, Endomorphism[ClosedState]],
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
) Reader[time.Time, Endomorphism[BreakerState]] {
return F.Flow2(
io.Chain,
identity.Flap[IO[BreakerState]](F.Pipe1(
currentTime,
io.Map(F.Flow2(
addSuccess,
either.Map[openState],
)))),
addSuccess,
either.Map[openState],
)
}
// handleFailureOnClosed handles a failed request when the circuit breaker is in closed state.
// It updates the closed state by recording the failure and checks if the circuit should open.
// handleFailureOnClosed creates a Reader that handles failed requests when the circuit is closed.
// This function manages the critical logic for determining whether a failure should cause the
// circuit breaker to open (transition from closed to open state).
//
// This function is part of the circuit breaker's state management for the closed state.
// When a request fails in closed state:
// 1. The current time is obtained
// 2. The addError function is called to record the failure in the ClosedState
// 3. The checkClosedState function is called to determine if the failure threshold is exceeded
// 4. If the threshold is exceeded (Check returns None):
// - The circuit transitions to open state using openCircuit
// - A new openState is created with resetAt time calculated from the retry policy
// 5. If the threshold is not exceeded (Check returns Some):
// - The circuit remains closed with the updated failure tracking
// The function orchestrates three key operations:
// 1. Records the failure in the ClosedState using addError
// 2. Checks if the failure threshold has been exceeded using checkClosedState
// 3. If threshold exceeded, opens the circuit; otherwise, keeps it closed with updated error count
//
// The decision flow is:
// - Add the error to the closed state's error tracking
// - Check if the updated closed state exceeds the failure threshold
// - If threshold exceeded (checkClosedState returns None):
// - Create a new openState with calculated reset time based on retry policy
// - Transition the circuit to open state (Left side of Either)
// - If threshold not exceeded (checkClosedState returns Some):
// - Keep the circuit closed with the updated error count
// - Continue allowing requests through
//
// Parameters:
// - currentTime: An IO operation that provides the current time
// - addError: A Reader that takes a time and returns an endomorphism for ClosedState,
// recording a failure (incrementing counter or adding to history)
// - checkClosedState: A Reader that takes a time and returns an option.Kleisli that checks
// if the ClosedState should remain closed. Returns Some if circuit stays closed, None if it should open.
// - openCircuit: A Reader that takes a time and returns an openState with calculated resetAt time
// - addError: A Reader that takes the current time and returns an Endomorphism that updates
// the ClosedState by recording a failed operation. This typically increments an error
// counter or adds to an error history.
// - checkClosedState: A Reader that takes the current time and returns an option.Kleisli that
// validates whether the ClosedState is still within acceptable failure thresholds.
// Returns Some(ClosedState) if threshold not exceeded, None if threshold exceeded.
// - openCircuit: A Reader that takes the current time and creates a new openState with
// appropriate reset time calculated from the retry policy. Used when transitioning to open.
//
// Returns:
// - An io.Kleisli that takes another io.Kleisli and chains them together.
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
// This allows composing the failure handling with other state modifications.
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
// an endomorphism that either:
// - Keeps the circuit closed with updated error tracking (if threshold not exceeded)
// - Opens the circuit with calculated reset time (if threshold exceeded)
//
// Thread Safety: This function creates IO operations that will atomically modify the
// IORef[BreakerState] when executed. The state modifications are thread-safe.
//
// Type signature:
//
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
//
// State Transitions:
// - Closed -> Closed: When failure threshold is not exceeded (Some from checkClosedState)
// - Closed -> Open: When failure threshold is exceeded (None from checkClosedState)
// Thread Safety: This is a pure function that creates new state instances. The returned
// endomorphism is safe for concurrent use as it does not mutate its input.
//
// Usage Context:
// - Called when a request fails while the circuit is closed
// - Records the failure in the ClosedState (counter or history)
// - May trigger transition to open state if threshold is exceeded
// - Called after a failed request completes while the circuit is closed
// - Implements the core circuit breaker logic for opening the circuit
// - Determines when to stop allowing requests through to protect the failing service
// - Critical for preventing cascading failures in distributed systems
//
// State Transition:
// - Closed (under threshold) -> Closed (with incremented error count)
// - Closed (at/over threshold) -> Open (with reset time for recovery attempt)
func handleFailureOnClosed(
currentTime IO[time.Time],
addError Reader[time.Time, Endomorphism[ClosedState]],
checkClosedState Reader[time.Time, option.Kleisli[ClosedState, ClosedState]],
openCircuit Reader[time.Time, openState],
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
return F.Flow2(
io.Chain,
identity.Flap[IO[BreakerState]](F.Pipe1(
currentTime,
io.Map(func(ct time.Time) either.Operator[openState, ClosedState, ClosedState] {
return either.Chain(F.Flow3(
addError(ct),
checkClosedState(ct),
option.Fold(
F.Pipe2(
ct,
lazy.Of,
lazy.Map(F.Flow2(
openCircuit,
createOpenCircuit,
)),
),
createClosedCircuit,
),
))
}))),
) Reader[time.Time, Endomorphism[BreakerState]] {
return F.Pipe2(
F.Pipe1(
addError,
reader.ApS(reader.Map[ClosedState], checkClosedState),
),
reader.Chain(F.Flow2(
reader.Map[ClosedState](option.Fold(
F.Pipe2(
openCircuit,
reader.Map[time.Time](createOpenCircuit),
lazy.Of,
),
F.Flow2(
createClosedCircuit,
reader.Of[time.Time],
),
)),
reader.Sequence,
)),
reader.Map[time.Time](either.Chain[openState, ClosedState, ClosedState]),
)
}
func handleErrorOnClosed2[E any](
checkError option.Kleisli[E, E],
onSuccess Reader[time.Time, Endomorphism[BreakerState]],
onFailure Reader[time.Time, Endomorphism[BreakerState]],
) reader.Kleisli[time.Time, E, Endomorphism[BreakerState]] {
return F.Flow3(
checkError,
option.MapTo[E](onFailure),
option.GetOrElse(lazy.Of(onSuccess)),
)
}
func stateModifier(
modify io.Kleisli[Endomorphism[BreakerState], BreakerState],
) reader.Operator[time.Time, Endomorphism[BreakerState], IO[BreakerState]] {
return reader.Map[time.Time](modify)
}
func reportOnClose2(
onClosed ReaderIO[time.Time, Void],
onOpened ReaderIO[time.Time, Void],
) readerio.Operator[time.Time, BreakerState, Void] {
return readerio.Chain(either.Fold(
reader.Of[openState](onOpened),
reader.Of[ClosedState](onClosed),
))
}
func applyAndReportClose2(
currentTime IO[time.Time],
metrics readerio.Operator[time.Time, BreakerState, Void],
) func(io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
return func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
return F.Flow3(
reader.Map[time.Time](modify),
metrics,
readerio.ReadIO[Void](currentTime),
)
}
}
// MakeCircuitBreaker creates a circuit breaker implementation for a higher-kinded type.
@@ -402,6 +432,8 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
chainFirstIOK func(io.Kleisli[T, BreakerState]) func(HKTT) HKTT,
chainFirstLeftIOK func(io.Kleisli[E, BreakerState]) func(HKTT) HKTT,
chainFirstIOK2 func(io.Kleisli[Either[E, T], Void]) func(HKTT) HKTT,
fromIO func(IO[func(HKTT) HKTT]) HKTOP,
flap func(HKTT) func(HKTOP) HKTHKTT,
flatten func(HKTHKTT) HKTT,
@@ -437,47 +469,22 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
reader.Of[HKTT],
)
handleSuccess := handleSuccessOnClosed(currentTime, addSuccess)
handleFailure := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
handleSuccess2 := handleSuccessOnClosed(addSuccess)
handleFailure2 := handleFailureOnClosed(addError, checkClosedState, openCircuit)
handleError2 := handleErrorOnClosed2(checkError, handleSuccess2, handleFailure2)
metricsClose2 := reportOnClose2(metrics.Accept, metrics.Open)
apply2 := applyAndReportClose2(currentTime, metricsClose2)
onClosed := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {
return F.Flow2(
// error case
chainFirstLeftIOK(F.Flow3(
checkError,
option.Fold(
// the error is not applicable, handle as success
F.Pipe2(
modify,
handleSuccess,
lazy.Of,
),
// the error is relevant, record it
F.Pipe2(
modify,
handleFailure,
reader.Of[E],
),
),
// metering
io.ChainFirst(either.Fold(
F.Flow2(
openedAtLens.Get,
metrics.Open,
),
func(c ClosedState) IO[Void] {
return io.Of(function.VOID)
},
)),
)),
// good case
chainFirstIOK(F.Pipe2(
modify,
handleSuccess,
reader.Of[T],
)),
)
return chainFirstIOK2(F.Flow2(
either.Fold(
handleError2,
reader.Of[T](handleSuccess2),
),
apply2(modify),
))
}
onCanary := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {

View File

@@ -5,12 +5,12 @@ import (
"testing"
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioref"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
@@ -452,43 +452,128 @@ func TestIsResetTimeExceeded(t *testing.T) {
// TestHandleSuccessOnClosed tests the handleSuccessOnClosed function
func TestHandleSuccessOnClosed(t *testing.T) {
t.Run("resets failure count on success", func(t *testing.T) {
t.Run("updates closed state with success when circuit is closed", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addSuccess := reader.From1(ClosedState.AddSuccess)
currentTime := vt.Now()
// Create initial state with some failures
now := vt.Now()
// Create a simple addSuccess reader that increments a counter
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
// Create initial closed state
initialClosed := MakeClosedStateCounter(3)
initialClosed = initialClosed.AddError(now)
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
// Apply handleSuccessOnClosed
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
result := endomorphism(initialState)
handler := handleSuccessOnClosed(currentTime, addSuccess)
// Verify the state is still closed
assert.True(t, IsClosed(result), "state should remain closed after success")
// Apply the handler
result := io.Run(handler(modify))
// Verify state is still closed and failures are reset
assert.True(t, IsClosed(result), "circuit should remain closed after success")
// Verify the closed state was updated
closedState := either.Fold(
func(openState) ClosedState { return initialClosed },
F.Identity[ClosedState],
)(result)
// The success should have been recorded (implementation-specific verification)
assert.NotNil(t, closedState, "closed state should be present")
})
t.Run("keeps circuit closed", func(t *testing.T) {
t.Run("does not affect open state", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addSuccess := reader.From1(ClosedState.AddSuccess)
currentTime := vt.Now()
initialState := createClosedCircuit(MakeClosedStateCounter(3))
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
handler := handleSuccessOnClosed(currentTime, addSuccess)
result := io.Run(handler(modify))
// Create initial open state
initialOpen := openState{
openedAt: currentTime.Add(-1 * time.Minute),
resetAt: currentTime.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
initialState := createOpenCircuit(initialOpen)
assert.True(t, IsClosed(result), "circuit should remain closed")
// Apply handleSuccessOnClosed
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
result := endomorphism(initialState)
// Verify the state remains open and unchanged
assert.True(t, IsOpen(result), "state should remain open")
// Extract and verify the open state is unchanged
openResult := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return initialOpen },
)(result)
assert.Equal(t, initialOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
assert.Equal(t, initialOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
assert.Equal(t, initialOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
})
t.Run("preserves time parameter through reader", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
time1 := vt.Now()
vt.Advance(1 * time.Hour)
time2 := vt.Now()
var capturedTime time.Time
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
capturedTime = ct
return F.Identity[ClosedState]
}
initialClosed := MakeClosedStateCounter(3)
initialState := createClosedCircuit(initialClosed)
handler := handleSuccessOnClosed(addSuccess)
// Apply with time1
endomorphism1 := handler(time1)
endomorphism1(initialState)
assert.Equal(t, time1, capturedTime, "should pass time1 to addSuccess")
// Apply with time2
endomorphism2 := handler(time2)
endomorphism2(initialState)
assert.Equal(t, time2, capturedTime, "should pass time2 to addSuccess")
})
t.Run("composes correctly with multiple successes", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
initialClosed := MakeClosedStateCounter(3)
initialState := createClosedCircuit(initialClosed)
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
// Apply multiple times
result1 := endomorphism(initialState)
result2 := endomorphism(result1)
result3 := endomorphism(result2)
// All should remain closed
assert.True(t, IsClosed(result1), "state should remain closed after first success")
assert.True(t, IsClosed(result2), "state should remain closed after second success")
assert.True(t, IsClosed(result3), "state should remain closed after third success")
})
}
@@ -496,9 +581,26 @@ func TestHandleSuccessOnClosed(t *testing.T) {
func TestHandleFailureOnClosed(t *testing.T) {
t.Run("keeps circuit closed when threshold not exceeded", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
// Create a closed state that allows 3 errors
initialClosed := MakeClosedStateCounter(3)
// addError increments error count
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
// checkClosedState returns Some if under threshold
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
// openCircuit creates an open state (shouldn't be called in this test)
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -508,26 +610,39 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
// Create initial state with room for more failures
now := vt.Now()
initialClosed := MakeClosedStateCounter(5) // threshold is 5
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
// First error - should stay closed
result1 := endomorphism(initialState)
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
assert.True(t, IsClosed(result), "circuit should remain closed when threshold not exceeded")
// Second error - should stay closed
result2 := endomorphism(result1)
assert.True(t, IsClosed(result2), "circuit should remain closed after second error")
})
t.Run("opens circuit when threshold exceeded", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
// Create a closed state that allows only 2 errors (opens at 2nd error)
initialClosed := MakeClosedStateCounter(2)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -537,26 +652,85 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
// Create initial state at threshold
now := vt.Now()
initialClosed := MakeClosedStateCounter(2) // threshold is 2
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
// First error - should stay closed (count=1, threshold=2)
result1 := endomorphism(initialState)
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
assert.True(t, IsOpen(result), "circuit should open when threshold exceeded")
// Second error - should open (count=2, threshold=2)
result2 := endomorphism(result1)
assert.True(t, IsOpen(result2), "circuit should open when threshold reached")
})
t.Run("records failure in closed state", func(t *testing.T) {
t.Run("creates open state with correct reset time", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
expectedResetTime := currentTime.Add(5 * time.Minute)
initialClosed := MakeClosedStateCounter(1) // Opens at 1st error
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: expectedResetTime,
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// First error - should open immediately (threshold=1)
result1 := endomorphism(initialState)
assert.True(t, IsOpen(result1), "circuit should open after first error")
// Verify the open state has correct reset time
resultOpen := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(result1)
assert.Equal(t, expectedResetTime, resultOpen.resetAt, "reset time should match expected")
assert.Equal(t, currentTime, resultOpen.openedAt, "opened time should be current time")
})
t.Run("edge case: zero error threshold", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
// Create a closed state that allows 0 errors (opens immediately)
initialClosed := MakeClosedStateCounter(0)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -566,14 +740,212 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
initialState := createClosedCircuit(MakeClosedStateCounter(10))
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Should still be closed but with failure recorded
assert.True(t, IsClosed(result), "circuit should remain closed")
// First error should immediately open the circuit
result := endomorphism(initialState)
assert.True(t, IsOpen(result), "circuit should open immediately with zero threshold")
})
t.Run("edge case: very high error threshold", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
// Create a closed state that allows 1000 errors
initialClosed := MakeClosedStateCounter(1000)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Apply many errors
result := initialState
for i := 0; i < 100; i++ {
result = endomorphism(result)
}
// Should still be closed after 100 errors
assert.True(t, IsClosed(result), "circuit should remain closed with high threshold")
})
t.Run("preserves time parameter through reader chain", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
time1 := vt.Now()
vt.Advance(2 * time.Hour)
time2 := vt.Now()
var capturedAddErrorTime, capturedCheckTime, capturedOpenTime time.Time
initialClosed := MakeClosedStateCounter(2) // Need 2 errors to open
addError := func(ct time.Time) Endomorphism[ClosedState] {
capturedAddErrorTime = ct
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
capturedCheckTime = ct
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
capturedOpenTime = ct
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
// Apply with time1 - first error, stays closed
endomorphism1 := handler(time1)
result1 := endomorphism1(initialState)
assert.Equal(t, time1, capturedAddErrorTime, "addError should receive time1")
assert.Equal(t, time1, capturedCheckTime, "checkClosedState should receive time1")
// Apply with time2 - second error, should trigger open
endomorphism2 := handler(time2)
endomorphism2(result1)
assert.Equal(t, time2, capturedAddErrorTime, "addError should receive time2")
assert.Equal(t, time2, capturedCheckTime, "checkClosedState should receive time2")
assert.Equal(t, time2, capturedOpenTime, "openCircuit should receive time2")
})
t.Run("handles transition from closed to open correctly", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
initialClosed := MakeClosedStateCounter(2) // Opens at 2nd error
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Start with closed state
state := createClosedCircuit(initialClosed)
assert.True(t, IsClosed(state), "initial state should be closed")
// First error - should stay closed (count=1, threshold=2)
state = endomorphism(state)
assert.True(t, IsClosed(state), "should remain closed after first error")
// Second error - should open (count=2, threshold=2)
state = endomorphism(state)
assert.True(t, IsOpen(state), "should open after second error")
// Verify it's truly open with correct properties
resultOpen := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(state)
assert.False(t, resultOpen.canaryRequest, "canaryRequest should be false initially")
assert.Equal(t, currentTime, resultOpen.openedAt, "openedAt should be current time")
})
t.Run("does not affect already open state", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
// Start with an already open state
existingOpen := openState{
openedAt: currentTime.Add(-5 * time.Minute),
resetAt: currentTime.Add(5 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: true,
}
initialState := createOpenCircuit(existingOpen)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Apply to open state - should not change it
result := endomorphism(initialState)
assert.True(t, IsOpen(result), "state should remain open")
// The open state should be unchanged since handleFailureOnClosed
// only operates on the Right (closed) side of the Either
openResult := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(result)
assert.Equal(t, existingOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
assert.Equal(t, existingOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
assert.Equal(t, existingOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
})
}

View File

@@ -28,7 +28,10 @@ import (
//
// Thread Safety: This type is immutable and safe for concurrent use.
type CircuitBreakerError struct {
Name string
// Name: The name identifying this circuit breaker instance
Name string
// ResetAt: The time at which the circuit breaker will transition from open to half-open state
ResetAt time.Time
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
)
type (
@@ -110,6 +111,25 @@ type (
name string
logger *log.Logger
}
// voidMetrics is a no-op implementation of the Metrics interface that does nothing.
// All methods return the same pre-allocated IO[Void] operation that immediately returns
// without performing any action.
//
// This implementation is useful for:
// - Testing scenarios where metrics collection is not needed
// - Production environments where metrics overhead should be eliminated
// - Benchmarking circuit breaker logic without metrics interference
// - Default initialization when no metrics implementation is provided
//
// Thread Safety: This implementation is safe for concurrent use. The noop IO operation
// is immutable and can be safely shared across goroutines.
//
// Performance: This is the most efficient Metrics implementation as it performs no
// operations and has minimal memory overhead (single shared IO[Void] instance).
voidMetrics struct {
noop IO[Void]
}
)
// doLog is a helper method that creates an IO operation for logging a circuit breaker event.
@@ -206,3 +226,79 @@ func (m *loggingMetrics) Canary(ct time.Time) IO[Void] {
func MakeMetricsFromLogger(name string, logger *log.Logger) Metrics {
return &loggingMetrics{name: name, logger: logger}
}
// Open implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Open(_ time.Time) IO[Void] {
return m.noop
}
// Accept implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Accept(_ time.Time) IO[Void] {
return m.noop
}
// Canary implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Canary(_ time.Time) IO[Void] {
return m.noop
}
// Close implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Close(_ time.Time) IO[Void] {
return m.noop
}
// Reject implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Reject(_ time.Time) IO[Void] {
return m.noop
}
// MakeVoidMetrics creates a no-op Metrics implementation that performs no operations.
// All methods return the same pre-allocated IO[Void] operation that does nothing when executed.
//
// This is useful for:
// - Testing scenarios where metrics collection is not needed
// - Production environments where metrics overhead should be eliminated
// - Benchmarking circuit breaker logic without metrics interference
// - Default initialization when no metrics implementation is provided
//
// Returns:
// - Metrics: A thread-safe no-op Metrics implementation
//
// Thread Safety: The returned Metrics implementation is safe for concurrent use.
// All methods return the same immutable IO[Void] operation.
//
// Performance: This is the most efficient Metrics implementation with minimal overhead.
// The IO[Void] operation is pre-allocated once and reused for all method calls.
//
// Example:
//
// metrics := MakeVoidMetrics()
//
// // All operations do nothing
// io.Run(metrics.Open(time.Now())) // No-op
// io.Run(metrics.Accept(time.Now())) // No-op
// io.Run(metrics.Reject(time.Now())) // No-op
//
// // Useful for testing
// breaker := MakeCircuitBreaker(
// // ... other parameters ...
// MakeVoidMetrics(), // No metrics overhead
// )
func MakeVoidMetrics() Metrics {
return &voidMetrics{io.Of(function.VOID)}
}

View File

@@ -504,3 +504,443 @@ func TestMetricsIOOperations(t *testing.T) {
assert.Len(t, lines, 3, "should execute multiple times")
})
}
// TestMakeVoidMetrics tests the MakeVoidMetrics constructor
func TestMakeVoidMetrics(t *testing.T) {
t.Run("creates valid Metrics implementation", func(t *testing.T) {
metrics := MakeVoidMetrics()
assert.NotNil(t, metrics, "MakeVoidMetrics should return non-nil Metrics")
})
t.Run("returns voidMetrics type", func(t *testing.T) {
metrics := MakeVoidMetrics()
_, ok := metrics.(*voidMetrics)
assert.True(t, ok, "should return *voidMetrics type")
})
t.Run("initializes noop IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics().(*voidMetrics)
assert.NotNil(t, metrics.noop, "noop IO operation should be initialized")
})
}
// TestVoidMetricsAccept tests the Accept method of voidMetrics
func TestVoidMetricsAccept(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics().(*voidMetrics)
timestamp := time.Now()
ioOp1 := metrics.Accept(timestamp)
ioOp2 := metrics.Accept(timestamp)
// Both should be non-nil (we can't compare functions directly in Go)
assert.NotNil(t, ioOp1, "should return non-nil IO operation")
assert.NotNil(t, ioOp2, "should return non-nil IO operation")
// Verify they execute without error
io.Run(ioOp1)
io.Run(ioOp2)
})
t.Run("ignores timestamp parameter", func(t *testing.T) {
metrics := MakeVoidMetrics()
time1 := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
time2 := time.Date(2026, 1, 9, 16, 30, 0, 0, time.UTC)
ioOp1 := metrics.Accept(time1)
ioOp2 := metrics.Accept(time2)
// Should return same operation regardless of timestamp
io.Run(ioOp1)
io.Run(ioOp2)
// No assertions needed - just verify it doesn't panic
})
}
// TestVoidMetricsReject tests the Reject method of voidMetrics
func TestVoidMetricsReject(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsOpen tests the Open method of voidMetrics
func TestVoidMetricsOpen(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsClose tests the Close method of voidMetrics
func TestVoidMetricsClose(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsCanary tests the Canary method of voidMetrics
func TestVoidMetricsCanary(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsThreadSafety tests concurrent access to voidMetrics
func TestVoidMetricsThreadSafety(t *testing.T) {
t.Run("handles concurrent metric calls", func(t *testing.T) {
metrics := MakeVoidMetrics()
var wg sync.WaitGroup
numGoroutines := 100
wg.Add(numGoroutines * 5) // 5 methods
timestamp := time.Now()
// Launch multiple goroutines calling all methods concurrently
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
io.Run(metrics.Accept(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Reject(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Open(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Close(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Canary(timestamp))
}()
}
wg.Wait()
// Test passes if no panic occurs
})
t.Run("all methods return valid IO operations concurrently", func(t *testing.T) {
metrics := MakeVoidMetrics()
var wg sync.WaitGroup
numGoroutines := 50
wg.Add(numGoroutines)
timestamp := time.Now()
results := make([]IO[Void], numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(idx int) {
defer wg.Done()
// Each goroutine calls a different method
switch idx % 5 {
case 0:
results[idx] = metrics.Accept(timestamp)
case 1:
results[idx] = metrics.Reject(timestamp)
case 2:
results[idx] = metrics.Open(timestamp)
case 3:
results[idx] = metrics.Close(timestamp)
case 4:
results[idx] = metrics.Canary(timestamp)
}
}(i)
}
wg.Wait()
// All results should be non-nil and executable
for i, result := range results {
assert.NotNil(t, result, "result %d should be non-nil", i)
io.Run(result) // Verify it executes without error
}
})
}
// TestVoidMetricsPerformance tests performance characteristics
func TestVoidMetricsPerformance(t *testing.T) {
t.Run("has minimal overhead", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
// Execute many operations quickly
iterations := 10000
for i := 0; i < iterations; i++ {
io.Run(metrics.Accept(timestamp))
io.Run(metrics.Reject(timestamp))
io.Run(metrics.Open(timestamp))
io.Run(metrics.Close(timestamp))
io.Run(metrics.Canary(timestamp))
}
// Test passes if it completes quickly without issues
})
t.Run("all methods return valid IO operations", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
// All methods should return non-nil IO operations
accept := metrics.Accept(timestamp)
reject := metrics.Reject(timestamp)
open := metrics.Open(timestamp)
close := metrics.Close(timestamp)
canary := metrics.Canary(timestamp)
assert.NotNil(t, accept, "Accept should return non-nil")
assert.NotNil(t, reject, "Reject should return non-nil")
assert.NotNil(t, open, "Open should return non-nil")
assert.NotNil(t, close, "Close should return non-nil")
assert.NotNil(t, canary, "Canary should return non-nil")
// All should execute without error
io.Run(accept)
io.Run(reject)
io.Run(open)
io.Run(close)
io.Run(canary)
})
}
// TestVoidMetricsIntegration tests integration scenarios
func TestVoidMetricsIntegration(t *testing.T) {
t.Run("can be used as drop-in replacement for loggingMetrics", func(t *testing.T) {
// Create both types of metrics
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
loggingMetrics := MakeMetricsFromLogger("TestCircuit", logger)
voidMetrics := MakeVoidMetrics()
timestamp := time.Now()
// Both should implement the same interface
var m1 Metrics = loggingMetrics
var m2 Metrics = voidMetrics
// Both should be callable
io.Run(m1.Accept(timestamp))
io.Run(m2.Accept(timestamp))
// Logging metrics should have output
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
// Void metrics should have no observable side effects
// (we can't directly test this, but the test passes if no panic occurs)
})
t.Run("simulates complete circuit breaker lifecycle without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
baseTime := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
// Simulate circuit breaker lifecycle - all should be no-ops
io.Run(metrics.Accept(baseTime))
io.Run(metrics.Accept(baseTime.Add(1 * time.Second)))
io.Run(metrics.Open(baseTime.Add(2 * time.Second)))
io.Run(metrics.Reject(baseTime.Add(3 * time.Second)))
io.Run(metrics.Canary(baseTime.Add(30 * time.Second)))
io.Run(metrics.Close(baseTime.Add(31 * time.Second)))
// Test passes if no panic occurs and completes quickly
})
}
// TestVoidMetricsEdgeCases tests edge cases
func TestVoidMetricsEdgeCases(t *testing.T) {
t.Run("handles zero time", func(t *testing.T) {
metrics := MakeVoidMetrics()
zeroTime := time.Time{}
io.Run(metrics.Accept(zeroTime))
io.Run(metrics.Reject(zeroTime))
io.Run(metrics.Open(zeroTime))
io.Run(metrics.Close(zeroTime))
io.Run(metrics.Canary(zeroTime))
// Test passes if no panic occurs
})
t.Run("handles far future time", func(t *testing.T) {
metrics := MakeVoidMetrics()
futureTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
io.Run(metrics.Accept(futureTime))
io.Run(metrics.Reject(futureTime))
io.Run(metrics.Open(futureTime))
io.Run(metrics.Close(futureTime))
io.Run(metrics.Canary(futureTime))
// Test passes if no panic occurs
})
t.Run("IO operations are idempotent", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
// Execute same operation multiple times
io.Run(ioOp)
io.Run(ioOp)
io.Run(ioOp)
// Test passes if no panic occurs
})
}
// TestMetricsComparison compares loggingMetrics and voidMetrics
func TestMetricsComparison(t *testing.T) {
t.Run("both implement Metrics interface", func(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
var m1 Metrics = MakeMetricsFromLogger("Test", logger)
var m2 Metrics = MakeVoidMetrics()
assert.NotNil(t, m1)
assert.NotNil(t, m2)
})
t.Run("voidMetrics has no observable side effects unlike loggingMetrics", func(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
loggingMetrics := MakeMetricsFromLogger("Test", logger)
voidMetrics := MakeVoidMetrics()
timestamp := time.Now()
// Logging metrics produces output
io.Run(loggingMetrics.Accept(timestamp))
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
// Void metrics has no observable output
// (we can only verify it doesn't panic)
io.Run(voidMetrics.Accept(timestamp))
})
}

View File

@@ -34,6 +34,7 @@ import (
"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"
"github.com/IBM/fp-go/v2/retry"
"github.com/IBM/fp-go/v2/state"
)
@@ -79,10 +80,13 @@ type (
// and produces a value of type A. Used for dependency injection and configuration.
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// openState represents the internal state when the circuit breaker is open.
// In the open state, requests are blocked to give the failing service time to recover.
// The circuit breaker will transition to a half-open state (canary request) after resetAt.
openState struct {
// openedAt is the time when the circuit breaker opened the circuit
openedAt time.Time
// resetAt is the time when the circuit breaker should attempt a canary request

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateTraverseTuple(f *os.File, i int) {
@@ -422,10 +423,10 @@ func ApplyCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateApplyHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func createCombinations(n int, all, prev []int) [][]int {
@@ -284,10 +285,10 @@ func BindCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateBindHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func Commands() []*C.Command {

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"strings"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// Deprecated:
@@ -261,10 +262,10 @@ func ContextReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateContextReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateMakeProvider(f *os.File, i int) {
@@ -221,10 +222,10 @@ func DICommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateDIHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func eitherHKT(typeE string) func(typeA string) string {
@@ -190,10 +191,10 @@ func EitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func identityHKT(typeA string) string {
@@ -93,10 +94,10 @@ func IdentityCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIdentityHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIO(param string) string {
@@ -102,10 +103,10 @@ func IOCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// [GA ~func() ET.Either[E, A], GB ~func() ET.Either[E, B], GTAB ~func() ET.Either[E, T.Tuple2[A, B]], E, A, B any](a GA, b GB) GTAB {
@@ -273,10 +274,10 @@ func IOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIOOption(param string) string {
@@ -107,10 +108,10 @@ func IOOptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -17,6 +17,7 @@ package cli
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/token"
@@ -28,7 +29,7 @@ import (
"text/template"
S "github.com/IBM/fp-go/v2/string"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (
@@ -535,9 +536,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
fieldTypeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := fieldTypeName
@@ -697,9 +698,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
typeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := typeName
isComparable := false
@@ -934,12 +935,12 @@ func LensCommand() *C.Command {
flagVerbose,
flagIncludeTestFiles,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateLensHelpers(
ctx.String(keyLensDir),
ctx.String(keyFilename),
ctx.Bool(keyVerbose),
ctx.Bool(keyIncludeTestFile),
cmd.String(keyLensDir),
cmd.String(keyFilename),
cmd.Bool(keyVerbose),
cmd.Bool(keyIncludeTestFile),
)
},
}

View File

@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
}
func TestParseFileWithUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Config struct {
PublicName string
privateName string
PublicValue int
privateValue *int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Config struct
config := structs[0]
assert.Equal(t, "Config", config.Name)
assert.Len(t, config.Fields, 4, "Should include both exported and unexported fields")
// Check exported field
assert.Equal(t, "PublicName", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Check unexported field
assert.Equal(t, "privateName", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.False(t, config.Fields[1].IsOptional)
// Check exported int field
assert.Equal(t, "PublicValue", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.False(t, config.Fields[2].IsOptional)
// Check unexported pointer field
assert.Equal(t, "privateValue", config.Fields[3].Name)
assert.Equal(t, "*int", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
}
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type MixedStruct struct {
PublicField string
privateField int
OptionalPrivate *string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "MixedStructLenses")
assert.Contains(t, contentStr, "MakeMixedStructLenses")
// Check that lenses are generated for all fields (exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
// Check lens constructors
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
// Check setters
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
}
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type PrivateConfig struct {
name string
value int
enabled bool
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check PrivateConfig struct
config := structs[0]
assert.Equal(t, "PrivateConfig", config.Name)
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
// Check all fields are unexported
assert.Equal(t, "name", config.Fields[0].Name)
assert.Equal(t, "value", config.Fields[1].Name)
assert.Equal(t, "enabled", config.Fields[2].Name)
}
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
type BaseConfig struct {
publicBase string
privateBase int
}
// fp-go:Lens
type ExtendedConfig struct {
BaseConfig
PublicField string
privateField bool
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "ExtendedConfigLenses")
// Check that lenses are generated for embedded unexported fields
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
// Check that lenses are generated for direct fields (both exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
}
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
// Create a temporary test file with various field visibility patterns
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type ComplexStruct struct {
// Exported fields
Name string
Age int
Email *string
// Unexported fields
password string
secretKey []byte
internalID *int
// Mixed with tags
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check ComplexStruct
complex := structs[0]
assert.Equal(t, "ComplexStruct", complex.Name)
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
// Verify field names and types
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
for i, expectedName := range fieldNames {
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
}
// Check optional fields
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func optionHKT(typeA string) string {
@@ -200,10 +201,10 @@ func OptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateUnsliced(f *os.File, i int) {
@@ -423,10 +424,10 @@ func PipeCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generatePipeHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderFrom(f, fg *os.File, i int) {
@@ -154,10 +155,10 @@ func ReaderCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderIOEitherFrom(f, fg *os.File, i int) {
@@ -284,10 +285,10 @@ func ReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"strings"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func writeTupleType(f *os.File, symbol string, i int) {
@@ -615,10 +616,10 @@ func TupleCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateTupleHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -177,3 +177,255 @@ func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
}
}
}
// Compose is an alias for Local that emphasizes the composition aspect of consumer transformation.
// It composes a preprocessing function with a consumer, creating a new consumer that applies
// the function before consuming the value.
//
// This function is semantically identical to Local but uses terminology that may be more familiar
// to developers coming from functional programming backgrounds where "compose" is a common operation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// The name "Compose" highlights that we're composing two operations:
// 1. The transformation function f: R2 -> R1
// 2. The consumer c: R1 -> ()
//
// Result: A composed consumer: R2 -> ()
//
// Type Parameters:
// - R1: The input type of the original Consumer (what it expects)
// - R2: The input type of the new Consumer (what you have)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic composition:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Compose with a string-to-int parser
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Compose(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Composing multiple transformations:
//
// type Data struct {
// Value string
// }
//
// type Wrapper struct {
// Data Data
// }
//
// // Consumer that logs strings
// logString := func(s string) {
// fmt.Println(s)
// }
//
// // Compose transformations step by step
// extractData := func(w Wrapper) Data { return w.Data }
// extractValue := func(d Data) string { return d.Value }
//
// logData := consumer.Compose(extractValue)(logString)
// logWrapper := consumer.Compose(extractData)(logData)
//
// logWrapper(Wrapper{Data: Data{Value: "Hello"}}) // Logs: "Hello"
//
// Example - Function composition style:
//
// // Compose is particularly useful when thinking in terms of function composition
// type Request struct {
// Body []byte
// }
//
// // Consumer that processes strings
// processString := func(s string) {
// fmt.Printf("Processing: %s\n", s)
// }
//
// // Compose byte-to-string conversion with processing
// bytesToString := func(b []byte) string {
// return string(b)
// }
// extractBody := func(r Request) []byte {
// return r.Body
// }
//
// // Chain compositions
// processBytes := consumer.Compose(bytesToString)(processString)
// processRequest := consumer.Compose(extractBody)(processBytes)
//
// processRequest(Request{Body: []byte("test")}) // Logs: "Processing: test"
//
// Relationship to Local:
// - Compose and Local are identical in implementation
// - Compose emphasizes the functional composition aspect
// - Local emphasizes the environment/context transformation aspect
// - Use Compose when thinking about function composition
// - Use Local when thinking about adapting to different contexts
//
// Use Cases:
// - Building processing pipelines with clear composition semantics
// - Adapting consumers in a functional programming style
// - Creating reusable consumer transformations
// - Chaining multiple preprocessing steps
func Compose[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
return Local(f)
}
// Contramap is the categorical name for the contravariant functor operation on Consumers.
// It transforms a Consumer by preprocessing its input, making it the dual of the covariant
// functor's map operation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#contravariant
//
// In category theory, a contravariant functor reverses the direction of morphisms.
// While a covariant functor maps f: A -> B to map(f): F[A] -> F[B],
// a contravariant functor maps f: A -> B to contramap(f): F[B] -> F[A].
//
// For Consumers:
// - Consumer[A] is contravariant in A
// - Given f: R2 -> R1, contramap(f) transforms Consumer[R1] to Consumer[R2]
// - The direction is reversed: we go from Consumer[R1] to Consumer[R2]
//
// This is semantically identical to Local and Compose, but uses the standard
// categorical terminology that emphasizes the contravariant nature of the transformation.
//
// Type Parameters:
// - R1: The input type of the original Consumer (what it expects)
// - R2: The input type of the new Consumer (what you have)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic contravariant mapping:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Contramap with a string-to-int parser
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Contramap(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Demonstrating contravariance:
//
// // In covariant functors (like Option, Array), map goes "forward":
// // map: (A -> B) -> F[A] -> F[B]
// //
// // In contravariant functors (like Consumer), contramap goes "backward":
// // contramap: (B -> A) -> F[A] -> F[B]
//
// type Animal struct{ Name string }
// type Dog struct{ Animal Animal; Breed string }
//
// // Consumer for animals
// consumeAnimal := func(a Animal) {
// fmt.Printf("Animal: %s\n", a.Name)
// }
//
// // Function from Dog to Animal (B -> A)
// dogToAnimal := func(d Dog) Animal {
// return d.Animal
// }
//
// // Contramap creates Consumer[Dog] from Consumer[Animal]
// // Direction is reversed: Consumer[Animal] -> Consumer[Dog]
// consumeDog := consumer.Contramap(dogToAnimal)(consumeAnimal)
//
// consumeDog(Dog{
// Animal: Animal{Name: "Buddy"},
// Breed: "Golden Retriever",
// }) // Logs: "Animal: Buddy"
//
// Example - Contravariant functor laws:
//
// // Law 1: Identity
// // contramap(identity) = identity
// identity := func(x int) int { return x }
// consumer1 := consumer.Contramap(identity)(consumeInt)
// // consumer1 behaves identically to consumeInt
//
// // Law 2: Composition
// // contramap(f . g) = contramap(g) . contramap(f)
// // Note: composition order is reversed compared to covariant map
// f := func(s string) int { n, _ := strconv.Atoi(s); return n }
// g := func(b bool) string { if b { return "1" } else { return "0" } }
//
// // These two are equivalent:
// consumer2 := consumer.Contramap(func(b bool) int { return f(g(b)) })(consumeInt)
// consumer3 := consumer.Contramap(g)(consumer.Contramap(f)(consumeInt))
//
// Example - Practical use with type hierarchies:
//
// type Logger interface {
// Log(string)
// }
//
// type Message struct {
// Text string
// Timestamp time.Time
// }
//
// // Consumer that logs strings
// logString := func(s string) {
// fmt.Println(s)
// }
//
// // Contramap to handle Message types
// extractText := func(m Message) string {
// return fmt.Sprintf("[%s] %s", m.Timestamp.Format(time.RFC3339), m.Text)
// }
//
// logMessage := consumer.Contramap(extractText)(logString)
// logMessage(Message{
// Text: "Hello",
// Timestamp: time.Now(),
// }) // Logs: "[2024-01-20T10:00:00Z] Hello"
//
// Relationship to Local and Compose:
// - Contramap, Local, and Compose are identical in implementation
// - Contramap emphasizes the categorical/theoretical aspect
// - Local emphasizes the context transformation aspect
// - Compose emphasizes the function composition aspect
// - Use Contramap when working with category theory concepts
// - Use Local when adapting to different contexts
// - Use Compose when building functional pipelines
//
// Category Theory Background:
// - Consumer[A] forms a contravariant functor
// - The contravariant functor laws must hold:
// 1. contramap(id) = id
// 2. contramap(f ∘ g) = contramap(g) ∘ contramap(f)
// - This is dual to the covariant functor (map) operation
// - Consumers are contravariant because they consume rather than produce values
//
// Use Cases:
// - Working with contravariant functors in a categorical style
// - Adapting consumers to work with more specific types
// - Building type-safe consumer transformations
// - Implementing profunctor patterns (Consumer is a profunctor)
func Contramap[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
return Local(f)
}

View File

@@ -381,3 +381,513 @@ func TestLocal(t *testing.T) {
assert.Equal(t, 42, captured)
})
}
func TestContramap(t *testing.T) {
t.Run("basic contravariant mapping", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
parseToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Contramap(parseToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("contravariant identity law", func(t *testing.T) {
// contramap(identity) = identity
var captured int
consumeInt := func(x int) {
captured = x
}
identity := function.Identity[int]
consumeIdentity := Contramap(identity)(consumeInt)
consumeIdentity(42)
assert.Equal(t, 42, captured)
// Should behave identically to original consumer
consumeInt(100)
capturedDirect := captured
consumeIdentity(100)
capturedMapped := captured
assert.Equal(t, capturedDirect, capturedMapped)
})
t.Run("contravariant composition law", func(t *testing.T) {
// contramap(f . g) = contramap(g) . contramap(f)
var captured int
consumeInt := func(x int) {
captured = x
}
f := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
g := func(b bool) string {
if b {
return "1"
}
return "0"
}
// Compose f and g manually
fg := func(b bool) int {
return f(g(b))
}
// Method 1: contramap(f . g)
consumer1 := Contramap(fg)(consumeInt)
consumer1(true)
result1 := captured
// Method 2: contramap(g) . contramap(f)
consumer2 := Contramap(g)(Contramap(f)(consumeInt))
consumer2(true)
result2 := captured
assert.Equal(t, result1, result2)
assert.Equal(t, 1, result1)
})
t.Run("type hierarchy adaptation", func(t *testing.T) {
type Animal struct {
Name string
}
type Dog struct {
Animal Animal
Breed string
}
var capturedName string
consumeAnimal := func(a Animal) {
capturedName = a.Name
}
dogToAnimal := func(d Dog) Animal {
return d.Animal
}
consumeDog := Contramap(dogToAnimal)(consumeAnimal)
consumeDog(Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
})
assert.Equal(t, "Buddy", capturedName)
})
t.Run("field extraction with contramap", func(t *testing.T) {
type Message struct {
Text string
Timestamp time.Time
}
var capturedText string
consumeString := func(s string) {
capturedText = s
}
extractText := func(m Message) string {
return m.Text
}
consumeMessage := Contramap(extractText)(consumeString)
consumeMessage(Message{
Text: "Hello",
Timestamp: time.Now(),
})
assert.Equal(t, "Hello", capturedText)
})
t.Run("multiple contramap applications", func(t *testing.T) {
type Level3 struct{ Value int }
type Level2 struct{ L3 Level3 }
type Level1 struct{ L2 Level2 }
var captured int
consumeInt := func(x int) {
captured = x
}
extract3 := func(l3 Level3) int { return l3.Value }
extract2 := func(l2 Level2) Level3 { return l2.L3 }
extract1 := func(l1 Level1) Level2 { return l1.L2 }
// Chain contramap operations
consumeLevel3 := Contramap(extract3)(consumeInt)
consumeLevel2 := Contramap(extract2)(consumeLevel3)
consumeLevel1 := Contramap(extract1)(consumeLevel2)
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
assert.Equal(t, 42, captured)
})
t.Run("contramap with calculation", func(t *testing.T) {
type Rectangle struct {
Width int
Height int
}
var capturedArea int
consumeArea := func(area int) {
capturedArea = area
}
calculateArea := func(r Rectangle) int {
return r.Width * r.Height
}
consumeRectangle := Contramap(calculateArea)(consumeArea)
consumeRectangle(Rectangle{Width: 5, Height: 10})
assert.Equal(t, 50, capturedArea)
})
t.Run("contramap preserves side effects", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
contramappedConsumer := Contramap(transform)(consumer)
contramappedConsumer("1")
contramappedConsumer("2")
contramappedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("contramap with pointer types", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
dereference := func(p *int) int {
if p == nil {
return 0
}
return *p
}
consumePointer := Contramap(dereference)(consumeInt)
value := 42
consumePointer(&value)
assert.Equal(t, 42, captured)
consumePointer(nil)
assert.Equal(t, 0, captured)
})
t.Run("contramap equivalence with Local", func(t *testing.T) {
var capturedLocal, capturedContramap int
consumeIntLocal := func(x int) {
capturedLocal = x
}
consumeIntContramap := func(x int) {
capturedContramap = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// Both should produce identical results
consumerLocal := Local(transform)(consumeIntLocal)
consumerContramap := Contramap(transform)(consumeIntContramap)
consumerLocal("42")
consumerContramap("42")
assert.Equal(t, capturedLocal, capturedContramap)
assert.Equal(t, 42, capturedLocal)
})
}
func TestCompose(t *testing.T) {
t.Run("basic composition", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
parseToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Compose(parseToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("composing multiple transformations", func(t *testing.T) {
type Data struct {
Value string
}
type Wrapper struct {
Data Data
}
var captured string
consumeString := func(s string) {
captured = s
}
extractData := func(w Wrapper) Data { return w.Data }
extractValue := func(d Data) string { return d.Value }
// Compose step by step
consumeData := Compose(extractValue)(consumeString)
consumeWrapper := Compose(extractData)(consumeData)
consumeWrapper(Wrapper{Data: Data{Value: "Hello"}})
assert.Equal(t, "Hello", captured)
})
t.Run("function composition style", func(t *testing.T) {
type Request struct {
Body []byte
}
var captured string
processString := func(s string) {
captured = s
}
bytesToString := func(b []byte) string {
return string(b)
}
extractBody := func(r Request) []byte {
return r.Body
}
// Chain compositions
processBytes := Compose(bytesToString)(processString)
processRequest := Compose(extractBody)(processBytes)
processRequest(Request{Body: []byte("test")})
assert.Equal(t, "test", captured)
})
t.Run("compose with identity", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
identity := function.Identity[int]
composedConsumer := Compose(identity)(consumeInt)
composedConsumer(42)
assert.Equal(t, 42, captured)
})
t.Run("compose with field extraction", func(t *testing.T) {
type User struct {
Name string
Email string
Age int
}
var capturedName string
consumeName := func(name string) {
capturedName = name
}
extractName := func(u User) string {
return u.Name
}
consumeUser := Compose(extractName)(consumeName)
consumeUser(User{Name: "Alice", Email: "alice@example.com", Age: 30})
assert.Equal(t, "Alice", capturedName)
})
t.Run("compose with calculation", func(t *testing.T) {
type Circle struct {
Radius float64
}
var capturedArea float64
consumeArea := func(area float64) {
capturedArea = area
}
calculateArea := func(c Circle) float64 {
return 3.14159 * c.Radius * c.Radius
}
consumeCircle := Compose(calculateArea)(consumeArea)
consumeCircle(Circle{Radius: 5.0})
assert.InDelta(t, 78.53975, capturedArea, 0.00001)
})
t.Run("compose with slice operations", func(t *testing.T) {
var captured int
consumeLength := func(n int) {
captured = n
}
getLength := func(s []string) int {
return len(s)
}
consumeSlice := Compose(getLength)(consumeLength)
consumeSlice([]string{"a", "b", "c", "d"})
assert.Equal(t, 4, captured)
})
t.Run("compose with map operations", func(t *testing.T) {
var captured bool
consumeHasKey := func(has bool) {
captured = has
}
hasKey := func(m map[string]int) bool {
_, exists := m["key"]
return exists
}
consumeMap := Compose(hasKey)(consumeHasKey)
consumeMap(map[string]int{"key": 42})
assert.True(t, captured)
consumeMap(map[string]int{"other": 42})
assert.False(t, captured)
})
t.Run("compose preserves consumer behavior", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
composedConsumer := Compose(transform)(consumer)
composedConsumer("1")
composedConsumer("2")
composedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("compose with error handling", func(t *testing.T) {
type Result struct {
Value int
Error error
}
var captured int
consumeInt := func(x int) {
captured = x
}
extractValue := func(r Result) int {
if r.Error != nil {
return -1
}
return r.Value
}
consumeResult := Compose(extractValue)(consumeInt)
consumeResult(Result{Value: 42, Error: nil})
assert.Equal(t, 42, captured)
consumeResult(Result{Value: 100, Error: assert.AnError})
assert.Equal(t, -1, captured)
})
t.Run("compose equivalence with Local", func(t *testing.T) {
var capturedLocal, capturedCompose int
consumeIntLocal := func(x int) {
capturedLocal = x
}
consumeIntCompose := func(x int) {
capturedCompose = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// Both should produce identical results
consumerLocal := Local(transform)(consumeIntLocal)
consumerCompose := Compose(transform)(consumeIntCompose)
consumerLocal("42")
consumerCompose("42")
assert.Equal(t, capturedLocal, capturedCompose)
assert.Equal(t, 42, capturedLocal)
})
t.Run("compose equivalence with Contramap", func(t *testing.T) {
var capturedCompose, capturedContramap int
consumeIntCompose := func(x int) {
capturedCompose = x
}
consumeIntContramap := func(x int) {
capturedContramap = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// All three should produce identical results
consumerCompose := Compose(transform)(consumeIntCompose)
consumerContramap := Contramap(transform)(consumeIntContramap)
consumerCompose("42")
consumerContramap("42")
assert.Equal(t, capturedCompose, capturedContramap)
assert.Equal(t, 42, capturedCompose)
})
}

View File

@@ -44,7 +44,7 @@ import (
// return result.Of("done")
// }
//
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
// cancel() // Cancel immediately
//
// wrapped := WithContext(ctx, computation)

View File

@@ -61,7 +61,7 @@ import (
//
// // Safely read file with automatic cleanup
// safeRead := Bracket(acquireFile, readFile, closeFile)
// result := safeRead(context.Background())()
// result := safeRead(t.Context())()
//
//go:inline
func Bracket[

View File

@@ -50,7 +50,7 @@ import (
// // Sequence it to apply Config first
// sequenced := SequenceReader[Config, int](getMultiplier)
// cfg := Config{Timeout: 30}
// result := sequenced(cfg)(context.Background())() // Returns 60
// result := sequenced(cfg)(t.Context())() // Returns 60
//
//go:inline
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
@@ -107,7 +107,7 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
//
// // Provide Config to get final result
// cfg := Config{Multiplier: 5}
// finalResult := result(cfg)(context.Background())() // Returns 50
// finalResult := result(cfg)(t.Context())() // Returns 50
//
//go:inline
func TraverseReader[R, A, B any](

View File

@@ -81,7 +81,7 @@ func SLogWithCallback[A any](
// Chain(SLog[string]("Extracted name")),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Logs: "Extracted name" value="Alice"
//

View File

@@ -45,7 +45,7 @@ func TestPromapBasic(t *testing.T) {
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, "42", result)
})
@@ -69,7 +69,7 @@ func TestContramapBasic(t *testing.T) {
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, 100, result)
})
@@ -90,7 +90,7 @@ func TestLocalBasic(t *testing.T) {
}
adapted := Local[bool](addTimeout)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.True(t, result)
})

View File

@@ -560,6 +560,63 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
return RIO.Read[A](r)
}
// ReadIO executes a ReaderIO computation by providing a context wrapped in an IO effect.
// This is useful when the context itself needs to be computed or retrieved through side effects.
//
// The function takes an IO[context.Context] (an effectful computation that produces a context) and returns
// a function that can execute a ReaderIO[A] to produce an IO[A].
//
// This is particularly useful in scenarios where:
// - The context needs to be created with side effects (e.g., loading configuration)
// - The context requires initialization or setup
// - You want to compose context creation with the computation that uses it
//
// The execution flow is:
// 1. Execute the IO[context.Context] to get the context
// 2. Pass the context to the ReaderIO[A] to get an IO[A]
// 3. Execute the resulting IO[A] to get the final result A
//
// Type Parameters:
// - A: The result type of the ReaderIO computation
//
// Parameters:
// - r: An IO effect that produces a context.Context
//
// Returns:
// - A function that takes a ReaderIO[A] and returns an IO[A]
//
// Example:
//
// import (
// "context"
// G "github.com/IBM/fp-go/v2/io"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Create context with side effects (e.g., loading config)
// createContext := G.Of(context.WithValue(t.Context(), "key", "value"))
//
// // A computation that uses the context
// getValue := readerio.FromReader(func(ctx context.Context) string {
// if val := ctx.Value("key"); val != nil {
// return val.(string)
// }
// return "default"
// })
//
// // Compose them together
// result := readerio.ReadIO[string](createContext)(getValue)
// value := result() // Executes both effects and returns "value"
//
// Comparison with Read:
// - [Read]: Takes a pure context.Context value and executes the ReaderIO immediately
// - [ReadIO]: Takes an IO[context.Context] and chains the effects together
//
//go:inline
func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
return RIO.ReadIO[A](r)
}
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
//
// This is the Reader's local operation, which allows you to modify the environment
@@ -607,7 +664,7 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
// user := result(t.Context())() // Returns "Alice"
//
// Timeout Example:
//
@@ -674,7 +731,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// fetchData,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{} after 5s timeout
// data := result(t.Context())() // Returns Data{} after 5s timeout
//
// Successful Example:
//
@@ -683,7 +740,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// quickFetch,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{Value: "quick"}
// 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)
@@ -734,12 +791,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// fetchData,
// readerio.WithDeadline[Data](deadline),
// )
// data := result(context.Background())() // Returns Data{} if past deadline
// data := result(t.Context())() // Returns Data{} if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)

View File

@@ -31,7 +31,7 @@ func TestMonadMap(t *testing.T) {
rio := Of(5)
doubled := MonadMap(rio, N.Mul(2))
result := doubled(context.Background())()
result := doubled(t.Context())()
assert.Equal(t, 10, result)
}
@@ -41,14 +41,14 @@ func TestMap(t *testing.T) {
Map(utils.Double),
)
assert.Equal(t, 2, g(context.Background())())
assert.Equal(t, 2, g(t.Context())())
}
func TestMonadMapTo(t *testing.T) {
rio := Of(42)
replaced := MonadMapTo(rio, "constant")
result := replaced(context.Background())()
result := replaced(t.Context())()
assert.Equal(t, "constant", result)
}
@@ -58,7 +58,7 @@ func TestMapTo(t *testing.T) {
MapTo[int]("constant"),
)
assert.Equal(t, "constant", result(context.Background())())
assert.Equal(t, "constant", result(t.Context())())
}
func TestMonadChain(t *testing.T) {
@@ -67,7 +67,7 @@ func TestMonadChain(t *testing.T) {
return Of(n * 3)
})
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestChain(t *testing.T) {
@@ -78,7 +78,7 @@ func TestChain(t *testing.T) {
}),
)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestMonadChainFirst(t *testing.T) {
@@ -89,7 +89,7 @@ func TestMonadChainFirst(t *testing.T) {
return Of("side effect")
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -104,7 +104,7 @@ func TestChainFirst(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -117,7 +117,7 @@ func TestMonadTap(t *testing.T) {
return Of(func() {})
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -132,14 +132,14 @@ func TestTap(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestOf(t *testing.T) {
rio := Of(100)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 100, result)
}
@@ -149,7 +149,7 @@ func TestMonadAp(t *testing.T) {
faIO := Of(5)
result := MonadAp(fabIO, faIO)
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestAp(t *testing.T) {
@@ -158,7 +158,7 @@ func TestAp(t *testing.T) {
Ap[int](Of(1)),
)
assert.Equal(t, 2, g(context.Background())())
assert.Equal(t, 2, g(t.Context())())
}
func TestMonadApSeq(t *testing.T) {
@@ -166,7 +166,7 @@ func TestMonadApSeq(t *testing.T) {
faIO := Of(5)
result := MonadApSeq(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestApSeq(t *testing.T) {
@@ -175,7 +175,7 @@ func TestApSeq(t *testing.T) {
ApSeq[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
assert.Equal(t, 15, g(t.Context())())
}
func TestMonadApPar(t *testing.T) {
@@ -183,7 +183,7 @@ func TestMonadApPar(t *testing.T) {
faIO := Of(5)
result := MonadApPar(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestApPar(t *testing.T) {
@@ -192,12 +192,12 @@ func TestApPar(t *testing.T) {
ApPar[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
assert.Equal(t, 15, g(t.Context())())
}
func TestAsk(t *testing.T) {
rio := Ask()
ctx := context.WithValue(context.Background(), "key", "value")
ctx := context.WithValue(t.Context(), "key", "value")
result := rio(ctx)()
assert.Equal(t, ctx, result)
@@ -207,7 +207,7 @@ func TestFromIO(t *testing.T) {
ioAction := G.Of(42)
rio := FromIO(ioAction)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -217,7 +217,7 @@ func TestFromReader(t *testing.T) {
}
rio := FromReader(rdr)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -226,7 +226,7 @@ func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
rio := FromLazy(lazy)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -236,7 +236,7 @@ func TestMonadChainIOK(t *testing.T) {
return G.Of(n * 4)
})
assert.Equal(t, 20, result(context.Background())())
assert.Equal(t, 20, result(t.Context())())
}
func TestChainIOK(t *testing.T) {
@@ -247,7 +247,7 @@ func TestChainIOK(t *testing.T) {
}),
)
assert.Equal(t, 20, result(context.Background())())
assert.Equal(t, 20, result(t.Context())())
}
func TestMonadChainFirstIOK(t *testing.T) {
@@ -258,7 +258,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
return G.Of("side effect")
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -273,7 +273,7 @@ func TestChainFirstIOK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -286,7 +286,7 @@ func TestMonadTapIOK(t *testing.T) {
return G.Of(func() {})
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -301,7 +301,7 @@ func TestTapIOK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -313,8 +313,8 @@ func TestDefer(t *testing.T) {
return Of(counter)
})
result1 := rio(context.Background())()
result2 := rio(context.Background())()
result1 := rio(t.Context())()
result2 := rio(t.Context())()
assert.Equal(t, 1, result1)
assert.Equal(t, 2, result2)
@@ -328,8 +328,8 @@ func TestMemoize(t *testing.T) {
return counter
}))
result1 := memoized(context.Background())()
result2 := memoized(context.Background())()
result1 := memoized(t.Context())()
result2 := memoized(t.Context())()
assert.Equal(t, 1, result1)
assert.Equal(t, 1, result2) // Same value, memoized
@@ -339,7 +339,7 @@ func TestFlatten(t *testing.T) {
nested := Of(Of(42))
flattened := Flatten(nested)
result := flattened(context.Background())()
result := flattened(t.Context())()
assert.Equal(t, 42, result)
}
@@ -347,7 +347,7 @@ func TestMonadFlap(t *testing.T) {
fabIO := Of(N.Mul(3))
result := MonadFlap(fabIO, 7)
assert.Equal(t, 21, result(context.Background())())
assert.Equal(t, 21, result(t.Context())())
}
func TestFlap(t *testing.T) {
@@ -356,7 +356,7 @@ func TestFlap(t *testing.T) {
Flap[int](7),
)
assert.Equal(t, 21, result(context.Background())())
assert.Equal(t, 21, result(t.Context())())
}
func TestMonadChainReaderK(t *testing.T) {
@@ -365,7 +365,7 @@ func TestMonadChainReaderK(t *testing.T) {
return func(ctx context.Context) int { return n * 2 }
})
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestChainReaderK(t *testing.T) {
@@ -376,7 +376,7 @@ func TestChainReaderK(t *testing.T) {
}),
)
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestMonadChainFirstReaderK(t *testing.T) {
@@ -389,7 +389,7 @@ func TestMonadChainFirstReaderK(t *testing.T) {
}
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -406,7 +406,7 @@ func TestChainFirstReaderK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -421,7 +421,7 @@ func TestMonadTapReaderK(t *testing.T) {
}
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -438,14 +438,14 @@ func TestTapReaderK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestRead(t *testing.T) {
rio := Of(42)
ctx := context.Background()
ctx := t.Context()
ioAction := Read[int](ctx)(rio)
result := ioAction()
@@ -463,7 +463,7 @@ func TestComplexPipeline(t *testing.T) {
Map(N.Add(10)),
)
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
assert.Equal(t, 20, result(t.Context())()) // (5 * 2) + 10 = 20
}
func TestFromIOWithChain(t *testing.T) {
@@ -476,7 +476,7 @@ func TestFromIOWithChain(t *testing.T) {
}),
)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestTapWithLogging(t *testing.T) {
@@ -496,7 +496,192 @@ func TestTapWithLogging(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 84, value)
assert.Equal(t, []int{42, 84}, logged)
}
func TestReadIO(t *testing.T) {
// Test basic ReadIO functionality
contextIO := G.Of(context.WithValue(t.Context(), "testKey", "testValue"))
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("testKey"); val != nil {
return val.(string)
}
return "default"
})
ioAction := ReadIO[string](contextIO)(rio)
result := ioAction()
assert.Equal(t, "testValue", result)
}
func TestReadIOWithBackground(t *testing.T) {
// Test ReadIO with plain background context
contextIO := G.Of(t.Context())
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
result := ioAction()
assert.Equal(t, 42, result)
}
func TestReadIOWithChain(t *testing.T) {
// Test ReadIO with chained operations
contextIO := G.Of(context.WithValue(t.Context(), "multiplier", 3))
result := F.Pipe1(
FromReader(func(ctx context.Context) int {
if val := ctx.Value("multiplier"); val != nil {
return val.(int)
}
return 1
}),
Chain(func(n int) ReaderIO[int] {
return Of(n * 10)
}),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 30, value) // 3 * 10
}
func TestReadIOWithMap(t *testing.T) {
// Test ReadIO with Map operations
contextIO := G.Of(t.Context())
result := F.Pipe2(
Of(5),
Map(N.Mul(2)),
Map(N.Add(10)),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 20, value) // (5 * 2) + 10
}
func TestReadIOWithSideEffects(t *testing.T) {
// Test ReadIO with side effects in context creation
counter := 0
contextIO := func() context.Context {
counter++
return context.WithValue(t.Context(), "counter", counter)
}
rio := FromReader(func(ctx context.Context) int {
if val := ctx.Value("counter"); val != nil {
return val.(int)
}
return 0
})
ioAction := ReadIO[int](contextIO)(rio)
result := ioAction()
assert.Equal(t, 1, result)
assert.Equal(t, 1, counter)
}
func TestReadIOMultipleExecutions(t *testing.T) {
// Test that ReadIO creates fresh effects on each execution
counter := 0
contextIO := func() context.Context {
counter++
return t.Context()
}
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
result1 := ioAction()
result2 := ioAction()
assert.Equal(t, 42, result1)
assert.Equal(t, 42, result2)
assert.Equal(t, 2, counter) // Context IO executed twice
}
func TestReadIOComparisonWithRead(t *testing.T) {
// Compare ReadIO with Read to show the difference
ctx := context.WithValue(t.Context(), "key", "value")
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("key"); val != nil {
return val.(string)
}
return "default"
})
// Using Read (direct context)
ioAction1 := Read[string](ctx)(rio)
result1 := ioAction1()
// Using ReadIO (context wrapped in IO)
contextIO := G.Of(ctx)
ioAction2 := ReadIO[string](contextIO)(rio)
result2 := ioAction2()
assert.Equal(t, result1, result2)
assert.Equal(t, "value", result1)
assert.Equal(t, "value", result2)
}
func TestReadIOWithComplexContext(t *testing.T) {
// Test ReadIO with complex context manipulation
type contextKey string
const (
userKey contextKey = "user"
tokenKey contextKey = "token"
)
contextIO := G.Of(
context.WithValue(
context.WithValue(t.Context(), userKey, "Alice"),
tokenKey,
"secret123",
),
)
rio := FromReader(func(ctx context.Context) map[string]string {
result := make(map[string]string)
if user := ctx.Value(userKey); user != nil {
result["user"] = user.(string)
}
if token := ctx.Value(tokenKey); token != nil {
result["token"] = token.(string)
}
return result
})
ioAction := ReadIO[map[string]string](contextIO)(rio)
result := ioAction()
assert.Equal(t, "Alice", result["user"])
assert.Equal(t, "secret123", result["token"])
}
func TestReadIOWithAsk(t *testing.T) {
// Test ReadIO combined with Ask
contextIO := G.Of(context.WithValue(t.Context(), "data", 100))
result := F.Pipe1(
Ask(),
Map(func(ctx context.Context) int {
if val := ctx.Value("data"); val != nil {
return val.(int)
}
return 0
}),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 100, value)
}

View File

@@ -53,7 +53,7 @@ import (
// }
//
// countdown := TailRec(countdownStep)
// result := countdown(10)(context.Background())() // Returns "Done!"
// result := countdown(10)(t.Context())() // Returns "Done!"
//
// Example - Sum with context:
//
@@ -77,7 +77,7 @@ import (
// }
//
// sum := TailRec(sumStep)
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(context.Background())()
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(t.Context())()
// // Returns 15, safe even for very large slices
//
//go:inline

View File

@@ -80,7 +80,7 @@ import (
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute
// ctx := context.Background()
// ctx := t.Context()
// result := retryingFetch(ctx)() // Returns "success" after 3 attempts
//
//go:inline

View File

@@ -56,7 +56,7 @@ This creates several problems:
```go
computation := getComputation()
ctx := context.Background()
ctx := t.Context()
cfg := Config{Value: 42}
// Must apply in this specific order
@@ -176,7 +176,7 @@ db := Database{ConnectionString: "localhost:5432"}
query := queryWithDB(db) // ✅ Database injected
// Use query with any context
result := query(context.Background())()
result := query(t.Context())()
```
### 3. Point-Free Composition
@@ -289,7 +289,7 @@ withConfig := traversed(getValue)
// Now we can provide Config to get the final result
cfg := Config{Multiplier: 5, Prefix: "Result"}
ctx := context.Background()
ctx := t.Context()
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
```

View File

@@ -74,7 +74,7 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
// safeFetch := WithContextK(fetchUser)
//
// // If context is cancelled, returns immediately without executing fetchUser
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
// cancel() // Cancel immediately
// result := safeFetch(123)(ctx)() // Returns context.Canceled error
//

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/circuitbreaker"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/retry"
)
@@ -27,6 +28,9 @@ func MakeCircuitBreaker[T any](
Left,
ChainFirstIOK,
ChainFirstLeftIOK,
readerio.ChainFirstIOK,
FromIO,
Flap,
Flatten,

View File

@@ -187,7 +187,7 @@ func main() {
result := cb(env)
// Execute the protected operation
ctx := context.Background()
ctx := t.Context()
protectedOp := pair.Tail(result)
outcome := protectedOp(ctx)()
}

View File

@@ -113,7 +113,7 @@
// }
//
// // Execute the computation
// ctx := context.Background()
// ctx := t.Context()
// result := fetchUser("123")(ctx)()
// // result is Either[error, User]
//
@@ -161,7 +161,7 @@
// All operations respect context cancellation. When a context is cancelled, operations
// will return an error containing the cancellation cause:
//
// ctx, cancel := context.WithCancelCause(context.Background())
// ctx, cancel := context.WithCancelCause(t.Context())
// cancel(errors.New("operation cancelled"))
// result := computation(ctx)() // Returns Left with cancellation error
//

View File

@@ -37,7 +37,7 @@ import (
// return either.Eq(eq.FromEquals(func(x, y int) bool { return x == y }))(a, b)
// })
// eqRIE := Eq(eqInt)
// ctx := context.Background()
// ctx := t.Context()
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
//
//go:inline

View File

@@ -16,7 +16,6 @@
package file
import (
"context"
"os"
"testing"
@@ -30,7 +29,7 @@ func TestWithTempFile(t *testing.T) {
res := WithTempFile(onWriteAll[*os.File]([]byte("Carsten")))
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
}
func TestWithTempFileOnClosedFile(t *testing.T) {
@@ -43,5 +42,5 @@ func TestWithTempFileOnClosedFile(t *testing.T) {
)
})
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
}

View File

@@ -43,7 +43,7 @@ import (
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
//
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
// result := filter(readerioresult.Right(42))(context.Background())()
// result := filter(readerioresult.Right(42))(t.Context())()
//
//go:inline
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {

View File

@@ -71,7 +71,7 @@ import (
//
// // Now we can partially apply the Config
// cfg := Config{Timeout: 30}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(cfg)() // Returns Right(60)
//
// This is especially useful in point-free style when building computation pipelines:
@@ -133,7 +133,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
//
// // Partially apply the Database
// db := Database{ConnectionString: "localhost:5432"}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
//
// In point-free style, this enables clean composition:
@@ -195,7 +195,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R
//
// // Partially apply the Config
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(cfg)() // Returns Right(3)
//
// // With invalid config
@@ -276,7 +276,7 @@ func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kl
//
// // Now we can provide the Config to get the final result
// cfg := Config{Multiplier: 5}
// ctx := context.Background()
// ctx := t.Context()
// finalResult := result(cfg)(ctx)() // Returns Right(50)
//
// In point-free style, this enables clean composition:

View File

@@ -22,6 +22,7 @@ import (
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
@@ -233,7 +234,7 @@ func Example_sequenceReaderResult_errorHandling() {
ctx := context.Background()
pipeline := F.Pipe2(
sequenced(ctx),
RIOE.Map(func(x int) int { return x * 2 }),
RIOE.Map(N.Mul(2)),
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
return RIOE.Of(fmt.Sprintf("Result: %d", x))
}),

View File

@@ -41,7 +41,7 @@ func TestSequenceReader(t *testing.T) {
// The Reader environment (string) is now the first parameter
sequenced := SequenceReader(original)
ctx := context.Background()
ctx := t.Context()
// Test original
result1 := original(ctx)()
@@ -75,7 +75,7 @@ func TestSequenceReader(t *testing.T) {
}
db := Database{ConnectionString: "localhost:5432"}
ctx := context.Background()
ctx := t.Context()
expected := "Query on localhost:5432"
@@ -106,7 +106,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -132,7 +132,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Sequence
sequenced := SequenceReader(original)
@@ -158,7 +158,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReader(original)
// Test with zero values
@@ -184,7 +184,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReader(original)
@@ -217,14 +217,14 @@ func TestSequenceReader(t *testing.T) {
withConfig := sequenced(cfg)
// Now we have a ReaderIOResult[int] that can be used in different contexts
ctx1 := context.Background()
ctx1 := t.Context()
result1 := withConfig(ctx1)()
assert.True(t, either.IsRight(result1))
value1, _ := either.Unwrap(result1)
assert.Equal(t, 50, value1)
// Can reuse with different context
ctx2 := context.Background()
ctx2 := t.Context()
result2 := withConfig(ctx2)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
@@ -246,7 +246,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReaderIO(original)
// Test original
@@ -273,7 +273,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -303,7 +303,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReaderIO(original)
@@ -327,7 +327,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReaderResult(original)
// Test original
@@ -356,7 +356,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -384,7 +384,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with inner error
result1 := original(ctx)()
@@ -421,7 +421,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test outer error
sequenced1 := SequenceReaderResult(makeOriginal(-20))
@@ -460,7 +460,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReaderResult(original)
@@ -484,7 +484,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
empty := Empty{}
sequenced := SequenceReader(original)
@@ -514,7 +514,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
data := &Data{Value: 100}
sequenced := SequenceReader(original)
@@ -544,7 +544,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReader(original)
// Call multiple times with same inputs
@@ -583,7 +583,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -614,7 +614,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsLeft(finalResult))
@@ -643,7 +643,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Database and execute
db := Database{Prefix: "ID"}
ctx := context.Background()
ctx := t.Context()
finalResult := result(db)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -673,7 +673,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Settings and execute
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := context.Background()
ctx := t.Context()
finalResult := result(settings)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -705,14 +705,14 @@ func TestTraverseReader(t *testing.T) {
withConfig := result(cfg)
// Can now use with different contexts
ctx1 := context.Background()
ctx1 := t.Context()
finalResult1 := withConfig(ctx1)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)
assert.Equal(t, 30, value1)
// Reuse with different context
ctx2 := context.Background()
ctx2 := t.Context()
finalResult2 := withConfig(ctx2)()
assert.True(t, either.IsRight(finalResult2))
value2, _ := either.Unwrap(finalResult2)
@@ -746,7 +746,7 @@ func TestTraverseReader(t *testing.T) {
result := traversed(original)
// Use canceled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
cfg := Config{Value: 5}
@@ -778,7 +778,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config with zero offset
cfg := Config{Offset: 0}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -807,7 +807,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 4}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -843,7 +843,7 @@ func TestTraverseReader(t *testing.T) {
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
ctx := context.Background()
ctx := t.Context()
finalResult1 := result(rules1)(ctx)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)

View File

@@ -42,7 +42,7 @@
// )
//
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
package builder
import (
@@ -103,7 +103,7 @@ import (
// B.WithJSONBody(map[string]string{"name": "John"}),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
//
// Example without body:
//
@@ -113,7 +113,7 @@ import (
// B.WithMethod("GET"),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
func Requester(builder *R.Builder) RIOEH.Requester {
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {

View File

@@ -55,7 +55,7 @@ func TestBuilderWithQuery(t *testing.T) {
}),
)
assert.True(t, E.IsRight(req(context.Background())()))
assert.True(t, E.IsRight(req(t.Context())()))
}
// TestBuilderWithoutBody tests creating a request without a body
@@ -67,7 +67,7 @@ func TestBuilderWithoutBody(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -90,7 +90,7 @@ func TestBuilderWithBody(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -112,7 +112,7 @@ func TestBuilderWithHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -130,7 +130,7 @@ func TestBuilderWithInvalidURL(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
@@ -144,7 +144,7 @@ func TestBuilderWithEmptyMethod(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
// Empty method should still work (defaults to GET in http.NewRequest)
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -161,7 +161,7 @@ func TestBuilderWithMultipleHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -185,7 +185,7 @@ func TestBuilderWithBodyAndHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -207,7 +207,7 @@ func TestBuilderContextCancellation(t *testing.T) {
requester := Requester(builder)
// Create a cancelled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel() // Cancel immediately
result := requester(ctx)()
@@ -233,7 +233,7 @@ func TestBuilderWithDifferentMethods(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
@@ -256,7 +256,7 @@ func TestBuilderWithJSON(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -277,7 +277,7 @@ func TestBuilderWithBearer(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")

View File

@@ -28,7 +28,7 @@
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// result := ReadJSON[MyType](client)(request)
// response := result(context.Background())()
// response := result(t.Context())()
package http
import (
@@ -157,7 +157,7 @@ func MakeClient(httpClient *http.Client) Client {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// fullResp := ReadFullResponse(client)(request)
// result := fullResp(context.Background())()
// result := fullResp(t.Context())()
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
return F.Flow3(
@@ -194,7 +194,7 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// readBytes := ReadAll(client)
// result := readBytes(request)(context.Background())()
// result := readBytes(request)(t.Context())()
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
return F.Flow2(
ReadFullResponse(client),
@@ -218,7 +218,7 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/text")
// readText := ReadText(client)
// result := readText(request)(context.Background())()
// result := readText(request)(t.Context())()
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
return F.Flow2(
ReadAll(client),
@@ -277,7 +277,7 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/user/1")
// readUser := ReadJSON[User](client)
// result := readUser(request)(context.Background())()
// result := readUser(request)(t.Context())()
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
return F.Flow2(
readJSON(client),

View File

@@ -429,7 +429,7 @@ func LogEntryExitWithCallback[A any](
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
//
// // Execute
// result := loggedFetch(context.Background())()
// result := loggedFetch(t.Context())()
// // Logs:
// // [entering 1] fetchUser
// // [exiting 1] fetchUser [0.1s]
@@ -441,7 +441,7 @@ func LogEntryExitWithCallback[A any](
// }
//
// logged := LogEntryExit[string]("failingOp")(failingOp())
// result := logged(context.Background())()
// result := logged(t.Context())()
// // Logs:
// // [entering 2] failingOp
// // [throwing 2] failingOp [0.0s]: connection timeout
@@ -461,7 +461,7 @@ func LogEntryExitWithCallback[A any](
// LogEntryExit[[]Order]("fetchOrders"),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // Logs:
// // [entering 3] fetchUser
// // [exiting 3] fetchUser [0.1s]
@@ -474,8 +474,8 @@ func LogEntryExitWithCallback[A any](
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
//
// go op1(context.Background())()
// go op2(context.Background())()
// go op1(t.Context())()
// go op2(t.Context())()
// // Logs (order may vary):
// // [entering 5] operation1
// // [entering 6] operation2
@@ -615,7 +615,7 @@ func SLogWithCallback[A any](
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
// // If error, logs: "Fetched user" error="user not found"
//
@@ -679,7 +679,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Returns: result.Of("Alice")
//
@@ -694,7 +694,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// TapSLog[Payment]("Payment processed"),
// )
//
// result := processOrder(context.Background())()
// result := processOrder(t.Context())()
// // Logs each successful step with the intermediate values
// // If any step fails, subsequent TapSLog calls don't log
//

View File

@@ -26,7 +26,7 @@ func TestLoggingContext(t *testing.T) {
LogEntryExit[string]("TestLoggingContext2"),
)
assert.Equal(t, result.Of("Sample"), data(context.Background())())
assert.Equal(t, result.Of("Sample"), data(t.Context())())
}
// TestLogEntryExitSuccess tests successful operation logging
@@ -43,7 +43,7 @@ func TestLogEntryExitSuccess(t *testing.T) {
LogEntryExit[string]("TestOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of("success value"), res)
@@ -70,7 +70,7 @@ func TestLogEntryExitError(t *testing.T) {
LogEntryExit[string]("FailingOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsLeft(res))
@@ -105,7 +105,7 @@ func TestLogEntryExitNested(t *testing.T) {
}),
)
res := outerOp(context.Background())()
res := outerOp(t.Context())()
assert.True(t, result.IsRight(res))
@@ -137,7 +137,7 @@ func TestLogEntryExitWithCallback(t *testing.T) {
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of(42), res)
@@ -163,7 +163,7 @@ func TestLogEntryExitDisabled(t *testing.T) {
LogEntryExit[string]("DisabledOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
@@ -197,7 +197,7 @@ func TestLogEntryExitF(t *testing.T) {
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
@@ -234,7 +234,7 @@ func TestLogEntryExitFWithError(t *testing.T) {
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsLeft(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
@@ -257,7 +257,7 @@ func TestLoggingIDUniqueness(t *testing.T) {
Of(i),
LogEntryExit[int]("Operation"),
)
op(context.Background())()
op(t.Context())()
}
logOutput := buf.String()
@@ -287,7 +287,7 @@ func TestLogEntryExitWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
ctx := logging.WithLogger(contextLogger)(t.Context())
operation := F.Pipe1(
Of("context value"),
@@ -326,7 +326,7 @@ func TestLogEntryExitTiming(t *testing.T) {
LogEntryExit[string]("SlowOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
@@ -379,7 +379,7 @@ func TestLogEntryExitChainedOperations(t *testing.T) {
)),
)
res := pipeline(context.Background())()
res := pipeline(t.Context())()
assert.Equal(t, result.Of("2"), res)
@@ -408,7 +408,7 @@ func TestTapSLog(t *testing.T) {
Map(N.Mul(2)),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of(84), res)
@@ -443,7 +443,7 @@ func TestTapSLogInPipeline(t *testing.T) {
TapSLog[int]("Step 3: Final length"),
)
res := pipeline(context.Background())()
res := pipeline(t.Context())()
assert.Equal(t, result.Of(11), res)
@@ -472,7 +472,7 @@ func TestTapSLogWithError(t *testing.T) {
Map(N.Mul(2)),
)
res := pipeline(context.Background())()
res := pipeline(t.Context())()
assert.True(t, result.IsLeft(res))
@@ -504,7 +504,7 @@ func TestTapSLogWithStruct(t *testing.T) {
Map(func(u User) string { return u.Name }),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of("Alice"), res)
@@ -530,7 +530,7 @@ func TestTapSLogDisabled(t *testing.T) {
Map(N.Mul(2)),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of(84), res)
@@ -546,7 +546,7 @@ func TestTapSLogWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
ctx := logging.WithLogger(contextLogger)(t.Context())
operation := F.Pipe2(
Of("test value"),
@@ -572,7 +572,7 @@ func TestSLogLogsSuccessValue(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
// Create a Result and log it
res1 := result.Of(42)
@@ -594,7 +594,7 @@ func TestSLogLogsErrorValue(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("test error")
// Create an error Result and log it
@@ -620,7 +620,7 @@ func TestSLogWithCallbackCustomLevel(t *testing.T) {
return logger
}
ctx := context.Background()
ctx := t.Context()
// Create a Result and log it with custom callback
res1 := result.Of(42)
@@ -645,7 +645,7 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
return logger
}
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("warning error")
// Create an error Result and log it with custom callback

View File

@@ -43,7 +43,7 @@ func TestPromapBasic(t *testing.T) {
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of("42"), result)
})
@@ -67,7 +67,7 @@ func TestContramapBasic(t *testing.T) {
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of(100), result)
})
@@ -91,7 +91,7 @@ func TestLocalBasic(t *testing.T) {
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of("Alice"), result)
})

View File

@@ -914,6 +914,21 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}
//go:inline
func ReadIO[A any](r IO[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIO[A](r)
}
//go:inline
func ReadIOEither[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIOEither[A](r)
}
//go:inline
func ReadIOResult[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIOResult[A](r)
}
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type. If the input is a Right value, it passes through unchanged.
@@ -1026,7 +1041,7 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
// getUser,
// addUser,
// )
// value, err := result(context.Background())() // Returns ("Alice", nil)
// value, err := result(t.Context())() // Returns ("Alice", nil)
//
// Timeout Example:
//
@@ -1097,7 +1112,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// fetchData,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) after 5s
//
// Successful Example:
//
@@ -1106,7 +1121,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// quickFetch,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
@@ -1158,12 +1173,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// fetchData,
// readerioresult.WithDeadline[Data](deadline),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)

View File

@@ -36,56 +36,56 @@ func TestFromEither(t *testing.T) {
t.Run("Right value", func(t *testing.T) {
either := E.Right[error]("success")
result := FromEither(either)
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
})
t.Run("Left value", func(t *testing.T) {
err := errors.New("test error")
either := E.Left[string](err)
result := FromEither(either)
assert.Equal(t, E.Left[string](err), result(context.Background())())
assert.Equal(t, E.Left[string](err), result(t.Context())())
})
}
func TestFromResult(t *testing.T) {
t.Run("Success", func(t *testing.T) {
result := FromResult(E.Right[error](42))
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("Error", func(t *testing.T) {
err := errors.New("test error")
result := FromResult(E.Left[int](err))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestLeft(t *testing.T) {
err := errors.New("test error")
result := Left[string](err)
assert.Equal(t, E.Left[string](err), result(context.Background())())
assert.Equal(t, E.Left[string](err), result(t.Context())())
}
func TestRight(t *testing.T) {
result := Right("success")
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
}
func TestOf(t *testing.T) {
result := Of(42)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestMonadMap(t *testing.T) {
t.Run("Map over Right", func(t *testing.T) {
result := MonadMap(Of(5), N.Mul(2))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("Map over Left", func(t *testing.T) {
err := errors.New("test error")
result := MonadMap(Left[int](err), N.Mul(2))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -93,34 +93,34 @@ func TestMap(t *testing.T) {
t.Run("Map with success", func(t *testing.T) {
mapper := Map(N.Mul(2))
result := mapper(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("Map with error", func(t *testing.T) {
err := errors.New("test error")
mapper := Map(N.Mul(2))
result := mapper(Left[int](err))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("MapTo with success", func(t *testing.T) {
result := MonadMapTo(Of("original"), 42)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("MapTo with error", func(t *testing.T) {
err := errors.New("test error")
result := MonadMapTo(Left[string](err), 42)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestMapTo(t *testing.T) {
mapper := MapTo[string](42)
result := mapper(Of("original"))
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestMonadChain(t *testing.T) {
@@ -128,7 +128,7 @@ func TestMonadChain(t *testing.T) {
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("Chain with error in first", func(t *testing.T) {
@@ -136,7 +136,7 @@ func TestMonadChain(t *testing.T) {
result := MonadChain(Left[int](err), func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
t.Run("Chain with error in second", func(t *testing.T) {
@@ -144,7 +144,7 @@ func TestMonadChain(t *testing.T) {
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
return Left[int](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -153,7 +153,7 @@ func TestChain(t *testing.T) {
return Of(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestMonadChainFirst(t *testing.T) {
@@ -161,7 +161,7 @@ func TestMonadChainFirst(t *testing.T) {
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
return Of("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
})
t.Run("ChainFirst propagates error from second", func(t *testing.T) {
@@ -169,7 +169,7 @@ func TestMonadChainFirst(t *testing.T) {
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
return Left[string](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -178,7 +178,7 @@ func TestChainFirst(t *testing.T) {
return Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestMonadApSeq(t *testing.T) {
@@ -186,7 +186,7 @@ func TestMonadApSeq(t *testing.T) {
fab := Of(N.Mul(2))
fa := Of(5)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ApSeq with error in function", func(t *testing.T) {
@@ -194,7 +194,7 @@ func TestMonadApSeq(t *testing.T) {
fab := Left[func(int) int](err)
fa := Of(5)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
t.Run("ApSeq with error in value", func(t *testing.T) {
@@ -202,7 +202,7 @@ func TestMonadApSeq(t *testing.T) {
fab := Of(N.Mul(2))
fa := Left[int](err)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -210,7 +210,7 @@ func TestApSeq(t *testing.T) {
fa := Of(5)
fab := Of(N.Mul(2))
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestApPar(t *testing.T) {
@@ -218,11 +218,11 @@ func TestApPar(t *testing.T) {
fa := Of(5)
fab := Of(N.Mul(2))
result := MonadApPar(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ApPar with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
fa := Of(5)
fab := Of(N.Mul(2))
@@ -239,7 +239,7 @@ func TestFromPredicate(t *testing.T) {
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
)
result := pred(5)
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
})
t.Run("Predicate false", func(t *testing.T) {
@@ -248,7 +248,7 @@ func TestFromPredicate(t *testing.T) {
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
)
result := pred(-5)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsLeft(res))
})
}
@@ -259,7 +259,7 @@ func TestOrElse(t *testing.T) {
return Of(42)
})
result := fallback(Of(10))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("OrElse with error", func(t *testing.T) {
@@ -268,13 +268,13 @@ func TestOrElse(t *testing.T) {
return Of(42)
})
result := fallback(Left[int](err))
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
}
func TestAsk(t *testing.T) {
result := Ask()
ctx := context.Background()
ctx := t.Context()
res := result(ctx)()
assert.True(t, E.IsRight(res))
ctxResult := E.ToOption(res)
@@ -286,7 +286,7 @@ func TestMonadChainEitherK(t *testing.T) {
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
return E.Right[error](x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ChainEitherK with error", func(t *testing.T) {
@@ -294,7 +294,7 @@ func TestMonadChainEitherK(t *testing.T) {
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
return E.Left[int](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -303,7 +303,7 @@ func TestChainEitherK(t *testing.T) {
return E.Right[error](x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestMonadChainFirstEitherK(t *testing.T) {
@@ -311,7 +311,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
return E.Right[error]("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
})
t.Run("ChainFirstEitherK propagates error", func(t *testing.T) {
@@ -319,7 +319,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
return E.Left[string](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -328,7 +328,7 @@ func TestChainFirstEitherK(t *testing.T) {
return E.Right[error]("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestChainOptionK(t *testing.T) {
@@ -339,7 +339,7 @@ func TestChainOptionK(t *testing.T) {
return O.Some(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ChainOptionK with None", func(t *testing.T) {
@@ -349,7 +349,7 @@ func TestChainOptionK(t *testing.T) {
return O.None[int]()
})
result := chainer(Of(5))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsLeft(res))
})
}
@@ -358,44 +358,44 @@ func TestFromIOEither(t *testing.T) {
t.Run("FromIOEither with success", func(t *testing.T) {
ioe := IOE.Of[error](42)
result := FromIOEither(ioe)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("FromIOEither with error", func(t *testing.T) {
err := errors.New("test error")
ioe := IOE.Left[int](err)
result := FromIOEither(ioe)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestFromIOResult(t *testing.T) {
ioe := IOE.Of[error](42)
result := FromIOResult(ioe)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestFromIO(t *testing.T) {
io := IOG.Of(42)
result := FromIO(io)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestFromReader(t *testing.T) {
reader := R.Of[context.Context](42)
result := FromReader(reader)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
result := FromLazy(lazy)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestNever(t *testing.T) {
t.Run("Never with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
result := Never[int]()
// Cancel immediately
@@ -406,7 +406,7 @@ func TestNever(t *testing.T) {
})
t.Run("Never with timeout", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
result := Never[int]()
@@ -419,7 +419,7 @@ func TestMonadChainIOK(t *testing.T) {
result := MonadChainIOK(Of(5), func(x int) IOG.IO[int] {
return IOG.Of(x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestChainIOK(t *testing.T) {
@@ -427,14 +427,14 @@ func TestChainIOK(t *testing.T) {
return IOG.Of(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestMonadChainFirstIOK(t *testing.T) {
result := MonadChainFirstIOK(Of(5), func(x int) IOG.IO[string] {
return IOG.Of("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestChainFirstIOK(t *testing.T) {
@@ -442,7 +442,7 @@ func TestChainFirstIOK(t *testing.T) {
return IOG.Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestChainIOEitherK(t *testing.T) {
@@ -451,7 +451,7 @@ func TestChainIOEitherK(t *testing.T) {
return IOE.Of[error](x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ChainIOEitherK with error", func(t *testing.T) {
@@ -460,7 +460,7 @@ func TestChainIOEitherK(t *testing.T) {
return IOE.Left[int](err)
})
result := chainer(Of(5))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -469,7 +469,7 @@ func TestDelay(t *testing.T) {
start := time.Now()
delayed := Delay[int](100 * time.Millisecond)
result := delayed(Of(42))
res := result(context.Background())()
res := result(t.Context())()
elapsed := time.Since(start)
assert.True(t, E.IsRight(res))
@@ -477,7 +477,7 @@ func TestDelay(t *testing.T) {
})
t.Run("Delay with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
delayed := Delay[int](100 * time.Millisecond)
result := delayed(Of(42))
@@ -500,11 +500,11 @@ func TestDefer(t *testing.T) {
})
// First execution
res1 := deferred(context.Background())()
res1 := deferred(t.Context())()
assert.True(t, E.IsRight(res1))
// Second execution should generate a new computation
res2 := deferred(context.Background())()
res2 := deferred(t.Context())()
assert.True(t, E.IsRight(res2))
// Counter should be incremented for each execution
@@ -518,7 +518,7 @@ func TestTryCatch(t *testing.T) {
return 42, nil
}
})
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("TryCatch with error", func(t *testing.T) {
@@ -528,7 +528,7 @@ func TestTryCatch(t *testing.T) {
return 0, err
}
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -537,7 +537,7 @@ func TestMonadAlt(t *testing.T) {
first := Of(42)
second := func() ReaderIOResult[int] { return Of(100) }
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("Alt with first error", func(t *testing.T) {
@@ -545,7 +545,7 @@ func TestMonadAlt(t *testing.T) {
first := Left[int](err)
second := func() ReaderIOResult[int] { return Of(100) }
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](100), result(context.Background())())
assert.Equal(t, E.Right[error](100), result(t.Context())())
})
}
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
err := errors.New("test error")
alternative := Alt(func() ReaderIOResult[int] { return Of(100) })
result := alternative(Left[int](err))
assert.Equal(t, E.Right[error](100), result(context.Background())())
assert.Equal(t, E.Right[error](100), result(t.Context())())
}
func TestMemoize(t *testing.T) {
@@ -564,13 +564,13 @@ func TestMemoize(t *testing.T) {
}))
// First execution
res1 := computation(context.Background())()
res1 := computation(t.Context())()
assert.True(t, E.IsRight(res1))
val1 := E.ToOption(res1)
assert.Equal(t, O.Of(1), val1)
// Second execution should return cached value
res2 := computation(context.Background())()
res2 := computation(t.Context())()
assert.True(t, E.IsRight(res2))
val2 := E.ToOption(res2)
assert.Equal(t, O.Of(1), val2)
@@ -582,19 +582,19 @@ func TestMemoize(t *testing.T) {
func TestFlatten(t *testing.T) {
nested := Of(Of(42))
result := Flatten(nested)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestMonadFlap(t *testing.T) {
fab := Of(N.Mul(2))
result := MonadFlap(fab, 5)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestFlap(t *testing.T) {
flapper := Flap[int](5)
result := flapper(Of(N.Mul(2)))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestFold(t *testing.T) {
@@ -608,7 +608,7 @@ func TestFold(t *testing.T) {
},
)
result := folder(Of(42))
assert.Equal(t, E.Right[error]("success: 42"), result(context.Background())())
assert.Equal(t, E.Right[error]("success: 42"), result(t.Context())())
})
t.Run("Fold with error", func(t *testing.T) {
@@ -622,7 +622,7 @@ func TestFold(t *testing.T) {
},
)
result := folder(Left[int](err))
assert.Equal(t, E.Right[error]("error: test error"), result(context.Background())())
assert.Equal(t, E.Right[error]("error: test error"), result(t.Context())())
})
}
@@ -634,7 +634,7 @@ func TestGetOrElse(t *testing.T) {
}
})
result := getter(Of(42))
assert.Equal(t, 42, result(context.Background())())
assert.Equal(t, 42, result(t.Context())())
})
t.Run("GetOrElse with error", func(t *testing.T) {
@@ -645,19 +645,19 @@ func TestGetOrElse(t *testing.T) {
}
})
result := getter(Left[int](err))
assert.Equal(t, 0, result(context.Background())())
assert.Equal(t, 0, result(t.Context())())
})
}
func TestWithContext(t *testing.T) {
t.Run("WithContext with valid context", func(t *testing.T) {
computation := WithContext(Of(42))
result := computation(context.Background())()
result := computation(t.Context())()
assert.Equal(t, E.Right[error](42), result)
})
t.Run("WithContext with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
computation := WithContext(Of(42))
@@ -672,7 +672,7 @@ func TestEitherize0(t *testing.T) {
}
eitherized := Eitherize0(f)
result := eitherized()
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestUneitherize0(t *testing.T) {
@@ -680,7 +680,7 @@ func TestUneitherize0(t *testing.T) {
return Of(42)
}
uneitherized := Uneitherize0(f)
result, err := uneitherized(context.Background())
result, err := uneitherized(t.Context())
assert.NoError(t, err)
assert.Equal(t, 42, result)
}
@@ -691,7 +691,7 @@ func TestEitherize1(t *testing.T) {
}
eitherized := Eitherize1(f)
result := eitherized(5)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestUneitherize1(t *testing.T) {
@@ -699,14 +699,14 @@ func TestUneitherize1(t *testing.T) {
return Of(x * 2)
}
uneitherized := Uneitherize1(f)
result, err := uneitherized(context.Background(), 5)
result, err := uneitherized(t.Context(), 5)
assert.NoError(t, err)
assert.Equal(t, 10, result)
}
func TestSequenceT2(t *testing.T) {
result := SequenceT2(Of(1), Of(2))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
tuple := E.ToOption(res)
assert.True(t, O.IsSome(tuple))
@@ -717,13 +717,13 @@ func TestSequenceT2(t *testing.T) {
func TestSequenceSeqT2(t *testing.T) {
result := SequenceSeqT2(Of(1), Of(2))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
}
func TestSequenceParT2(t *testing.T) {
result := SequenceParT2(Of(1), Of(2))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
}
@@ -734,7 +734,7 @@ func TestTraverseArray(t *testing.T) {
return Of(x * 2)
})
result := traverser(arr)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
@@ -750,7 +750,7 @@ func TestTraverseArray(t *testing.T) {
return Of(x * 2)
})
result := traverser(arr)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsLeft(res))
})
}
@@ -758,7 +758,7 @@ func TestTraverseArray(t *testing.T) {
func TestSequenceArray(t *testing.T) {
arr := []ReaderIOResult[int]{Of(1), Of(2), Of(3)}
result := SequenceArray(arr)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
@@ -769,7 +769,7 @@ func TestTraverseRecord(t *testing.T) {
result := TraverseRecord[string](func(x int) ReaderIOResult[int] {
return Of(x * 2)
})(rec)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
recOpt := E.ToOption(res)
assert.True(t, O.IsSome(recOpt))
@@ -784,7 +784,7 @@ func TestSequenceRecord(t *testing.T) {
"b": Of(2),
}
result := SequenceRecord(rec)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
recOpt := E.ToOption(res)
assert.True(t, O.IsSome(recOpt))
@@ -798,7 +798,7 @@ func TestAltSemigroup(t *testing.T) {
err := errors.New("test error")
result := sg.Concat(Left[int](err), Of(42))
res := result(context.Background())()
res := result(t.Context())()
assert.Equal(t, E.Right[error](42), res)
}
@@ -810,7 +810,7 @@ func TestApplicativeMonoid(t *testing.T) {
))
result := intAddMonoid.Concat(Of(5), Of(10))
res := result(context.Background())()
res := result(t.Context())()
assert.Equal(t, E.Right[error](15), res)
}
@@ -835,7 +835,7 @@ func TestBracket(t *testing.T) {
}
result := Bracket(acquire, use, release)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, acquired)
assert.True(t, released)
@@ -863,7 +863,7 @@ func TestBracket(t *testing.T) {
}
result := Bracket(acquire, use, release)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, acquired)
assert.True(t, released)

View File

@@ -29,7 +29,7 @@ import (
func TestInnerContextCancelSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, parentCancel := context.WithCancel(outer)
defer parentCancel()
@@ -49,7 +49,7 @@ func TestInnerContextCancelSemantics(t *testing.T) {
func TestOuterContextCancelSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, outerCancel := context.WithCancel(outer)
defer outerCancel()
@@ -69,7 +69,7 @@ func TestOuterContextCancelSemantics(t *testing.T) {
func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, outerCancel := context.WithCancel(outer)
defer outerCancel()
@@ -95,7 +95,7 @@ func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
func TestCancelCauseSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, outerCancel := context.WithCancelCause(outer)
defer outerCancel(nil)
@@ -119,7 +119,7 @@ func TestCancelCauseSemantics(t *testing.T) {
func TestTimer(t *testing.T) {
delta := 3 * time.Second
timer := Timer(delta)
ctx := context.Background()
ctx := t.Context()
t0 := time.Now()
res := timer(ctx)()
@@ -146,7 +146,7 @@ func TestCanceledApply(t *testing.T) {
Ap[string](errValue),
)
res := applied(context.Background())()
res := applied(t.Context())()
assert.Equal(t, E.Left[string](err), res)
}
@@ -159,7 +159,7 @@ func TestRegularApply(t *testing.T) {
Ap[string](value),
)
res := applied(context.Background())()
res := applied(t.Context())()
assert.Equal(t, E.Of[error]("CARSTEN"), res)
}
@@ -187,7 +187,7 @@ func TestWithResourceNoErrors(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 1, countAcquire)
assert.Equal(t, 1, countBody)
@@ -217,7 +217,7 @@ func TestWithResourceErrorInBody(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 1, countAcquire)
assert.Equal(t, 0, countBody)
@@ -247,7 +247,7 @@ func TestWithResourceErrorInAcquire(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 0, countAcquire)
assert.Equal(t, 0, countBody)
@@ -277,7 +277,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 1, countAcquire)
assert.Equal(t, 1, countBody)
@@ -286,7 +286,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
}
func TestMonadChainFirstLeft(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
@@ -353,7 +353,7 @@ func TestMonadChainFirstLeft(t *testing.T) {
}
func TestChainFirstLeft(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {

View File

@@ -108,7 +108,7 @@ import (
// countdown := readerioresult.TailRec(countdownStep)
//
// // With cancellation
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
// defer cancel()
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
//
@@ -141,7 +141,7 @@ import (
// }
//
// processFiles := readerioresult.TailRec(processStep)
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
//
// // Can be cancelled at any point during processing
// go func() {
@@ -159,7 +159,7 @@ import (
//
// // Safe for very large inputs with cancellation support
// largeCountdown := readerioresult.TailRec(countdownStep)
// ctx := context.Background()
// ctx := t.Context()
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
//
// # Performance Considerations

View File

@@ -44,7 +44,7 @@ func TestTailRec_BasicRecursion(t *testing.T) {
}
countdown := TailRec(countdownStep)
result := countdown(5)(context.Background())()
result := countdown(5)(t.Context())()
assert.Equal(t, E.Of[error]("Done!"), result)
}
@@ -71,7 +71,7 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
}
factorial := TailRec(factorialStep)
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
result := factorial(FactorialState{n: 5, acc: 1})(t.Context())()
assert.Equal(t, E.Of[error](120), result) // 5! = 120
}
@@ -95,7 +95,7 @@ func TestTailRec_ErrorHandling(t *testing.T) {
}
errorRecursion := TailRec(errorStep)
result := errorRecursion(5)(context.Background())()
result := errorRecursion(5)(t.Context())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
@@ -125,7 +125,7 @@ func TestTailRec_ContextCancellation(t *testing.T) {
slowRecursion := TailRec(slowStep)
// Create a context that will be cancelled after 100ms
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
start := time.Now()
@@ -159,7 +159,7 @@ func TestTailRec_ImmediateCancellation(t *testing.T) {
countdown := TailRec(countdownStep)
// Create an already cancelled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := countdown(5)(ctx)()
@@ -186,7 +186,7 @@ func TestTailRec_StackSafety(t *testing.T) {
}
countdown := TailRec(countdownStep)
result := countdown(largeN)(context.Background())()
result := countdown(largeN)(t.Context())()
assert.Equal(t, E.Of[error](0), result)
}
@@ -217,7 +217,7 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
countdown := TailRec(countdownStep)
// Cancel after 50ms to allow some iterations but not all
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
defer cancel()
result := countdown(largeN)(ctx)()
@@ -274,7 +274,7 @@ func TestTailRec_ComplexState(t *testing.T) {
errors: []error{},
}
result := processItems(initialState)(context.Background())()
result := processItems(initialState)(t.Context())()
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
})
@@ -286,7 +286,7 @@ func TestTailRec_ComplexState(t *testing.T) {
errors: []error{},
}
result := processItems(initialState)(context.Background())()
result := processItems(initialState)(t.Context())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
@@ -336,7 +336,7 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
}
// Cancel after 100ms (should allow ~5 files to be processed)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
start := time.Now()
@@ -366,7 +366,7 @@ func TestTailRec_ZeroIterations(t *testing.T) {
}
immediate := TailRec(immediateStep)
result := immediate(100)(context.Background())()
result := immediate(100)(t.Context())()
assert.Equal(t, E.Of[error]("immediate"), result)
}
@@ -392,7 +392,7 @@ func TestTailRec_ContextWithDeadline(t *testing.T) {
slowRecursion := TailRec(slowStep)
// Set deadline 80ms from now
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(80*time.Millisecond))
defer cancel()
result := slowRecursion(10)(ctx)()
@@ -427,7 +427,7 @@ func TestTailRec_ContextWithValue(t *testing.T) {
}
valueRecursion := TailRec(valueStep)
ctx := context.WithValue(context.Background(), testKey, "test-value")
ctx := context.WithValue(t.Context(), testKey, "test-value")
result := valueRecursion(3)(ctx)()
assert.Equal(t, E.Of[error]("Done!"), result)

View File

@@ -107,7 +107,7 @@ import (
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
// defer cancel()
// ioResult := retryingFetch(ctx)
// finalResult := ioResult()

View File

@@ -113,7 +113,7 @@ type (
// }
//
// The computation is executed by providing a context and then invoking the result:
// ctx := context.Background()
// ctx := t.Context()
// result := fetchUser("123")(ctx)()
ReaderIOResult[A any] = RIOR.ReaderIOResult[context.Context, A]

View File

@@ -0,0 +1,453 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
"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/reader"
"github.com/IBM/fp-go/v2/readerio"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition with two reader contexts.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// type OuterEnv struct {
// Database string
// }
// type InnerEnv struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
// result := readerreaderioeither.Do[OuterEnv, InnerEnv, error](State{})
//
//go:inline
func Do[R, S any](
empty S,
) ReaderReaderIOResult[R, S] {
return Of[R](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access both the outer (R) and inner (C) reader environments.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// type OuterEnv struct {
// Database string
// }
// type InnerEnv struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
//
// result := F.Pipe2(
// readerreaderioeither.Do[OuterEnv, InnerEnv, error](State{}),
// readerreaderioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerreaderioeither.ReaderReaderIOResult[OuterEnv, InnerEnv, error, User] {
// return func(outer OuterEnv) readerioeither.ReaderIOEither[InnerEnv, error, User] {
// return readerioeither.Asks(func(inner InnerEnv) ioeither.IOEither[error, User] {
// return inner.UserRepo.FindUser(outer.Database)
// })
// }
// },
// ),
// readerreaderioeither.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerreaderioeither.ReaderReaderIOResult[OuterEnv, InnerEnv, error, []Post] {
// return func(outer OuterEnv) readerioeither.ReaderIOEither[InnerEnv, error, []Post] {
// return readerioeither.Asks(func(inner InnerEnv) ioeither.IOEither[error, []Post] {
// return inner.PostRepo.FindPostsByUser(outer.Database, s.User.ID)
// })
// }
// },
// ),
// )
//
//go:inline
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderReaderIOResult[R, T],
) Operator[R, S1, S2] {
return chain.Bind(
Chain[R, S1, S2],
Map[R, T, S2],
setter,
f,
)
}
// Let attaches a pure computation result to a context [S1] to produce a context [S2].
// Unlike [Bind], the computation function f is pure (doesn't perform effects).
//
// Example:
//
// readerreaderioeither.Let(
// func(fullName string) func(State) State {
// return func(s State) State { s.FullName = fullName; return s }
// },
// func(s State) string {
// return s.FirstName + " " + s.LastName
// },
// )
//
//go:inline
func Let[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[R, S1, S2] {
return functor.Let(
Map[R, S1, S2],
setter,
f,
)
}
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
//
// Example:
//
// readerreaderioeither.LetTo(
// func(status string) func(State) State {
// return func(s State) State { s.Status = status; return s }
// },
// "active",
// )
//
//go:inline
func LetTo[R, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[R, S1, S2] {
return functor.LetTo(
Map[R, S1, S2],
setter,
b,
)
}
// BindTo wraps a value of type T into a context S1 using the provided setter function.
// This is typically used as the first operation after [Do] to initialize the context.
//
// Example:
//
// F.Pipe1(
// readerreaderioeither.Of[OuterEnv, InnerEnv, error](42),
// readerreaderioeither.BindTo(func(n int) State { return State{Count: n} }),
// )
//
//go:inline
func BindTo[R, S1, T any](
setter func(T) S1,
) Operator[R, T, S1] {
return chain.BindTo(
Map[R, T, S1],
setter,
)
}
// ApS applies a computation in parallel (applicative style) and attaches its result to the context.
// Unlike [Bind], this doesn't allow the computation to depend on the current context state.
//
// Example:
//
// readerreaderioeither.ApS(
// func(count int) func(State) State {
// return func(s State) State { s.Count = count; return s }
// },
// getCount, // ReaderReaderIOResult[OuterEnv, InnerEnv, error, int]
// )
//
//go:inline
func ApS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderReaderIOResult[R, T],
) Operator[R, S1, S2] {
return apply.ApS(
Ap[S2, R, T],
Map[R, S1, func(T) S2],
setter,
fa,
)
}
// ApSL is a lens-based version of [ApS] that uses a lens to focus on a specific field in the context.
//
//go:inline
func ApSL[R, S, T any](
lens Lens[S, T],
fa ReaderReaderIOResult[R, T],
) Operator[R, S, S] {
return ApS(lens.Set, fa)
}
// BindL is a lens-based version of [Bind] that uses a lens to focus on a specific field in the context.
//
//go:inline
func BindL[R, S, T any](
lens Lens[S, T],
f func(T) ReaderReaderIOResult[R, T],
) Operator[R, S, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
}
// LetL is a lens-based version of [Let] that uses a lens to focus on a specific field in the context.
//
//go:inline
func LetL[R, S, T any](
lens Lens[S, T],
f func(T) T,
) Operator[R, S, S] {
return Let[R](lens.Set, F.Flow2(lens.Get, f))
}
// LetToL is a lens-based version of [LetTo] that uses a lens to focus on a specific field in the context.
//
//go:inline
func LetToL[R, S, T any](
lens Lens[S, T],
b T,
) Operator[R, S, S] {
return LetTo[R](lens.Set, b)
}
// BindIOEitherK binds a computation that returns an IOEither to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindIOEitherK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f ioeither.Kleisli[error, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromIOEither[R, T]))
}
//go:inline
func BindIOResultK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f ioresult.Kleisli[S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromIOResult[R, T]))
}
// BindIOK binds a computation that returns an IO to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindIOK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f io.Kleisli[S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromIO[R, T]))
}
// BindReaderK binds a computation that returns a Reader to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindReaderK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[R, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromReader[R, T]))
}
// BindReaderIOK binds a computation that returns a ReaderIO to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindReaderIOK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[R, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromReaderIO[R, T]))
}
// BindEitherK binds a computation that returns an Either to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindEitherK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f either.Kleisli[error, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromEither[R, T]))
}
// BindIOEitherKL is a lens-based version of [BindIOEitherK].
//
//go:inline
func BindIOEitherKL[R, S, T any](
lens Lens[S, T],
f ioeither.Kleisli[error, T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[R, T]))
}
// BindIOKL is a lens-based version of [BindIOK].
//
//go:inline
func BindIOKL[R, S, T any](
lens Lens[S, T],
f io.Kleisli[T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromIO[R, T]))
}
// BindReaderKL is a lens-based version of [BindReaderK].
//
//go:inline
func BindReaderKL[R, S, T any](
lens Lens[S, T],
f reader.Kleisli[R, T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromReader[R, T]))
}
// BindReaderIOKL is a lens-based version of [BindReaderIOK].
//
//go:inline
func BindReaderIOKL[R, S, T any](
lens Lens[S, T],
f readerio.Kleisli[R, T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromReaderIO[R, T]))
}
// ApIOEitherS applies an IOEither computation and attaches its result to the context.
//
//go:inline
func ApIOEitherS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa IOEither[error, T],
) Operator[R, S1, S2] {
return ApS(setter, FromIOEither[R](fa))
}
// ApIOS applies an IO computation and attaches its result to the context.
//
//go:inline
func ApIOS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa IO[T],
) Operator[R, S1, S2] {
return ApS(setter, FromIO[R](fa))
}
// ApReaderS applies a Reader computation and attaches its result to the context.
//
//go:inline
func ApReaderS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[R, T],
) Operator[R, S1, S2] {
return ApS(setter, FromReader(fa))
}
// ApReaderIOS applies a ReaderIO computation and attaches its result to the context.
//
//go:inline
func ApReaderIOS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[R, T],
) Operator[R, S1, S2] {
return ApS(setter, FromReaderIO(fa))
}
// ApEitherS applies an Either computation and attaches its result to the context.
//
//go:inline
func ApEitherS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa Either[error, T],
) Operator[R, S1, S2] {
return ApS(setter, FromEither[R](fa))
}
// ApIOEitherSL is a lens-based version of [ApIOEitherS].
//
//go:inline
func ApIOEitherSL[R, S, T any](
lens Lens[S, T],
fa IOEither[error, T],
) Operator[R, S, S] {
return ApIOEitherS[R](lens.Set, fa)
}
// ApIOSL is a lens-based version of [ApIOS].
//
//go:inline
func ApIOSL[R, S, T any](
lens Lens[S, T],
fa IO[T],
) Operator[R, S, S] {
return ApSL(lens, FromIO[R](fa))
}
// ApReaderSL is a lens-based version of [ApReaderS].
//
//go:inline
func ApReaderSL[R, S, T any](
lens Lens[S, T],
fa Reader[R, T],
) Operator[R, S, S] {
return ApReaderS(lens.Set, fa)
}
// ApReaderIOSL is a lens-based version of [ApReaderIOS].
//
//go:inline
func ApReaderIOSL[R, S, T any](
lens Lens[S, T],
fa ReaderIO[R, T],
) Operator[R, S, S] {
return ApReaderIOS(lens.Set, fa)
}
// ApEitherSL is a lens-based version of [ApEitherS].
//
//go:inline
func ApEitherSL[R, S, T any](
lens Lens[S, T],
fa Either[error, T],
) Operator[R, S, S] {
return ApEitherS[R](lens.Set, fa)
}

View File

@@ -0,0 +1,720 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"errors"
"testing"
"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"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type AppConfig struct {
DatabaseURL string
LogLevel string
}
var defaultConfig = AppConfig{
DatabaseURL: "postgres://localhost",
LogLevel: "info",
}
func getLastName(s utils.Initial) ReaderReaderIOResult[AppConfig, string] {
return Of[AppConfig]("Doe")
}
func getGivenName(s utils.WithLastName) ReaderReaderIOResult[AppConfig, string] {
return Of[AppConfig]("John")
}
func TestDo(t *testing.T) {
res := Do[AppConfig](utils.Empty)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsRight(outcome))
assert.Equal(t, result.Of(utils.Empty), outcome)
}
func TestBind(t *testing.T) {
res := F.Pipe3(
Do[AppConfig](utils.Empty),
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("John Doe"), outcome)
}
func TestBindWithError(t *testing.T) {
testErr := errors.New("bind error")
getLastNameErr := func(s utils.Initial) ReaderReaderIOResult[AppConfig, string] {
return Left[AppConfig, string](testErr)
}
res := F.Pipe3(
Do[AppConfig](utils.Empty),
Bind(utils.SetLastName, getLastNameErr),
Bind(utils.SetGivenName, getGivenName),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestLet(t *testing.T) {
type State struct {
FirstName string
LastName string
FullName string
}
res := F.Pipe2(
Do[AppConfig](State{FirstName: "John", LastName: "Doe"}),
Let[AppConfig](
func(fullName string) func(State) State {
return func(s State) State {
s.FullName = fullName
return s
}
},
func(s State) string {
return s.FirstName + " " + s.LastName
},
),
Map[AppConfig](func(s State) string { return s.FullName }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("John Doe"), outcome)
}
func TestLetTo(t *testing.T) {
type State struct {
Status string
}
res := F.Pipe2(
Do[AppConfig](State{}),
LetTo[AppConfig](
func(status string) func(State) State {
return func(s State) State {
s.Status = status
return s
}
},
"active",
),
Map[AppConfig](func(s State) string { return s.Status }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("active"), outcome)
}
func TestBindTo(t *testing.T) {
type State struct {
Count int
}
res := F.Pipe2(
Of[AppConfig](42),
BindTo[AppConfig](func(n int) State { return State{Count: n} }),
Map[AppConfig](func(s State) int { return s.Count }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestApS(t *testing.T) {
res := F.Pipe3(
Do[AppConfig](utils.Empty),
ApS(utils.SetLastName, Of[AppConfig]("Doe")),
ApS(utils.SetGivenName, Of[AppConfig]("John")),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("John Doe"), outcome)
}
func TestApSWithError(t *testing.T) {
testErr := errors.New("aps error")
res := F.Pipe3(
Do[AppConfig](utils.Empty),
ApS(utils.SetLastName, Left[AppConfig, string](testErr)),
ApS(utils.SetGivenName, Of[AppConfig]("John")),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestBindReaderK(t *testing.T) {
type State struct {
Config string
}
getConfigPath := func(s State) func(AppConfig) string {
return func(cfg AppConfig) string {
return cfg.DatabaseURL
}
}
res := F.Pipe2(
Do[AppConfig](State{}),
BindReaderK(
func(path string) func(State) State {
return func(s State) State {
s.Config = path
return s
}
},
getConfigPath,
),
Map[AppConfig](func(s State) string { return s.Config }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("postgres://localhost"), outcome)
}
func TestBindIOResultK(t *testing.T) {
type State struct {
Value int
ParsedValue int
}
parseValue := func(s State) ioresult.IOResult[int] {
return func() result.Result[int] {
if s.Value < 0 {
return result.Left[int](errors.New("negative value"))
}
return result.Of(s.Value * 2)
}
}
t.Run("success case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindIOResultK[AppConfig](
func(parsed int) func(State) State {
return func(s State) State {
s.ParsedValue = parsed
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.ParsedValue }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("error case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: -5}),
BindIOResultK[AppConfig](
func(parsed int) func(State) State {
return func(s State) State {
s.ParsedValue = parsed
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.ParsedValue }),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestBindIOK(t *testing.T) {
type State struct {
Value int
}
getValue := func(s State) io.IO[int] {
return func() int {
return s.Value * 2
}
}
res := F.Pipe2(
Do[AppConfig](State{Value: 21}),
BindIOK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
getValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestBindReaderIOK(t *testing.T) {
type State struct {
Value int
}
getValue := func(s State) readerio.ReaderIO[AppConfig, int] {
return func(cfg AppConfig) io.IO[int] {
return func() int {
return s.Value + len(cfg.DatabaseURL)
}
}
}
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
BindReaderIOK(
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
getValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
// 10 + len("postgres://localhost") = 10 + 20 = 30
assert.Equal(t, result.Of(30), outcome)
}
func TestBindEitherK(t *testing.T) {
type State struct {
Value int
}
parseValue := func(s State) either.Either[error, int] {
if s.Value < 0 {
return either.Left[int](errors.New("negative"))
}
return either.Right[error](s.Value * 2)
}
t.Run("success case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("error case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: -5}),
BindEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestBindIOEitherK(t *testing.T) {
type State struct {
Value int
}
parseValue := func(s State) ioeither.IOEither[error, int] {
return func() either.Either[error, int] {
if s.Value < 0 {
return either.Left[int](errors.New("negative"))
}
return either.Right[error](s.Value * 2)
}
}
t.Run("success case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindIOEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("error case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: -5}),
BindIOEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestLensOperations(t *testing.T) {
type State struct {
Value int
}
valueLens := lens.MakeLens(
func(s State) int { return s.Value },
func(s State, v int) State {
s.Value = v
return s
},
)
t.Run("ApSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApSL(valueLens, Of[AppConfig](42)),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("BindL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
BindL(valueLens, func(v int) ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](v * 2)
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("LetL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
LetL[AppConfig](valueLens, func(v int) int { return v * 3 }),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("LetToL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
LetToL[AppConfig](valueLens, 99),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
})
t.Run("BindIOEitherKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindIOEitherKL[AppConfig](valueLens, func(v int) ioeither.IOEither[error, int] {
return ioeither.Of[error](v * 2)
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("BindIOKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 7}),
BindIOKL[AppConfig](valueLens, func(v int) io.IO[int] {
return func() int { return v * 3 }
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(21), outcome)
})
t.Run("BindReaderKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindReaderKL(valueLens, func(v int) reader.Reader[AppConfig, int] {
return func(cfg AppConfig) int {
return v + len(cfg.LogLevel)
}
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
// 5 + len("info") = 5 + 4 = 9
assert.Equal(t, result.Of(9), outcome)
})
t.Run("BindReaderIOKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
BindReaderIOKL(valueLens, func(v int) readerio.ReaderIO[AppConfig, int] {
return func(cfg AppConfig) io.IO[int] {
return func() int {
return v + len(cfg.DatabaseURL)
}
}
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
// 10 + len("postgres://localhost") = 10 + 20 = 30
assert.Equal(t, result.Of(30), outcome)
})
t.Run("ApIOEitherSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApIOEitherSL[AppConfig](valueLens, ioeither.Of[error](42)),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("ApIOSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApIOSL[AppConfig](valueLens, func() int { return 99 }),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
})
t.Run("ApReaderSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderSL(valueLens, func(cfg AppConfig) int {
return len(cfg.LogLevel)
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(4), outcome)
})
t.Run("ApReaderIOSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderIOSL(valueLens, func(cfg AppConfig) io.IO[int] {
return func() int { return len(cfg.DatabaseURL) }
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("ApEitherSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApEitherSL[AppConfig](valueLens, either.Right[error](77)),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(77), outcome)
})
}
func TestApOperations(t *testing.T) {
type State struct {
Value1 int
Value2 int
}
t.Run("ApIOEitherS", func(t *testing.T) {
res := F.Pipe3(
Do[AppConfig](State{}),
ApIOEitherS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
ioeither.Of[error](10),
),
ApIOEitherS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value2 = v
return s
}
},
ioeither.Of[error](20),
),
Map[AppConfig](func(s State) int { return s.Value1 + s.Value2 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("ApIOS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApIOS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
func() int { return 42 },
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("ApReaderS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderS(
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
func(cfg AppConfig) int { return len(cfg.LogLevel) },
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(4), outcome)
})
t.Run("ApReaderIOS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderIOS(
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
func(cfg AppConfig) io.IO[int] {
return func() int { return len(cfg.DatabaseURL) }
},
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("ApEitherS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApEitherS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
either.Right[error](99),
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
})
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
)
// Bracket ensures that a resource is properly cleaned up regardless of whether the operation
// succeeds or fails. It follows the acquire-use-release pattern with access to both outer (R)
// and inner (C) reader contexts.
//
// The release action is always called after the use action completes, whether it succeeds or fails.
// This makes it ideal for managing resources like file handles, database connections, or locks.
//
// Parameters:
// - acquire: Acquires the resource, returning a ReaderReaderIOEither[R, C, E, A]
// - use: Uses the acquired resource to perform an operation, returning ReaderReaderIOEither[R, C, E, B]
// - release: Releases the resource, receiving both the resource and the result of use
//
// Returns:
// - A ReaderReaderIOEither[R, C, E, B] that safely manages the resource lifecycle
//
// The release function receives:
// - The acquired resource (A)
// - The result of the use function (Either[E, B])
//
// Example:
//
// type OuterConfig struct {
// ConnectionPool string
// }
// type InnerConfig struct {
// Timeout time.Duration
// }
//
// // Acquire a database connection
// acquire := func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, *sql.DB] {
// return func(inner InnerConfig) ioeither.IOEither[error, *sql.DB] {
// return ioeither.TryCatch(
// func() (*sql.DB, error) {
// return sql.Open("postgres", outer.ConnectionPool)
// },
// func(err error) error { return err },
// )
// }
// }
//
// // Use the connection
// use := func(db *sql.DB) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, []User] {
// return func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, []User] {
// return func(inner InnerConfig) ioeither.IOEither[error, []User] {
// return queryUsers(db, inner.Timeout)
// }
// }
// }
//
// // Release the connection
// release := func(db *sql.DB, result either.Either[error, []User]) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, any] {
// return func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, any] {
// return func(inner InnerConfig) ioeither.IOEither[error, any] {
// return ioeither.TryCatch(
// func() (any, error) {
// return nil, db.Close()
// },
// func(err error) error { return err },
// )
// }
// }
// }
//
// result := readerreaderioeither.Bracket(acquire, use, release)
//
//go:inline
func Bracket[
R, A, B, ANY any](
acquire ReaderReaderIOResult[R, A],
use Kleisli[R, A, B],
release func(A, Result[B]) ReaderReaderIOResult[R, ANY],
) ReaderReaderIOResult[R, B] {
return RRIOE.Bracket(acquire, use, release)
}

View File

@@ -0,0 +1,396 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type Resource struct {
id string
acquired bool
released bool
}
func TestBracketSuccessPath(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource successfully
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("result from " + r.id)
}
}
}
}
// Release resource
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of("result from res1"), outcome)
assert.True(t, resource.acquired, "Resource should be acquired")
assert.True(t, resource.released, "Resource should be released")
}
func TestBracketUseFailure(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
useErr := errors.New("use failed")
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource with failure
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Left[string](useErr)
}
}
}
}
// Release resource (should still be called)
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Left[string](useErr), outcome)
assert.True(t, resource.acquired, "Resource should be acquired")
assert.True(t, resource.released, "Resource should be released even on failure")
}
func TestBracketAcquireFailure(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
acquireErr := errors.New("acquire failed")
useCalled := false
releaseCalled := false
// Acquire resource fails
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
return result.Left[*Resource](acquireErr)
}
}
}
// Use should not be called
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
useCalled = true
return result.Of("should not reach here")
}
}
}
}
// Release should not be called
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
releaseCalled = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Left[string](acquireErr), outcome)
assert.False(t, resource.acquired, "Resource should not be acquired")
assert.False(t, useCalled, "Use should not be called when acquire fails")
assert.False(t, releaseCalled, "Release should not be called when acquire fails")
}
func TestBracketReleaseReceivesResult(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
var capturedResult Result[string]
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("use result")
}
}
}
}
// Release captures the result
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
capturedResult = res
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of("use result"), outcome)
assert.Equal(t, result.Of("use result"), capturedResult)
assert.True(t, resource.released, "Resource should be released")
}
func TestBracketWithContextAccess(t *testing.T) {
cfg := AppConfig{DatabaseURL: "production-db", LogLevel: "debug"}
ctx := t.Context()
resource := &Resource{id: "res1"}
// Acquire uses outer context
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.id = c.DatabaseURL + "-resource"
resource.acquired = true
return result.Of(resource)
}
}
}
// Use uses both contexts
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
res := r.id + " with log level " + c.LogLevel
return result.Of(res)
}
}
}
}
// Release uses both contexts
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsRight(outcome))
assert.True(t, resource.acquired)
assert.True(t, resource.released)
assert.Equal(t, "production-db-resource", resource.id)
}
func TestBracketMultipleResources(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource1 := &Resource{id: "res1"}
resource2 := &Resource{id: "res2"}
// Acquire first resource
acquire1 := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource1.acquired = true
return result.Of(resource1)
}
}
}
// Use first resource to acquire second
use1 := func(r1 *Resource) ReaderReaderIOResult[AppConfig, string] {
// Nested bracket for second resource
acquire2 := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource2.acquired = true
return result.Of(resource2)
}
}
}
use2 := func(r2 *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of(r1.id + " and " + r2.id)
}
}
}
}
release2 := func(r2 *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r2.released = true
return result.Of[any](nil)
}
}
}
}
return Bracket(acquire2, use2, release2)
}
release1 := func(r1 *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r1.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire1, use1, release1)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of("res1 and res2"), outcome)
assert.True(t, resource1.acquired && resource1.released, "Resource 1 should be acquired and released")
assert.True(t, resource2.acquired && resource2.released, "Resource 2 should be acquired and released")
}
func TestBracketReleaseErrorDoesNotAffectResult(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
releaseErr := errors.New("release failed")
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource successfully
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("use success")
}
}
}
}
// Release fails but shouldn't affect the result
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
return result.Left[any](releaseErr)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
// The use result should be returned, not the release error
// (This behavior depends on the Bracket implementation)
assert.True(t, result.IsRight(outcome) || result.IsLeft(outcome))
assert.True(t, resource.acquired)
}

View File

@@ -0,0 +1,430 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"errors"
"testing"
"time"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// TestContextCancellationInMap tests that context cancellation is properly handled in Map operations
func TestContextCancellationInMap(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
computation := F.Pipe1(
Of[AppConfig](42),
Map[AppConfig](func(n int) int {
// This should still execute as Map doesn't check context
return n * 2
}),
)
outcome := computation(cfg)(ctx)()
// Map operations don't inherently check context, so they succeed
assert.Equal(t, result.Of(84), outcome)
}
// TestContextCancellationInChain tests context cancellation in Chain operations
func TestContextCancellationInChain(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithCancel(context.Background())
executed := false
computation := F.Pipe1(
Of[AppConfig](42),
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
// Check if context is cancelled
select {
case <-ctx.Done():
return result.Left[int](ctx.Err())
default:
executed = true
return result.Of(n * 2)
}
}
}
}
}),
)
cancel() // Cancel before execution
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsLeft(outcome))
assert.False(t, executed, "Chained operation should not execute when context is cancelled")
}
// TestContextCancellationWithTimeout tests timeout-based cancellation
func TestContextCancellationWithTimeout(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
computation := func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
// Simulate long-running operation
select {
case <-time.After(100 * time.Millisecond):
return result.Of(42)
case <-ctx.Done():
return result.Left[int](ctx.Err())
}
}
}
}
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsLeft(outcome))
result.Fold(
func(err error) any {
assert.ErrorIs(t, err, context.DeadlineExceeded)
return nil
},
func(v int) any {
t.Fatal("Should have timed out")
return nil
},
)(outcome)
}
// TestContextCancellationInBracket tests that bracket properly handles context cancellation
func TestContextCancellationInBracket(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithCancel(context.Background())
resource := &Resource{id: "res1"}
useCalled := false
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
select {
case <-ctx.Done():
return result.Left[string](ctx.Err())
default:
useCalled = true
return result.Of("success")
}
}
}
}
}
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r.released = true
return result.Of[any](nil)
}
}
}
}
cancel() // Cancel before use
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.True(t, resource.acquired, "Resource should be acquired")
assert.True(t, resource.released, "Resource should be released even with cancellation")
assert.False(t, useCalled, "Use should not execute when context is cancelled")
assert.True(t, result.IsLeft(outcome))
}
// TestContextCancellationInRetry tests context cancellation during retry operations
func TestContextCancellationInRetry(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
attempts := 0
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
select {
case <-ctx.Done():
return result.Left[int](ctx.Err())
case <-time.After(30 * time.Millisecond):
return result.Left[int](errors.New("temporary error"))
}
}
}
}
}
check := func(r Result[int]) bool {
return result.IsLeft(r)
}
policy := retry.LimitRetries(10)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsLeft(outcome))
// Should stop retrying when context is cancelled
assert.Less(t, attempts, 10, "Should stop retrying when context is cancelled")
}
// TestContextPropagationThroughMonadTransforms tests that context is properly propagated
func TestContextPropagationThroughMonadTransforms(t *testing.T) {
cfg := defaultConfig
t.Run("context propagates through Map", func(t *testing.T) {
ctx := context.WithValue(context.Background(), "key", "value")
var capturedCtx context.Context
computation := func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
capturedCtx = ctx
return result.Of("test")
}
}
}
_ = computation(cfg)(ctx)()
assert.Equal(t, "value", capturedCtx.Value("key"))
})
t.Run("context propagates through Chain", func(t *testing.T) {
ctx := context.WithValue(context.Background(), "key", "value")
var capturedCtx context.Context
computation := F.Pipe1(
Of[AppConfig](42),
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
capturedCtx = ctx
return result.Of(n * 2)
}
}
}
}),
)
_ = computation(cfg)(ctx)()
assert.Equal(t, "value", capturedCtx.Value("key"))
})
t.Run("context propagates through Ap", func(t *testing.T) {
ctx := context.WithValue(context.Background(), "key", "value")
var capturedCtx context.Context
fab := func(c AppConfig) ReaderIOResult[context.Context, func(int) int] {
return func(ctx context.Context) IOResult[func(int) int] {
return func() Result[func(int) int] {
capturedCtx = ctx
return result.Of(N.Mul(2))
}
}
}
fa := Of[AppConfig](21)
computation := MonadAp(fab, fa)
_ = computation(cfg)(ctx)()
assert.Equal(t, "value", capturedCtx.Value("key"))
})
}
// TestContextCancellationInAlt tests Alt operation with context cancellation
func TestContextCancellationInAlt(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithCancel(context.Background())
cancel()
firstCalled := false
secondCalled := false
first := func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
select {
case <-ctx.Done():
return result.Left[int](ctx.Err())
default:
firstCalled = true
return result.Left[int](errors.New("first error"))
}
}
}
}
second := func() ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
select {
case <-ctx.Done():
return result.Left[int](ctx.Err())
default:
secondCalled = true
return result.Of(42)
}
}
}
}
}
computation := MonadAlt(first, second)
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsLeft(outcome))
assert.False(t, firstCalled, "First should not execute when context is cancelled")
assert.False(t, secondCalled, "Second should not execute when context is cancelled")
}
// TestContextCancellationInDoNotation tests context cancellation in do-notation
func TestContextCancellationInDoNotation(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithCancel(context.Background())
type State struct {
Value1 int
Value2 int
}
step1Executed := false
step2Executed := false
computation := F.Pipe2(
Do[AppConfig](State{}),
Bind(
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
func(s State) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
select {
case <-ctx.Done():
return result.Left[int](ctx.Err())
default:
step1Executed = true
return result.Of(10)
}
}
}
}
},
),
Bind(
func(v int) func(State) State {
return func(s State) State {
s.Value2 = v
return s
}
},
func(s State) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
select {
case <-ctx.Done():
return result.Left[int](ctx.Err())
default:
step2Executed = true
return result.Of(20)
}
}
}
}
},
),
)
cancel() // Cancel before execution
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsLeft(outcome))
assert.False(t, step1Executed, "Step 1 should not execute when context is cancelled")
assert.False(t, step2Executed, "Step 2 should not execute when context is cancelled")
}
// TestContextCancellationBetweenSteps tests cancellation between sequential steps
func TestContextCancellationBetweenSteps(t *testing.T) {
cfg := defaultConfig
ctx, cancel := context.WithCancel(context.Background())
step1Executed := false
step2Executed := false
computation := F.Pipe1(
func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
step1Executed = true
cancel() // Cancel after first step
return result.Of(42)
}
}
},
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
select {
case <-ctx.Done():
return result.Left[int](ctx.Err())
default:
step2Executed = true
return result.Of(n * 2)
}
}
}
}
}),
)
outcome := computation(cfg)(ctx)()
assert.True(t, step1Executed, "Step 1 should execute")
assert.False(t, step2Executed, "Step 2 should not execute after cancellation")
assert.True(t, result.IsLeft(outcome))
}

View File

@@ -0,0 +1,76 @@
package readerreaderioresult
import (
"strconv"
"sync"
"testing"
A "github.com/IBM/fp-go/v2/array"
RES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type (
ConsoleDependency interface {
Log(msg string) IO[Void]
}
Res[A any] = RES.ReaderIOResult[A]
ConsoleEnv[A any] = ReaderReaderIOResult[ConsoleDependency, A]
consoleOnArray struct {
logs []string
mu sync.Mutex
}
)
var (
logConsole = reader.Curry1(ConsoleDependency.Log)
)
func (c *consoleOnArray) Log(msg string) IO[Void] {
return func() Void {
c.mu.Lock()
defer c.mu.Unlock()
c.logs = append(c.logs, msg)
return function.VOID
}
}
func makeConsoleOnArray() *consoleOnArray {
return &consoleOnArray{}
}
func TestConsoleEnv(t *testing.T) {
console := makeConsoleOnArray()
prg := F.Pipe1(
Of[ConsoleDependency]("Hello World!"),
TapReaderIOK(logConsole),
)
res := prg(console)(t.Context())()
assert.Equal(t, result.Of("Hello World!"), res)
assert.Equal(t, A.Of("Hello World!"), console.logs)
}
func TestConsoleEnvWithLocal(t *testing.T) {
console := makeConsoleOnArray()
prg := F.Pipe1(
Of[ConsoleDependency](42),
TapReaderIOK(reader.WithLocal(logConsole, strconv.Itoa)),
)
res := prg(console)(t.Context())()
assert.Equal(t, result.Of(42), res)
assert.Equal(t, A.Of("42"), console.logs)
}

View File

@@ -0,0 +1,477 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package readerreaderioresult provides a functional programming abstraction that combines
// four powerful concepts: Reader, Reader, IO, and Result (Either[error, A]) monads in a nested structure.
// This is a specialized version of readerreaderioeither where the error type is fixed to `error` and
// the inner context is fixed to `context.Context`.
//
// # Type Definition
//
// ReaderReaderIOResult[R, A] is defined as:
//
// type ReaderReaderIOResult[R, A] = ReaderReaderIOEither[R, context.Context, error, A]
//
// Which expands to:
//
// func(R) func(context.Context) func() Either[error, A]
//
// This represents a computation that:
// - Takes an outer environment/context of type R
// - Returns a function that takes a context.Context
// - Returns an IO operation (a thunk/function with no parameters)
// - Produces an Either[error, A] (Result[A]) when executed
//
// # Type Parameter Ordering Convention
//
// This package follows a consistent convention for ordering type parameters in function signatures.
// The general rule is: R -> C -> E -> T (outer context, inner context, error, type), where:
// - R: The outer Reader context/environment type
// - C: The inner Reader context/environment type (for the ReaderIOEither)
// - E: The Either error type
// - T: The value type(s) (A, B, etc.)
//
// However, when some type parameters can be automatically inferred by the Go compiler from
// function arguments, the convention is modified to minimize explicit type annotations:
//
// Rule: Undetectable types come first, followed by detectable types, while preserving
// the relative order within each group (R -> C -> E -> T).
//
// Examples:
//
// 1. All types detectable from first argument:
// MonadMap[R, C, E, A, B](fa ReaderReaderIOEither[R, C, E, A], f func(A) B)
// - R, C, E, A are detectable from fa
// - B is detectable from f
// - Order: R, C, E, A, B (standard order, all detectable)
//
// 2. Some types undetectable:
// FromReader[C, E, R, A](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A]
// - R, A are detectable from ma
// - C, E are undetectable (not in any argument)
// - Order: C, E, R, A (C, E first as undetectable, then R, A in standard order)
//
// 3. Multiple undetectable types:
// Local[C, E, A, R1, R2](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A]
// - C, E, A are undetectable
// - R1, R2 are detectable from f
// - Order: C, E, A, R1, R2 (undetectable first, then detectable)
//
// 4. Functions returning Kleisli arrows:
// ChainReaderOptionK[R, C, A, B, E](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, B]
// - Canonical order would be R, C, E, A, B
// - E is detectable from onNone parameter
// - R, C, A, B are not detectable (they're in the Kleisli argument type)
// - Order: R, C, A, B, E (undetectable R, C, A, B first, then detectable E)
//
// This convention allows for more ergonomic function calls:
//
// // Without convention - need to specify all types:
// result := FromReader[OuterCtx, InnerCtx, error, User](readerFunc)
//
// // With convention - only specify undetectable types:
// result := FromReader[InnerCtx, error](readerFunc) // R and A inferred from readerFunc
//
// The reasoning behind this approach is to reduce the number of explicit type parameters
// that developers need to specify when calling functions, improving code readability and
// reducing verbosity while maintaining type safety.
//
// Additional examples demonstrating the convention:
//
// 5. FromReaderOption[R, C, A, E](onNone Lazy[E]) Kleisli[R, C, E, ReaderOption[R, A], A]
// - Canonical order would be R, C, E, A
// - E is detectable from onNone parameter
// - R, C, A are not detectable (they're in the return type's Kleisli)
// - Order: R, C, A, E (undetectable R, C, A first, then detectable E)
//
// 6. MapLeft[R, C, A, E1, E2](f func(E1) E2) func(ReaderReaderIOEither[R, C, E1, A]) ReaderReaderIOEither[R, C, E2, A]
// - Canonical order would be R, C, E1, E2, A
// - E1, E2 are detectable from f parameter
// - R, C, A are not detectable (they're in the return type)
// - Order: R, C, A, E1, E2 (undetectable R, C, A first, then detectable E1, E2)
//
// Additional special cases:
//
// - Ap[B, R, C, E, A]: B is undetectable (in function return type), so B comes first
// - ChainOptionK[R, C, A, B, E]: R, C, A, B are undetectable, E is detectable from onNone
// - FromReaderIO[C, E, R, A]: C, E are undetectable, R, A are detectable from ReaderIO[R, A]
//
// All functions in this package follow this convention consistently.
//
// # Fantasy Land Specification
//
// This is a monad transformer combining:
// - Reader monad: https://github.com/fantasyland/fantasy-land
// - Reader monad (nested): https://github.com/fantasyland/fantasy-land
// - IO monad: https://github.com/fantasyland/fantasy-land
// - Either monad: https://github.com/fantasyland/fantasy-land#either
//
// Implemented Fantasy Land algebras:
// - Functor: https://github.com/fantasyland/fantasy-land#functor
// - Bifunctor: https://github.com/fantasyland/fantasy-land#bifunctor
// - Apply: https://github.com/fantasyland/fantasy-land#apply
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
// - Chain: https://github.com/fantasyland/fantasy-land#chain
// - Monad: https://github.com/fantasyland/fantasy-land#monad
// - Alt: https://github.com/fantasyland/fantasy-land#alt
//
// # ReaderReaderIOEither
//
// ReaderReaderIOEither[R, C, E, A] represents a computation that:
// - Depends on an outer context/environment of type R (outer Reader)
// - Returns a computation that depends on an inner context/environment of type C (inner Reader)
// - Performs side effects (IO)
// - Can fail with an error of type E or succeed with a value of type A (Either)
//
// This is particularly useful for:
// - Multi-level dependency injection patterns
// - Layered architectures with different context requirements at each layer
// - Composing operations that need access to multiple levels of configuration or context
// - Building reusable components that can be configured at different stages
//
// # Core Operations
//
// Construction:
// - Of/Right: Create a successful computation
// - Left: Create a failed computation
// - FromEither: Lift an Either into ReaderReaderIOEither
// - FromIO: Lift an IO into ReaderReaderIOEither
// - FromReader: Lift a Reader into ReaderReaderIOEither
// - FromReaderIO: Lift a ReaderIO into ReaderReaderIOEither
// - FromIOEither: Lift an IOEither into ReaderReaderIOEither
// - FromReaderEither: Lift a ReaderEither into ReaderReaderIOEither
// - FromReaderIOEither: Lift a ReaderIOEither into ReaderReaderIOEither
// - FromReaderOption: Lift a ReaderOption into ReaderReaderIOEither
//
// Transformation:
// - Map: Transform the success value
// - MapLeft: Transform the error value
// - Chain/Bind: Sequence dependent computations
// - Flatten: Flatten nested ReaderReaderIOEither
//
// Combination:
// - Ap: Apply a function in a context to a value in a context
// - ApSeq: Sequential application
// - ApPar: Parallel application
//
// Error Handling:
// - Alt: Choose the first successful computation
//
// Context Access:
// - Ask: Get the current outer context
// - Asks: Get a value derived from the outer context
// - Local: Run a computation with a modified outer context
// - Read: Execute with a specific outer context
//
// Kleisli Composition:
// - ChainEitherK: Chain with Either-returning functions
// - ChainReaderK: Chain with Reader-returning functions
// - ChainReaderIOK: Chain with ReaderIO-returning functions
// - ChainReaderEitherK: Chain with ReaderEither-returning functions
// - ChainReaderOptionK: Chain with ReaderOption-returning functions
// - ChainIOEitherK: Chain with IOEither-returning functions
// - ChainIOK: Chain with IO-returning functions
// - ChainOptionK: Chain with Option-returning functions
//
// First/Tap Operations (execute for side effects, return original value):
// - ChainFirst/Tap: Execute a computation but return the original value
// - ChainFirstEitherK/TapEitherK: Tap with Either-returning functions
// - ChainFirstReaderK/TapReaderK: Tap with Reader-returning functions
// - ChainFirstReaderIOK/TapReaderIOK: Tap with ReaderIO-returning functions
// - ChainFirstReaderEitherK/TapReaderEitherK: Tap with ReaderEither-returning functions
// - ChainFirstReaderOptionK/TapReaderOptionK: Tap with ReaderOption-returning functions
// - ChainFirstIOK/TapIOK: Tap with IO-returning functions
//
// # Example Usage
//
// type AppConfig struct {
// DatabaseURL string
// LogLevel string
// }
//
// // A computation that depends on AppConfig and context.Context
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
// return func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, User] {
// // Use cfg.DatabaseURL and cfg.LogLevel
// return func(ctx context.Context) ioresult.IOResult[User] {
// // Use ctx for cancellation/timeout
// return func() result.Result[User] {
// // Perform the actual IO operation
// // Return result.Of(user) or result.Error[User](err)
// }
// }
// }
// }
//
// // Compose operations
// result := function.Pipe2(
// fetchUser(123),
// Map[AppConfig](func(u User) string { return u.Name }),
// Chain[AppConfig](func(name string) ReaderReaderIOResult[AppConfig, string] {
// return Of[AppConfig]("Hello, " + name)
// }),
// )
//
// // Execute with config and context
// appConfig := AppConfig{DatabaseURL: "postgres://...", LogLevel: "info"}
// ctx := t.Context()
// outcome := result(appConfig)(ctx)() // Returns result.Result[string]
//
// # Use Cases
//
// This monad is particularly useful for:
// - Applications with layered configuration (app config + request context)
// - HTTP handlers that need both application config and request context
// - Database operations with connection pool config and query context
// - Retry logic with policy configuration and execution context
// - Resource management with bracket pattern across multiple contexts
//
// # Dependency Injection with the Outer Context
//
// The outer Reader context (type parameter R) provides a powerful mechanism for dependency injection
// in functional programming. This pattern is explained in detail in Scott Wlaschin's talk:
// "Dependency Injection, The Functional Way" - https://www.youtube.com/watch?v=xPlsVVaMoB0
//
// ## Core Concept
//
// Instead of using traditional OOP dependency injection frameworks, the Reader monad allows you to:
// 1. Define functions that declare their dependencies as type parameters
// 2. Compose these functions without providing the dependencies
// 3. Supply all dependencies at the "end of the world" (program entry point)
//
// This approach provides:
// - Compile-time safety: Missing dependencies cause compilation errors
// - Explicit dependencies: Function signatures show exactly what they need
// - Easy testing: Mock dependencies by providing different values
// - Pure functions: Dependencies are passed as parameters, not global state
//
// ## Examples from the Video Adapted to fp-go
//
// ### Example 1: Basic Reader Pattern (Video: "Reader Monad Basics")
//
// In the video, Scott shows how to pass configuration through a chain of functions.
// In fp-go with ReaderReaderIOResult:
//
// // Define your dependencies
// type AppConfig struct {
// DatabaseURL string
// APIKey string
// MaxRetries int
// }
//
// // Functions declare their dependencies via the R type parameter
// func getConnectionString() ReaderReaderIOResult[AppConfig, string] {
// return Asks[AppConfig](func(cfg AppConfig) string {
// return cfg.DatabaseURL
// })
// }
//
// func connectToDatabase() ReaderReaderIOResult[AppConfig, *sql.DB] {
// return MonadChain(
// getConnectionString(),
// func(connStr string) ReaderReaderIOResult[AppConfig, *sql.DB] {
// return FromIO[AppConfig](func() result.Result[*sql.DB] {
// db, err := sql.Open("postgres", connStr)
// return result.FromEither(either.FromError(db, err))
// })
// },
// )
// }
//
// ### Example 2: Composing Dependencies (Video: "Composing Reader Functions")
//
// The video demonstrates how Reader functions compose naturally.
// In fp-go, you can compose operations that all share the same dependency:
//
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
// return MonadChain(
// connectToDatabase(),
// func(db *sql.DB) ReaderReaderIOResult[AppConfig, User] {
// return FromIO[AppConfig](func() result.Result[User] {
// // Query database using db and return user
// // The AppConfig is still available if needed
// })
// },
// )
// }
//
// func enrichUser(user User) ReaderReaderIOResult[AppConfig, EnrichedUser] {
// return Asks[AppConfig, EnrichedUser](func(cfg AppConfig) EnrichedUser {
// // Use cfg.APIKey to call external service
// return EnrichedUser{User: user, Extra: "data"}
// })
// }
//
// // Compose without providing dependencies
// pipeline := function.Pipe2(
// fetchUser(123),
// Chain[AppConfig](enrichUser),
// )
//
// // Provide dependencies at the end
// config := AppConfig{DatabaseURL: "...", APIKey: "...", MaxRetries: 3}
// ctx := context.Background()
// result := pipeline(config)(ctx)()
//
// ### Example 3: Local Context Modification (Video: "Local Environment")
//
// The video shows how to temporarily modify the environment for a sub-computation.
// In fp-go, use the Local function:
//
// // Run a computation with modified configuration
// func withRetries(retries int, action ReaderReaderIOResult[AppConfig, string]) ReaderReaderIOResult[AppConfig, string] {
// return Local[string](func(cfg AppConfig) AppConfig {
// // Create a modified config with different retry count
// return AppConfig{
// DatabaseURL: cfg.DatabaseURL,
// APIKey: cfg.APIKey,
// MaxRetries: retries,
// }
// })(action)
// }
//
// // Use it
// result := withRetries(5, fetchUser(123))
//
// ### Example 4: Testing with Mock Dependencies (Video: "Testing with Reader")
//
// The video emphasizes how Reader makes testing easy by allowing mock dependencies.
// In fp-go:
//
// func TestFetchUser(t *testing.T) {
// // Create a test configuration
// testConfig := AppConfig{
// DatabaseURL: "mock://test",
// APIKey: "test-key",
// MaxRetries: 1,
// }
//
// // Run the computation with test config
// ctx := context.Background()
// result := fetchUser(123)(testConfig)(ctx)()
//
// // Assert on the result
// assert.True(t, either.IsRight(result))
// }
//
// ### Example 5: Multi-Layer Dependencies (Video: "Nested Readers")
//
// The video discusses nested readers for multi-layer architectures.
// ReaderReaderIOResult provides exactly this with R (outer) and context.Context (inner):
//
// type AppConfig struct {
// DatabaseURL string
// }
//
// // Outer context: Application-level configuration (AppConfig)
// // Inner context: Request-level context (context.Context)
// func handleRequest(userID int) ReaderReaderIOResult[AppConfig, Response] {
// return func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, Response] {
// // cfg is available here (outer context)
// return func(ctx context.Context) ioresult.IOResult[Response] {
// // ctx is available here (inner context)
// // Both cfg and ctx can be used
// return func() result.Result[Response] {
// // Perform operation using both contexts
// select {
// case <-ctx.Done():
// return result.Error[Response](ctx.Err())
// default:
// // Use cfg.DatabaseURL to connect
// return result.Of(Response{})
// }
// }
// }
// }
// }
//
// ### Example 6: Avoiding Global State (Video: "Problems with Global State")
//
// The video criticizes global state and shows how Reader solves this.
// In fp-go, instead of:
//
// // BAD: Global state
// var globalConfig AppConfig
//
// func fetchUser(id int) result.Result[User] {
// // Uses globalConfig implicitly
// db := connectTo(globalConfig.DatabaseURL)
// // ...
// }
//
// Use Reader to make dependencies explicit:
//
// // GOOD: Explicit dependencies
// func fetchUser(id int) ReaderReaderIOResult[AppConfig, User] {
// return MonadChain(
// Ask[AppConfig](), // Explicitly request the config
// func(cfg AppConfig) ReaderReaderIOResult[AppConfig, User] {
// // Use cfg explicitly
// return FromIO[AppConfig](func() result.Result[User] {
// db := connectTo(cfg.DatabaseURL)
// // ...
// })
// },
// )
// }
//
// ## Benefits of This Approach
//
// 1. **Type Safety**: The compiler ensures all dependencies are provided
// 2. **Testability**: Easy to provide mock dependencies for testing
// 3. **Composability**: Functions compose naturally without dependency wiring
// 4. **Explicitness**: Function signatures document their dependencies
// 5. **Immutability**: Dependencies are immutable values, not mutable global state
// 6. **Flexibility**: Use Local to modify dependencies for sub-computations
// 7. **Separation of Concerns**: Business logic is separate from dependency resolution
//
// ## Comparison with Traditional DI
//
// Traditional OOP DI (e.g., Spring, Guice):
// - Runtime dependency resolution
// - Magic/reflection-based wiring
// - Implicit dependencies (hidden in constructors)
// - Mutable containers
//
// Reader-based DI (fp-go):
// - Compile-time dependency resolution
// - Explicit function composition
// - Explicit dependencies (in type signatures)
// - Immutable values
//
// ## When to Use Each Layer
//
// - **Outer Reader (R)**: Application-level dependencies that rarely change
// - Database connection pools
// - API keys and secrets
// - Feature flags
// - Application configuration
//
// - **Inner Reader (context.Context)**: Request-level dependencies that change per operation
// - Request IDs and tracing
// - Cancellation signals
// - Deadlines and timeouts
// - User authentication tokens
//
// This two-layer approach mirrors the video's discussion of nested readers and provides
// a clean separation between application-level and request-level concerns.
//
// # Relationship to Other Packages
//
// - readerreaderioeither: The generic version with configurable error and context types
// - readerioresult: Single reader with context.Context and error
// - readerresult: Single reader with error (no IO)
// - context/readerioresult: Alias for readerioresult with context.Context
package readerreaderioresult

View File

@@ -0,0 +1,291 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerioeither"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
)
// Sequence swaps the order of nested environment parameters in a ReaderReaderIOResult computation.
//
// This function takes a ReaderReaderIOResult that produces another ReaderReaderIOResult and returns a
// Kleisli arrow that reverses the order of the outer environment parameters (R1 and R2). The result is
// a curried function that takes R1 first, then R2, and produces a computation with context.Context and error handling.
//
// Type Parameters:
// - R1: The first outer environment type (becomes the outermost after sequence)
// - R2: The second outer environment type (becomes inner after sequence)
// - A: The success value type
//
// Parameters:
// - ma: A ReaderReaderIOResult[R2, ReaderReaderIOResult[R1, A]]
//
// Returns:
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
//
// The function preserves error handling and IO effects at all levels while reordering the
// outer environment dependencies. The inner context.Context layer remains unchanged.
//
// This is particularly useful when you need to change the order in which contexts are provided
// to a nested computation, such as when composing operations that have different dependency orders.
//
// Example:
//
// type AppConfig struct {
// DatabaseURL string
// }
// type UserPrefs struct {
// Theme string
// }
//
// // Original: takes AppConfig, returns computation that may produce
// // another computation depending on UserPrefs
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context,
// ReaderReaderIOResult[UserPrefs, string]] {
// return readerioresult.Of[context.Context](
// Of[UserPrefs]("result"),
// )
// }
//
// // Sequence swaps UserPrefs and AppConfig order
// sequenced := Sequence[UserPrefs, AppConfig, string](original)
//
// // Now provide UserPrefs first, then AppConfig
// ctx := context.Background()
// result := sequenced(UserPrefs{Theme: "dark"})(AppConfig{DatabaseURL: "db"})(ctx)()
func Sequence[R1, R2, A any](ma ReaderReaderIOResult[R2, ReaderReaderIOResult[R1, A]]) Kleisli[R2, R1, A] {
return readert.Sequence(
readerioeither.Chain,
ma,
)
}
// SequenceReader swaps the order of environment parameters when the inner computation is a pure Reader.
//
// This function is similar to Sequence but specialized for the case where the innermost computation
// is a pure Reader (without IO or error handling) rather than another ReaderReaderIOResult. It takes
// a ReaderReaderIOResult that produces a Reader and returns a Kleisli arrow that reverses the order
// of the outer environment parameters.
//
// Type Parameters:
// - R1: The first environment type (becomes outermost after sequence)
// - R2: The second environment type (becomes inner after sequence)
// - A: The success value type
//
// Parameters:
// - ma: A ReaderReaderIOResult[R2, Reader[R1, A]]
//
// Returns:
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
//
// The function lifts the pure Reader computation into the ReaderIOResult context (with context.Context
// and error handling) while reordering the environment dependencies.
//
// Example:
//
// type AppConfig struct {
// Multiplier int
// }
// type Database struct {
// ConnectionString string
// }
//
// // Original: takes AppConfig, may produce a Reader[Database, int]
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, reader.Reader[Database, int]] {
// return readerioresult.Of[context.Context](func(db Database) int {
// return len(db.ConnectionString) * cfg.Multiplier
// })
// }
//
// // Sequence to provide Database first, then AppConfig
// sequenced := SequenceReader[Database, AppConfig, int](original)
// ctx := context.Background()
// result := sequenced(Database{ConnectionString: "localhost"})(AppConfig{Multiplier: 2})(ctx)()
func SequenceReader[R1, R2, A any](ma ReaderReaderIOResult[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
return readert.SequenceReader(
readerioeither.Map,
ma,
)
}
// SequenceReaderIO swaps the order of environment parameters when the inner computation is a ReaderIO.
//
// This function is specialized for the case where the innermost computation is a ReaderIO
// (with IO effects but no error handling) rather than another ReaderReaderIOResult. It takes
// a ReaderReaderIOResult that produces a ReaderIO and returns a Kleisli arrow that reverses
// the order of the outer environment parameters.
//
// Type Parameters:
// - R1: The first environment type (becomes outermost after sequence)
// - R2: The second environment type (becomes inner after sequence)
// - A: The success value type
//
// Parameters:
// - ma: A ReaderReaderIOResult[R2, ReaderIO[R1, A]]
//
// Returns:
// - A Kleisli[R2, R1, A], which is func(R1) ReaderReaderIOResult[R2, A]
//
// The function lifts the ReaderIO computation (which has IO effects but no error handling)
// into the ReaderIOResult context (with context.Context and error handling) while reordering
// the environment dependencies.
//
// Example:
//
// type AppConfig struct {
// FilePath string
// }
// type Logger struct {
// Level string
// }
//
// // Original: takes AppConfig, may produce a ReaderIO[Logger, string]
// original := func(cfg AppConfig) readerioresult.ReaderIOResult[context.Context, readerio.ReaderIO[Logger, string]] {
// return readerioresult.Of[context.Context](func(logger Logger) io.IO[string] {
// return func() string {
// return fmt.Sprintf("[%s] Reading from %s", logger.Level, cfg.FilePath)
// }
// })
// }
//
// // Sequence to provide Logger first, then AppConfig
// sequenced := SequenceReaderIO[Logger, AppConfig, string](original)
// ctx := context.Background()
// result := sequenced(Logger{Level: "INFO"})(AppConfig{FilePath: "/data"})(ctx)()
func SequenceReaderIO[R1, R2, A any](ma ReaderReaderIOResult[R2, ReaderIO[R1, A]]) Kleisli[R2, R1, A] {
return RRIOE.SequenceReaderIO(ma)
}
// Traverse transforms a ReaderReaderIOResult computation by applying a function that produces
// another ReaderReaderIOResult, effectively swapping the order of outer environment parameters.
//
// This function is useful when you have a computation that depends on environment R2 and
// produces a value of type A, and you want to transform it using a function that takes A
// and produces a computation depending on environment R1. The result is a curried function
// that takes R1 first, then R2, and produces a computation with context.Context and error handling.
//
// Type Parameters:
// - R2: The outer environment type from the original computation
// - R1: The inner environment type introduced by the transformation
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Kleisli arrow that transforms A into a ReaderReaderIOResult[R1, B]
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R2, A] and returns a Kleisli[R2, R1, B],
// which is func(R1) ReaderReaderIOResult[R2, B]
//
// The function preserves error handling and IO effects while reordering the environment dependencies.
// This is the generalized version of Sequence that also applies a transformation function.
//
// Example:
//
// type AppConfig struct {
// SystemID string
// }
// type UserConfig struct {
// UserID int
// }
//
// // Original computation depending on AppConfig
// original := Of[AppConfig](42)
//
// // Transformation that introduces UserConfig dependency
// transform := func(n int) ReaderReaderIOResult[UserConfig, string] {
// return func(userCfg UserConfig) readerioresult.ReaderIOResult[context.Context, string] {
// return readerioresult.Of[context.Context](fmt.Sprintf("User %d: %d", userCfg.UserID, n))
// }
// }
//
// // Apply traverse to swap order and transform
// traversed := Traverse[AppConfig, UserConfig, int, string](transform)(original)
//
// // Provide UserConfig first, then AppConfig
// ctx := context.Background()
// result := traversed(UserConfig{UserID: 1})(AppConfig{SystemID: "sys1"})(ctx)()
func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(ReaderReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
return readert.Traverse[ReaderReaderIOResult[R2, A]](
readerioeither.Map,
readerioeither.Chain,
f,
)
}
// TraverseReader transforms a ReaderReaderIOResult computation by applying a Reader-based function,
// effectively introducing a new environment dependency.
//
// This function takes a Reader-based transformation (Kleisli arrow) and returns a function that
// can transform a ReaderReaderIOResult. The result allows you to provide the Reader's environment (R1)
// first, which then produces a ReaderReaderIOResult that depends on environment R2.
//
// Type Parameters:
// - R2: The outer environment type from the original ReaderReaderIOResult
// - R1: The inner environment type introduced by the Reader transformation
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R1
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R2, A] and returns a Kleisli[R2, R1, B],
// which is func(R1) ReaderReaderIOResult[R2, B]
//
// The function preserves error handling and IO effects while adding the Reader environment dependency
// and reordering the environment parameters. This is useful when you want to introduce a pure
// (non-IO, non-error) environment dependency to an existing computation.
//
// Example:
//
// type AppConfig struct {
// Timeout int
// }
// type UserPreferences struct {
// Theme string
// }
//
// // Original computation depending on AppConfig
// original := Of[AppConfig](100)
//
// // Pure Reader transformation that introduces UserPreferences dependency
// formatWithTheme := func(value int) reader.Reader[UserPreferences, string] {
// return func(prefs UserPreferences) string {
// return fmt.Sprintf("[%s theme] Value: %d", prefs.Theme, value)
// }
// }
//
// // Apply traverse to introduce UserPreferences and swap order
// traversed := TraverseReader[AppConfig, UserPreferences, int, string](formatWithTheme)(original)
//
// // Provide UserPreferences first, then AppConfig
// ctx := context.Background()
// result := traversed(UserPreferences{Theme: "dark"})(AppConfig{Timeout: 30})(ctx)()
func TraverseReader[R2, R1, A, B any](
f reader.Kleisli[R1, A, B],
) func(ReaderReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
return readert.TraverseReader[ReaderReaderIOResult[R2, A]](
readerioeither.Map,
readerioeither.Map,
f,
)
}

View File

@@ -0,0 +1,778 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"errors"
"fmt"
"testing"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type Config1 struct {
value1 int
}
type Config2 struct {
value2 string
}
func TestSequence(t *testing.T) {
t.Run("swaps parameter order for simple types", func(t *testing.T) {
ctx := t.Context()
// Original: takes Config2, returns ReaderIOResult that may produce ReaderReaderIOResult[Config1, int]
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
return func(ctx1 context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
return func() Result[ReaderReaderIOResult[Config1, int]] {
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
return func(ctx2 context.Context) IOResult[int] {
return func() Result[int] {
return result.Of(cfg1.value1 + len(cfg2.value2))
}
}
})
}
}
}
// Sequence swaps Config1 and Config2 order
sequenced := Sequence(original)
cfg1 := Config1{value1: 10}
cfg2 := Config2{value2: "hello"}
// Test original: Config2 -> Context -> Config1 -> Context
result1 := original(cfg2)(ctx)()
assert.True(t, result.IsRight(result1))
innerFunc1, _ := result.Unwrap(result1)
innerResult1 := innerFunc1(cfg1)(ctx)()
assert.Equal(t, result.Of(15), innerResult1)
// Test sequenced: Config1 -> Config2 -> Context
innerFunc2 := sequenced(cfg1)
innerResult2 := innerFunc2(cfg2)(ctx)()
assert.Equal(t, result.Of(15), innerResult2)
})
t.Run("preserves error handling", func(t *testing.T) {
ctx := t.Context()
testErr := errors.New("test error")
// Original that returns an error
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
return func() Result[ReaderReaderIOResult[Config1, int]] {
return result.Left[ReaderReaderIOResult[Config1, int]](testErr)
}
}
}
sequenced := Sequence(original)
cfg1 := Config1{value1: 10}
cfg2 := Config2{value2: "hello"}
// Test sequenced preserves error
innerFunc := sequenced(cfg1)
outcome := innerFunc(cfg2)(ctx)()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("works with nested computations", func(t *testing.T) {
ctx := t.Context()
// Original with nested logic
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, string]] {
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, string]] {
return func() Result[ReaderReaderIOResult[Config1, string]] {
if len(cfg2.value2) == 0 {
return result.Left[ReaderReaderIOResult[Config1, string]](errors.New("empty string"))
}
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
if cfg1.value1 < 0 {
return result.Left[string](errors.New("negative value"))
}
return result.Of(fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1))
}
}
})
}
}
}
sequenced := Sequence(original)
// Test with valid inputs
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of("test:42"), result1)
// Test with empty string
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
assert.True(t, result.IsLeft(result2))
// Test with negative value
result3 := sequenced(Config1{value1: -1})(Config2{value2: "test"})(ctx)()
assert.True(t, result.IsLeft(result3))
})
t.Run("works with zero values", func(t *testing.T) {
ctx := t.Context()
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
return func() Result[ReaderReaderIOResult[Config1, int]] {
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
return result.Of(cfg1.value1 + len(cfg2.value2))
}
}
})
}
}
}
sequenced := Sequence(original)
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
assert.Equal(t, result.Of(0), outcome)
})
t.Run("maintains referential transparency", func(t *testing.T) {
ctx := t.Context()
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
return func() Result[ReaderReaderIOResult[Config1, int]] {
return result.Of(func(cfg1 Config1) RIORES.ReaderIOResult[int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
return result.Of(cfg1.value1 * len(cfg2.value2))
}
}
})
}
}
}
sequenced := Sequence(original)
cfg1 := Config1{value1: 3}
cfg2 := Config2{value2: "test"}
// Call multiple times with same inputs
for range 5 {
outcome := sequenced(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of(12), outcome)
}
})
}
func TestSequenceReader(t *testing.T) {
t.Run("swaps parameter order for Reader types", func(t *testing.T) {
ctx := t.Context()
// Original: takes Config2, returns ReaderIOResult that may produce Reader[Config1, int]
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
return func() Result[Reader[Config1, int]] {
return result.Of(func(cfg1 Config1) int {
return cfg1.value1 + len(cfg2.value2)
})
}
}
}
// Sequence swaps Config1 and Config2 order
sequenced := SequenceReader(original)
cfg1 := Config1{value1: 10}
cfg2 := Config2{value2: "hello"}
// Test original
result1 := original(cfg2)(ctx)()
assert.True(t, result.IsRight(result1))
innerFunc1, _ := result.Unwrap(result1)
value1 := innerFunc1(cfg1)
assert.Equal(t, 15, value1)
// Test sequenced
innerFunc2 := sequenced(cfg1)
result2 := innerFunc2(cfg2)(ctx)()
assert.True(t, result.IsRight(result2))
value2, _ := result.Unwrap(result2)
assert.Equal(t, 15, value2)
})
t.Run("preserves error handling", func(t *testing.T) {
ctx := t.Context()
testErr := errors.New("test error")
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
return func() Result[Reader[Config1, int]] {
return result.Left[Reader[Config1, int]](testErr)
}
}
}
sequenced := SequenceReader(original)
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("works with pure Reader computations", func(t *testing.T) {
ctx := t.Context()
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, string]] {
return func(ctx context.Context) IOResult[Reader[Config1, string]] {
return func() Result[Reader[Config1, string]] {
if len(cfg2.value2) == 0 {
return result.Left[Reader[Config1, string]](errors.New("empty string"))
}
return result.Of(func(cfg1 Config1) string {
return fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1)
})
}
}
}
sequenced := SequenceReader(original)
// Test with valid inputs
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of("test:42"), result1)
// Test with empty string
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
assert.True(t, result.IsLeft(result2))
})
t.Run("works with zero values", func(t *testing.T) {
ctx := t.Context()
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
return func() Result[Reader[Config1, int]] {
return result.Of(func(cfg1 Config1) int {
return cfg1.value1 + len(cfg2.value2)
})
}
}
}
sequenced := SequenceReader(original)
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
assert.Equal(t, result.Of(0), outcome)
})
t.Run("maintains referential transparency", func(t *testing.T) {
ctx := t.Context()
original := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
return func() Result[Reader[Config1, int]] {
return result.Of(func(cfg1 Config1) int {
return cfg1.value1 * len(cfg2.value2)
})
}
}
}
sequenced := SequenceReader(original)
cfg1 := Config1{value1: 3}
cfg2 := Config2{value2: "test"}
// Call multiple times with same inputs
for range 5 {
outcome := sequenced(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of(12), outcome)
}
})
}
func TestSequenceReaderIO(t *testing.T) {
t.Run("swaps parameter order for ReaderIO types", func(t *testing.T) {
ctx := t.Context()
// Original: takes Config2, returns ReaderIOResult that may produce ReaderIO[Config1, int]
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
return func() Result[ReaderIO[Config1, int]] {
return result.Of(func(cfg1 Config1) io.IO[int] {
return io.Of(cfg1.value1 + len(cfg2.value2))
})
}
}
}
// Sequence swaps Config1 and Config2 order
sequenced := SequenceReaderIO(original)
cfg1 := Config1{value1: 10}
cfg2 := Config2{value2: "hello"}
// Test original
result1 := original(cfg2)(ctx)()
assert.True(t, result.IsRight(result1))
innerFunc1, _ := result.Unwrap(result1)
value1 := innerFunc1(cfg1)()
assert.Equal(t, 15, value1)
// Test sequenced
innerFunc2 := sequenced(cfg1)
result2 := innerFunc2(cfg2)(ctx)()
assert.True(t, result.IsRight(result2))
value2, _ := result.Unwrap(result2)
assert.Equal(t, 15, value2)
})
t.Run("preserves error handling", func(t *testing.T) {
ctx := t.Context()
testErr := errors.New("test error")
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
return func() Result[ReaderIO[Config1, int]] {
return result.Left[ReaderIO[Config1, int]](testErr)
}
}
}
sequenced := SequenceReaderIO(original)
outcome := sequenced(Config1{value1: 10})(Config2{value2: "hello"})(ctx)()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("works with IO effects", func(t *testing.T) {
ctx := t.Context()
sideEffect := 0
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, string]] {
return func(ctx context.Context) IOResult[ReaderIO[Config1, string]] {
return func() Result[ReaderIO[Config1, string]] {
if len(cfg2.value2) == 0 {
return result.Left[ReaderIO[Config1, string]](errors.New("empty string"))
}
return result.Of(func(cfg1 Config1) io.IO[string] {
return func() string {
sideEffect = cfg1.value1
return fmt.Sprintf("%s:%d", cfg2.value2, cfg1.value1)
}
})
}
}
}
sequenced := SequenceReaderIO(original)
// Test with valid inputs
sideEffect = 0
result1 := sequenced(Config1{value1: 42})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of("test:42"), result1)
assert.Equal(t, 42, sideEffect)
// Test with empty string
sideEffect = 0
result2 := sequenced(Config1{value1: 42})(Config2{value2: ""})(ctx)()
assert.True(t, result.IsLeft(result2))
assert.Equal(t, 0, sideEffect) // Side effect should not occur
})
t.Run("works with zero values", func(t *testing.T) {
ctx := t.Context()
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
return func() Result[ReaderIO[Config1, int]] {
return result.Of(func(cfg1 Config1) io.IO[int] {
return io.Of(cfg1.value1 + len(cfg2.value2))
})
}
}
}
sequenced := SequenceReaderIO(original)
outcome := sequenced(Config1{value1: 0})(Config2{value2: ""})(ctx)()
assert.Equal(t, result.Of(0), outcome)
})
t.Run("executes IO effects correctly", func(t *testing.T) {
ctx := t.Context()
counter := 0
original := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
return func() Result[ReaderIO[Config1, int]] {
return result.Of(func(cfg1 Config1) io.IO[int] {
return func() int {
counter++
return cfg1.value1 + len(cfg2.value2)
}
})
}
}
}
sequenced := SequenceReaderIO(original)
cfg1 := Config1{value1: 10}
cfg2 := Config2{value2: "hello"}
// Each execution should increment counter
counter = 0
result1 := sequenced(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of(15), result1)
assert.Equal(t, 1, counter)
result2 := sequenced(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of(15), result2)
assert.Equal(t, 2, counter)
})
}
func TestTraverse(t *testing.T) {
t.Run("transforms and swaps parameter order", func(t *testing.T) {
ctx := t.Context()
// Original computation depending on Config2
original := Of[Config2](42)
// Transformation that introduces Config1 dependency
transform := func(n int) ReaderReaderIOResult[Config1, string] {
return func(cfg1 Config1) RIORES.ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("value=%d, cfg1=%d", n, cfg1.value1))
}
}
}
}
// Apply traverse to swap order and transform
traversed := Traverse[Config2](transform)(original)
cfg1 := Config1{value1: 100}
cfg2 := Config2{value2: "test"}
outcome := traversed(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of("value=42, cfg1=100"), outcome)
})
t.Run("preserves error handling in original", func(t *testing.T) {
ctx := t.Context()
testErr := errors.New("test error")
original := Left[Config2, int](testErr)
transform := func(n int) ReaderReaderIOResult[Config1, string] {
return Of[Config1](fmt.Sprintf("%d", n))
}
traversed := Traverse[Config2](transform)(original)
outcome := traversed(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Left[string](testErr), outcome)
})
t.Run("preserves error handling in transformation", func(t *testing.T) {
ctx := t.Context()
original := Of[Config2](42)
testErr := errors.New("transform error")
transform := func(n int) ReaderReaderIOResult[Config1, string] {
if n < 0 {
return Left[Config1, string](testErr)
}
return Of[Config1](fmt.Sprintf("%d", n))
}
// Test with negative value
originalNeg := Of[Config2](-1)
traversedNeg := Traverse[Config2](transform)(originalNeg)
resultNeg := traversedNeg(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Left[string](testErr), resultNeg)
// Test with positive value
traversedPos := Traverse[Config2](transform)(original)
resultPos := traversedPos(Config1{value1: 100})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of("42"), resultPos)
})
t.Run("works with complex transformations", func(t *testing.T) {
ctx := t.Context()
original := Of[Config2](10)
transform := func(n int) ReaderReaderIOResult[Config1, int] {
return func(cfg1 Config1) RIORES.ReaderIOResult[int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
return result.Of(n * cfg1.value1)
}
}
}
}
traversed := Traverse[Config2](transform)(original)
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of(50), outcome)
})
t.Run("can be composed with other operations", func(t *testing.T) {
ctx := t.Context()
original := Of[Config2](10)
transform := func(n int) ReaderReaderIOResult[Config1, int] {
return Of[Config1](n * 2)
}
outcome := F.Pipe2(
original,
Traverse[Config2](transform),
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
return k(Config1{value1: 5})
},
)
res := outcome(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of(20), res)
})
}
func TestTraverseReader(t *testing.T) {
t.Run("transforms with pure Reader and swaps parameter order", func(t *testing.T) {
ctx := t.Context()
// Original computation depending on Config2
original := Of[Config2](100)
// Pure Reader transformation that introduces Config1 dependency
formatWithConfig := func(value int) reader.Reader[Config1, string] {
return func(cfg1 Config1) string {
return fmt.Sprintf("value=%d, multiplier=%d, result=%d", value, cfg1.value1, value*cfg1.value1)
}
}
// Apply traverse to introduce Config1 and swap order
traversed := TraverseReader[Config2](formatWithConfig)(original)
cfg1 := Config1{value1: 5}
cfg2 := Config2{value2: "test"}
outcome := traversed(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of("value=100, multiplier=5, result=500"), outcome)
})
t.Run("preserves error handling", func(t *testing.T) {
ctx := t.Context()
testErr := errors.New("test error")
original := Left[Config2, int](testErr)
transform := func(n int) reader.Reader[Config1, string] {
return reader.Of[Config1](fmt.Sprintf("%d", n))
}
traversed := TraverseReader[Config2](transform)(original)
outcome := traversed(Config1{value1: 5})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Left[string](testErr), outcome)
})
t.Run("works with pure computations", func(t *testing.T) {
ctx := t.Context()
original := Of[Config2](42)
// Pure transformation using Reader
double := func(n int) reader.Reader[Config1, int] {
return func(cfg1 Config1) int {
return n * cfg1.value1
}
}
traversed := TraverseReader[Config2](double)(original)
outcome := traversed(Config1{value1: 3})(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of(126), outcome)
})
t.Run("works with zero values", func(t *testing.T) {
ctx := t.Context()
original := Of[Config2](0)
transform := func(n int) reader.Reader[Config1, int] {
return func(cfg1 Config1) int {
return n + cfg1.value1
}
}
traversed := TraverseReader[Config2](transform)(original)
outcome := traversed(Config1{value1: 0})(Config2{value2: ""})(ctx)()
assert.Equal(t, result.Of(0), outcome)
})
t.Run("maintains referential transparency", func(t *testing.T) {
ctx := t.Context()
original := Of[Config2](10)
transform := func(n int) reader.Reader[Config1, int] {
return func(cfg1 Config1) int {
return n * cfg1.value1
}
}
traversed := TraverseReader[Config2](transform)(original)
cfg1 := Config1{value1: 5}
cfg2 := Config2{value2: "test"}
// Call multiple times with same inputs
for range 5 {
outcome := traversed(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of(50), outcome)
}
})
t.Run("can be used in composition", func(t *testing.T) {
ctx := t.Context()
original := Of[Config2](10)
multiply := func(n int) reader.Reader[Config1, int] {
return func(cfg1 Config1) int {
return n * cfg1.value1
}
}
outcome := F.Pipe2(
original,
TraverseReader[Config2](multiply),
func(k Kleisli[Config2, Config1, int]) ReaderReaderIOResult[Config2, int] {
return k(Config1{value1: 3})
},
)
res := outcome(Config2{value2: "test"})(ctx)()
assert.Equal(t, result.Of(30), res)
})
}
func TestFlipIntegration(t *testing.T) {
t.Run("Sequence and Traverse work together", func(t *testing.T) {
ctx := t.Context()
// Create a nested computation
nested := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
return func() Result[ReaderReaderIOResult[Config1, int]] {
return result.Of(Of[Config1](len(cfg2.value2)))
}
}
}
// Sequence it
sequenced := Sequence(nested)
// Then traverse with a transformation
transform := func(n int) ReaderReaderIOResult[Config1, string] {
return Of[Config1](fmt.Sprintf("length=%d", n))
}
// Apply both operations
cfg1 := Config1{value1: 10}
cfg2 := Config2{value2: "hello"}
// First sequence
intermediate := sequenced(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of(5), intermediate)
// Then apply traverse on a new computation
original := Of[Config2](5)
traversed := Traverse[Config2](transform)(original)
outcome := traversed(cfg1)(cfg2)(ctx)()
assert.Equal(t, result.Of("length=5"), outcome)
})
t.Run("all flip functions preserve error semantics", func(t *testing.T) {
ctx := t.Context()
testErr := errors.New("test error")
cfg1 := Config1{value1: 10}
cfg2 := Config2{value2: "test"}
// Test Sequence with error
seqErr := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderReaderIOResult[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderReaderIOResult[Config1, int]] {
return func() Result[ReaderReaderIOResult[Config1, int]] {
return result.Left[ReaderReaderIOResult[Config1, int]](testErr)
}
}
}
seqResult := Sequence(seqErr)(cfg1)(cfg2)(ctx)()
assert.True(t, result.IsLeft(seqResult))
// Test SequenceReader with error
seqReaderErr := func(cfg2 Config2) RIORES.ReaderIOResult[Reader[Config1, int]] {
return func(ctx context.Context) IOResult[Reader[Config1, int]] {
return func() Result[Reader[Config1, int]] {
return result.Left[Reader[Config1, int]](testErr)
}
}
}
seqReaderResult := SequenceReader(seqReaderErr)(cfg1)(cfg2)(ctx)()
assert.True(t, result.IsLeft(seqReaderResult))
// Test SequenceReaderIO with error
seqReaderIOErr := func(cfg2 Config2) RIORES.ReaderIOResult[ReaderIO[Config1, int]] {
return func(ctx context.Context) IOResult[ReaderIO[Config1, int]] {
return func() Result[ReaderIO[Config1, int]] {
return result.Left[ReaderIO[Config1, int]](testErr)
}
}
}
seqReaderIOResult := SequenceReaderIO(seqReaderIOErr)(cfg1)(cfg2)(ctx)()
assert.True(t, result.IsLeft(seqReaderIOResult))
// Test Traverse with error
travErr := Left[Config2, int](testErr)
travTransform := func(n int) ReaderReaderIOResult[Config1, string] {
return Of[Config1](fmt.Sprintf("%d", n))
}
travResult := Traverse[Config2](travTransform)(travErr)(cfg1)(cfg2)(ctx)()
assert.True(t, result.IsLeft(travResult))
// Test TraverseReader with error
travReaderErr := Left[Config2, int](testErr)
travReaderTransform := func(n int) reader.Reader[Config1, string] {
return reader.Of[Config1](fmt.Sprintf("%d", n))
}
travReaderResult := TraverseReader[Config2](travReaderTransform)(travReaderErr)(cfg1)(cfg2)(ctx)()
assert.True(t, result.IsLeft(travReaderResult))
})
}

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2023 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"github.com/IBM/fp-go/v2/monoid"
)
type (
// Monoid represents a monoid structure for ReaderReaderIOResult[R, A].
// A monoid provides an identity element (empty) and an associative binary operation (concat).
Monoid[R, A any] = monoid.Monoid[ReaderReaderIOResult[R, A]]
)
// ApplicativeMonoid creates a monoid for ReaderReaderIOResult using applicative composition.
// It combines values using the provided monoid m and the applicative Ap operation.
// This allows combining multiple ReaderReaderIOResult values in parallel while merging their results.
//
// The resulting monoid satisfies:
// - Identity: concat(empty, x) = concat(x, empty) = x
// - Associativity: concat(concat(x, y), z) = concat(x, concat(y, z))
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
// import "github.com/IBM/fp-go/v2/number"
//
// // Create a monoid for combining integers with addition
// intMonoid := ApplicativeMonoid[Config](number.MonoidSum)
//
// // Combine multiple computations
// result := intMonoid.Concat(
// Of[Config](10),
// intMonoid.Concat(Of[Config](20), Of[Config](30)),
// ) // Results in 60
func ApplicativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
return monoid.ApplicativeMonoid(
Of[R, A],
MonadMap[R, A, func(A) A],
MonadAp[R, A, A],
m,
)
}
// ApplicativeMonoidSeq creates a monoid for ReaderReaderIOResult using sequential applicative composition.
// Similar to ApplicativeMonoid but evaluates effects sequentially rather than in parallel.
//
// Use this when:
// - Effects must be executed in a specific order
// - Side effects depend on sequential execution
// - You want to avoid concurrent execution
func ApplicativeMonoidSeq[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
return monoid.ApplicativeMonoid(
Of[R, A],
MonadMap[R, A, func(A) A],
MonadApSeq[R, A, A],
m,
)
}
// ApplicativeMonoidPar creates a monoid for ReaderReaderIOResult using parallel applicative composition.
// Similar to ApplicativeMonoid but explicitly evaluates effects in parallel.
//
// Use this when:
// - Effects are independent and can run concurrently
// - You want to maximize performance through parallelism
// - Order of execution doesn't matter
func ApplicativeMonoidPar[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
return monoid.ApplicativeMonoid(
Of[R, A],
MonadMap[R, A, func(A) A],
MonadApPar[R, A, A],
m,
)
}
// AlternativeMonoid creates a monoid that combines ReaderReaderIOResult values using both
// applicative composition and alternative (Alt) semantics.
//
// This monoid:
// - Uses Ap for combining successful values
// - Uses Alt for handling failures (tries alternatives on failure)
// - Provides a way to combine multiple computations with fallback behavior
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
// import "github.com/IBM/fp-go/v2/number"
//
// intMonoid := AlternativeMonoid[Config](number.MonoidSum)
//
// // If first computation fails, tries the second
// result := intMonoid.Concat(
// Left[Config, int](errors.New("failed")),
// Of[Config](42),
// ) // Results in Right(42)
func AlternativeMonoid[R, A any](m monoid.Monoid[A]) Monoid[R, A] {
return monoid.AlternativeMonoid(
Of[R, A],
MonadMap[R, A, func(A) A],
MonadAp[R, A, A],
MonadAlt[R, A],
m,
)
}
// AltMonoid creates a monoid based solely on the Alt operation.
// It provides a way to chain computations with fallback behavior.
//
// The monoid:
// - Uses the provided zero as the identity element
// - Uses Alt for concatenation (tries first, falls back to second on failure)
// - Implements a "first success" strategy
//
// Example:
//
// zero := func() ReaderReaderIOResult[Config, int] {
// return Left[Config, int](errors.New("no value"))
// }
// altMonoid := AltMonoid[Config, int](zero)
//
// // Tries computations in order until one succeeds
// result := altMonoid.Concat(
// Left[Config, int](errors.New("first failed")),
// altMonoid.Concat(
// Left[Config, int](errors.New("second failed")),
// Of[Config](42),
// ),
// ) // Results in Right(42)
func AltMonoid[R, A any](zero Lazy[ReaderReaderIOResult[R, A]]) Monoid[R, A] {
return monoid.AltMonoid(
zero,
MonadAlt[R, A],
)
}

View File

@@ -0,0 +1,337 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"errors"
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
var (
intAddMonoid = N.MonoidSum[int]()
strMonoid = S.Monoid
testError = errors.New("test error")
)
func TestApplicativeMonoid(t *testing.T) {
rrMonoid := ApplicativeMonoid[AppConfig](intAddMonoid)
cfg := defaultConfig
ctx := t.Context()
t.Run("empty element", func(t *testing.T) {
empty := rrMonoid.Empty()
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
})
t.Run("concat two success values", func(t *testing.T) {
rr1 := Of[AppConfig](5)
rr2 := Of[AppConfig](3)
combined := rrMonoid.Concat(rr1, rr2)
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
})
t.Run("concat with empty", func(t *testing.T) {
rr := Of[AppConfig](42)
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
assert.Equal(t, result.Of(42), combined1(cfg)(ctx)())
assert.Equal(t, result.Of(42), combined2(cfg)(ctx)())
})
t.Run("concat with left failure", func(t *testing.T) {
rrSuccess := Of[AppConfig](5)
rrFailure := Left[AppConfig, int](testError)
combined := rrMonoid.Concat(rrFailure, rrSuccess)
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
})
t.Run("concat with right failure", func(t *testing.T) {
rrSuccess := Of[AppConfig](5)
rrFailure := Left[AppConfig, int](testError)
combined := rrMonoid.Concat(rrSuccess, rrFailure)
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
})
t.Run("concat multiple values", func(t *testing.T) {
rr1 := Of[AppConfig](1)
rr2 := Of[AppConfig](2)
rr3 := Of[AppConfig](3)
rr4 := Of[AppConfig](4)
// Chain concat calls: ((1 + 2) + 3) + 4
combined := rrMonoid.Concat(
rrMonoid.Concat(
rrMonoid.Concat(rr1, rr2),
rr3,
),
rr4,
)
assert.Equal(t, result.Of(10), combined(cfg)(ctx)())
})
t.Run("string concatenation", func(t *testing.T) {
strRRMonoid := ApplicativeMonoid[AppConfig](strMonoid)
rr1 := Of[AppConfig]("Hello")
rr2 := Of[AppConfig](" ")
rr3 := Of[AppConfig]("World")
combined := strRRMonoid.Concat(
strRRMonoid.Concat(rr1, rr2),
rr3,
)
assert.Equal(t, result.Of("Hello World"), combined(cfg)(ctx)())
})
}
func TestApplicativeMonoidSeq(t *testing.T) {
rrMonoid := ApplicativeMonoidSeq[AppConfig](intAddMonoid)
cfg := defaultConfig
ctx := t.Context()
t.Run("empty element", func(t *testing.T) {
empty := rrMonoid.Empty()
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
})
t.Run("concat two success values", func(t *testing.T) {
rr1 := Of[AppConfig](5)
rr2 := Of[AppConfig](3)
combined := rrMonoid.Concat(rr1, rr2)
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
})
t.Run("concat with failure", func(t *testing.T) {
rrSuccess := Of[AppConfig](5)
rrFailure := Left[AppConfig, int](testError)
combined := rrMonoid.Concat(rrFailure, rrSuccess)
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
})
}
func TestApplicativeMonoidPar(t *testing.T) {
rrMonoid := ApplicativeMonoidPar[AppConfig](intAddMonoid)
cfg := defaultConfig
ctx := t.Context()
t.Run("empty element", func(t *testing.T) {
empty := rrMonoid.Empty()
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
})
t.Run("concat two success values", func(t *testing.T) {
rr1 := Of[AppConfig](5)
rr2 := Of[AppConfig](3)
combined := rrMonoid.Concat(rr1, rr2)
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
})
t.Run("concat with failure", func(t *testing.T) {
rrSuccess := Of[AppConfig](5)
rrFailure := Left[AppConfig, int](testError)
combined := rrMonoid.Concat(rrFailure, rrSuccess)
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
})
}
func TestAltMonoid(t *testing.T) {
zero := func() ReaderReaderIOResult[AppConfig, int] {
return Left[AppConfig, int](errors.New("empty"))
}
rrMonoid := AltMonoid(zero)
cfg := defaultConfig
ctx := t.Context()
t.Run("empty element", func(t *testing.T) {
empty := rrMonoid.Empty()
assert.True(t, result.IsLeft(empty(cfg)(ctx)()))
})
t.Run("concat two success values - uses first", func(t *testing.T) {
rr1 := Of[AppConfig](5)
rr2 := Of[AppConfig](3)
combined := rrMonoid.Concat(rr1, rr2)
// AltMonoid takes the first successful value
assert.Equal(t, result.Of(5), combined(cfg)(ctx)())
})
t.Run("concat failure then success", func(t *testing.T) {
rrFailure := Left[AppConfig, int](testError)
rrSuccess := Of[AppConfig](42)
combined := rrMonoid.Concat(rrFailure, rrSuccess)
// Should fall back to second when first fails
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
})
t.Run("concat success then failure", func(t *testing.T) {
rrSuccess := Of[AppConfig](42)
rrFailure := Left[AppConfig, int](testError)
combined := rrMonoid.Concat(rrSuccess, rrFailure)
// Should use first successful value
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
})
t.Run("concat two failures", func(t *testing.T) {
err1 := errors.New("error 1")
err2 := errors.New("error 2")
rr1 := Left[AppConfig, int](err1)
rr2 := Left[AppConfig, int](err2)
combined := rrMonoid.Concat(rr1, rr2)
// Should use second error when both fail
assert.True(t, result.IsLeft(combined(cfg)(ctx)()))
})
t.Run("concat with empty", func(t *testing.T) {
rr := Of[AppConfig](42)
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
assert.Equal(t, result.Of(42), combined1(cfg)(ctx)())
assert.Equal(t, result.Of(42), combined2(cfg)(ctx)())
})
t.Run("fallback chain", func(t *testing.T) {
// Simulate trying multiple sources until one succeeds
primary := Left[AppConfig, string](errors.New("primary failed"))
secondary := Left[AppConfig, string](errors.New("secondary failed"))
tertiary := Of[AppConfig]("tertiary success")
strZero := func() ReaderReaderIOResult[AppConfig, string] {
return Left[AppConfig, string](errors.New("all failed"))
}
strMonoid := AltMonoid(strZero)
// Chain concat: try primary, then secondary, then tertiary
combined := strMonoid.Concat(
strMonoid.Concat(primary, secondary),
tertiary,
)
assert.Equal(t, result.Of("tertiary success"), combined(cfg)(ctx)())
})
}
func TestAlternativeMonoid(t *testing.T) {
rrMonoid := AlternativeMonoid[AppConfig](intAddMonoid)
cfg := defaultConfig
ctx := t.Context()
t.Run("empty element", func(t *testing.T) {
empty := rrMonoid.Empty()
assert.Equal(t, result.Of(0), empty(cfg)(ctx)())
})
t.Run("concat two success values", func(t *testing.T) {
rr1 := Of[AppConfig](5)
rr2 := Of[AppConfig](3)
combined := rrMonoid.Concat(rr1, rr2)
assert.Equal(t, result.Of(8), combined(cfg)(ctx)())
})
t.Run("concat failure then success", func(t *testing.T) {
rrFailure := Left[AppConfig, int](testError)
rrSuccess := Of[AppConfig](42)
combined := rrMonoid.Concat(rrFailure, rrSuccess)
// Alternative falls back to second when first fails
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
})
t.Run("concat success then failure", func(t *testing.T) {
rrSuccess := Of[AppConfig](42)
rrFailure := Left[AppConfig, int](testError)
combined := rrMonoid.Concat(rrSuccess, rrFailure)
// Should use first successful value
assert.Equal(t, result.Of(42), combined(cfg)(ctx)())
})
t.Run("concat with empty", func(t *testing.T) {
rr := Of[AppConfig](42)
combined1 := rrMonoid.Concat(rr, rrMonoid.Empty())
combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr)
assert.Equal(t, result.Of(42), combined1(cfg)(ctx)())
assert.Equal(t, result.Of(42), combined2(cfg)(ctx)())
})
t.Run("multiple values with some failures", func(t *testing.T) {
rr1 := Left[AppConfig, int](errors.New("fail 1"))
rr2 := Of[AppConfig](5)
rr3 := Left[AppConfig, int](errors.New("fail 2"))
rr4 := Of[AppConfig](10)
// Alternative should skip failures and accumulate successes
combined := rrMonoid.Concat(
rrMonoid.Concat(
rrMonoid.Concat(rr1, rr2),
rr3,
),
rr4,
)
// Should accumulate successful values: 5 + 10 = 15
assert.Equal(t, result.Of(15), combined(cfg)(ctx)())
})
}
// Test monoid laws
func TestMonoidLaws(t *testing.T) {
rrMonoid := ApplicativeMonoid[AppConfig](intAddMonoid)
cfg := defaultConfig
ctx := t.Context()
// Left identity: empty <> x == x
t.Run("left identity", func(t *testing.T) {
x := Of[AppConfig](42)
result1 := rrMonoid.Concat(rrMonoid.Empty(), x)(cfg)(ctx)()
result2 := x(cfg)(ctx)()
assert.Equal(t, result2, result1)
})
// Right identity: x <> empty == x
t.Run("right identity", func(t *testing.T) {
x := Of[AppConfig](42)
result1 := rrMonoid.Concat(x, rrMonoid.Empty())(cfg)(ctx)()
result2 := x(cfg)(ctx)()
assert.Equal(t, result2, result1)
})
// Associativity: (x <> y) <> z == x <> (y <> z)
t.Run("associativity", func(t *testing.T) {
x := Of[AppConfig](1)
y := Of[AppConfig](2)
z := Of[AppConfig](3)
left := rrMonoid.Concat(rrMonoid.Concat(x, y), z)(cfg)(ctx)()
right := rrMonoid.Concat(x, rrMonoid.Concat(y, z))(cfg)(ctx)()
assert.Equal(t, right, left)
})
}

View File

@@ -0,0 +1,894 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"time"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/fromeither"
"github.com/IBM/fp-go/v2/internal/fromio"
"github.com/IBM/fp-go/v2/internal/fromioeither"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
)
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
// If the option is None, it uses the provided onNone function to generate an error.
//
//go:inline
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
return RRIOE.FromReaderOption[R, context.Context, A](onNone)
}
// FromReaderIOResult lifts a ReaderIOResult into a ReaderReaderIOResult.
// This adds an additional reader layer to the computation.
//
//go:inline
func FromReaderIOResult[R, A any](ma ReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReaderIOEither[context.Context](ma)
}
// FromReaderIO lifts a ReaderIO into a ReaderReaderIOResult.
// The IO computation is wrapped in a Right (success) value.
//
//go:inline
func FromReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReaderIO[context.Context, error](ma)
}
// RightReaderIO lifts a ReaderIO into a ReaderReaderIOResult as a Right (success) value.
// Alias for FromReaderIO.
//
//go:inline
func RightReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.RightReaderIO[context.Context, error](ma)
}
// LeftReaderIO lifts a ReaderIO that produces an error into a ReaderReaderIOResult as a Left (failure) value.
//
//go:inline
func LeftReaderIO[A, R any](me ReaderIO[R, error]) ReaderReaderIOResult[R, A] {
return RRIOE.LeftReaderIO[context.Context, A](me)
}
// MonadMap applies a function to the value inside a ReaderReaderIOResult (Functor operation).
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadMap[R, A, B any](fa ReaderReaderIOResult[R, A], f func(A) B) ReaderReaderIOResult[R, B] {
return reader.MonadMap(fa, RIOE.Map(f))
}
// Map applies a function to the value inside a ReaderReaderIOResult (Functor operation).
// This is the curried version that returns an operator.
//
//go:inline
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
return reader.Map[R](RIOE.Map(f))
}
// MonadMapTo replaces the value inside a ReaderReaderIOResult with a constant value.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadMapTo[R, A, B any](fa ReaderReaderIOResult[R, A], b B) ReaderReaderIOResult[R, B] {
return reader.MonadMap(fa, RIOE.MapTo[A](b))
}
// MapTo replaces the value inside a ReaderReaderIOResult with a constant value.
// This is the curried version that returns an operator.
//
//go:inline
func MapTo[R, A, B any](b B) Operator[R, A, B] {
return reader.Map[R](RIOE.MapTo[A](b))
}
// MonadChain sequences two computations, where the second depends on the result of the first (Monad operation).
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChain[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
return readert.MonadChain(
RIOE.MonadChain[A, B],
fa,
f,
)
}
// MonadChainFirst sequences two computations but returns the result of the first.
// Useful for performing side effects while preserving the original value.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainFirst[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
return chain.MonadChainFirst(
MonadChain[R, A, A],
MonadMap[R, B, A],
fa,
f)
}
// MonadTap is an alias for MonadChainFirst.
// Executes a side effect while preserving the original value.
//
//go:inline
func MonadTap[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirst(fa, f)
}
// MonadChainEitherK chains a computation that returns an Either.
// The Either is automatically lifted into ReaderReaderIOResult.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, B] {
return fromeither.MonadChainEitherK(
MonadChain[R, A, B],
FromEither[R, B],
ma,
f,
)
}
// ChainEitherK chains a computation that returns an Either.
// The Either is automatically lifted into ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B] {
return fromeither.ChainEitherK(
Chain[R, A, B],
FromEither[R, B],
f,
)
}
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
// Useful for validation or side effects that may fail.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainFirstEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, A] {
return fromeither.MonadChainFirstEitherK(
MonadChain[R, A, A],
MonadMap[R, B, A],
FromEither[R, B],
ma,
f,
)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK.
// Executes an Either-returning side effect while preserving the original value.
//
//go:inline
func MonadTapEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f either.Kleisli[error, A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirstEitherK(ma, f)
}
// ChainFirstEitherK chains a computation that returns an Either but preserves the original value.
// This is the curried version that returns an operator.
//
//go:inline
func ChainFirstEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
return fromeither.ChainFirstEitherK(
Chain[R, A, A],
Map[R, B, A],
FromEither[R, B],
f,
)
}
// TapEitherK is an alias for ChainFirstEitherK.
// Executes an Either-returning side effect while preserving the original value.
//
//go:inline
func TapEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
return ChainFirstEitherK[R](f)
}
// MonadChainReaderK chains a computation that returns a Reader.
// The Reader is automatically lifted into ReaderReaderIOResult.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, A, B],
FromReader[R, B],
ma,
f,
)
}
// ChainReaderK chains a computation that returns a Reader.
// The Reader is automatically lifted into ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain[R, A, B],
FromReader[R, B],
f,
)
}
// MonadChainFirstReaderK chains a computation that returns a Reader but preserves the original value.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainFirstReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReader[R, B],
ma,
f,
)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK.
// Executes a Reader-returning side effect while preserving the original value.
//
//go:inline
func MonadTapReaderK[R, A, B any](ma ReaderReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirstReaderK(ma, f)
}
// ChainFirstReaderK chains a computation that returns a Reader but preserves the original value.
// This is the curried version that returns an operator.
//
//go:inline
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReader[R, B],
f,
)
}
// TapReaderK is an alias for ChainFirstReaderK.
// Executes a Reader-returning side effect while preserving the original value.
//
//go:inline
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderK(f)
}
// MonadChainReaderIOK chains a computation that returns a ReaderIO.
// The ReaderIO is automatically lifted into ReaderReaderIOResult.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, A, B],
FromReaderIO[R, B],
ma,
f,
)
}
// ChainReaderIOK chains a computation that returns a ReaderIO.
// The ReaderIO is automatically lifted into ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain[R, A, B],
FromReaderIO[R, B],
f,
)
}
// MonadChainFirstReaderIOK chains a computation that returns a ReaderIO but preserves the original value.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReaderIO[R, B],
ma,
f,
)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK.
// Executes a ReaderIO-returning side effect while preserving the original value.
//
//go:inline
func MonadTapReaderIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirstReaderIOK(ma, f)
}
// ChainFirstReaderIOK chains a computation that returns a ReaderIO but preserves the original value.
// This is the curried version that returns an operator.
//
//go:inline
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReaderIO[R, B],
f,
)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK.
// Executes a ReaderIO-returning side effect while preserving the original value.
//
//go:inline
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderIOK(f)
}
// MonadChainReaderEitherK chains a computation that returns a ReaderEither.
// The ReaderEither is automatically lifted into ReaderReaderIOResult.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, A, B],
FromReaderEither[R, B],
ma,
f,
)
}
// ChainReaderEitherK chains a computation that returns a ReaderEither.
// The ReaderEither is automatically lifted into ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain[R, A, B],
FromReaderEither[R, B],
f,
)
}
// MonadChainFirstReaderEitherK chains a computation that returns a ReaderEither but preserves the original value.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReaderEither[R, B],
ma,
f,
)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK.
// Executes a ReaderEither-returning side effect while preserving the original value.
//
//go:inline
func MonadTapReaderEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirstReaderEitherK(ma, f)
}
// ChainFirstReaderEitherK chains a computation that returns a ReaderEither but preserves the original value.
// This is the curried version that returns an operator.
//
//go:inline
func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReaderEither[R, B],
f,
)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK.
// Executes a ReaderEither-returning side effect while preserving the original value.
//
//go:inline
func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return ChainFirstReaderEitherK(f)
}
// ChainReaderOptionK chains a computation that returns a ReaderOption.
// If the option is None, it uses the provided onNone function to generate an error.
// Returns a function that takes a ReaderOption Kleisli and returns an operator.
//
//go:inline
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
return RRIOE.ChainReaderOptionK[R, context.Context, A, B](onNone)
}
// ChainFirstReaderOptionK chains a computation that returns a ReaderOption but preserves the original value.
// If the option is None, it uses the provided onNone function to generate an error.
// Returns a function that takes a ReaderOption Kleisli and returns an operator.
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RRIOE.ChainFirstReaderOptionK[R, context.Context, A, B](onNone)
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK.
// Executes a ReaderOption-returning side effect while preserving the original value.
//
//go:inline
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderOptionK[R, A, B](onNone)
}
// MonadChainIOEitherK chains a computation that returns an IOEither.
// The IOEither is automatically lifted into ReaderReaderIOResult.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainIOEitherK[R, A, B any](ma ReaderReaderIOResult[R, A], f IOE.Kleisli[error, A, B]) ReaderReaderIOResult[R, B] {
return fromioeither.MonadChainIOEitherK(
MonadChain[R, A, B],
FromIOEither[R, B],
ma,
f,
)
}
// ChainIOEitherK chains a computation that returns an IOEither.
// The IOEither is automatically lifted into ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func ChainIOEitherK[R, A, B any](f IOE.Kleisli[error, A, B]) Operator[R, A, B] {
return fromioeither.ChainIOEitherK(
Chain[R, A, B],
FromIOEither[R, B],
f,
)
}
// MonadChainIOK chains a computation that returns an IO.
// The IO is automatically lifted into ReaderReaderIOResult.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, B] {
return fromio.MonadChainIOK(
MonadChain[R, A, B],
FromIO[R, B],
ma,
f,
)
}
// ChainIOK chains a computation that returns an IO.
// The IO is automatically lifted into ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func ChainIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, B] {
return fromio.ChainIOK(
Chain[R, A, B],
FromIO[R, B],
f,
)
}
// MonadChainFirstIOK chains a computation that returns an IO but preserves the original value.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainFirstIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, A] {
return fromio.MonadChainFirstIOK(
MonadChain[R, A, A],
MonadMap[R, B, A],
FromIO[R, B],
ma,
f,
)
}
// MonadTapIOK is an alias for MonadChainFirstIOK.
// Executes an IO-returning side effect while preserving the original value.
//
//go:inline
func MonadTapIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirstIOK(ma, f)
}
// ChainFirstIOK chains a computation that returns an IO but preserves the original value.
// This is the curried version that returns an operator.
//
//go:inline
func ChainFirstIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return fromio.ChainFirstIOK(
Chain[R, A, A],
Map[R, B, A],
FromIO[R, B],
f,
)
}
// TapIOK is an alias for ChainFirstIOK.
// Executes an IO-returning side effect while preserving the original value.
//
//go:inline
func TapIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return ChainFirstIOK[R](f)
}
// ChainOptionK chains a computation that returns an Option.
// If the option is None, it uses the provided onNone function to generate an error.
// Returns a function that takes an Option Kleisli and returns an operator.
//
//go:inline
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[R, A, B] {
return fromeither.ChainOptionK(
MonadChain[R, A, B],
FromEither[R, B],
onNone,
)
}
// MonadAp applies a function wrapped in a ReaderReaderIOResult to a value wrapped in a ReaderReaderIOResult (Applicative operation).
// This is the monadic version that takes both computations as parameters.
//
//go:inline
func MonadAp[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
return readert.MonadAp[
ReaderReaderIOResult[R, A],
ReaderReaderIOResult[R, B],
ReaderReaderIOResult[R, func(A) B], R, A](
RIOE.MonadAp[B, A],
fab,
fa,
)
}
// MonadApSeq is like MonadAp but evaluates effects sequentially.
//
//go:inline
func MonadApSeq[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
return readert.MonadAp[
ReaderReaderIOResult[R, A],
ReaderReaderIOResult[R, B],
ReaderReaderIOResult[R, func(A) B], R, A](
RIOE.MonadApSeq[B, A],
fab,
fa,
)
}
// MonadApPar is like MonadAp but evaluates effects in parallel.
//
//go:inline
func MonadApPar[R, A, B any](fab ReaderReaderIOResult[R, func(A) B], fa ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, B] {
return readert.MonadAp[
ReaderReaderIOResult[R, A],
ReaderReaderIOResult[R, B],
ReaderReaderIOResult[R, func(A) B], R, A](
RIOE.MonadApPar[B, A],
fab,
fa,
)
}
// Ap applies a function wrapped in a ReaderReaderIOResult to a value wrapped in a ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func Ap[B, R, A any](fa ReaderReaderIOResult[R, A]) Operator[R, func(A) B, B] {
return readert.Ap[
ReaderReaderIOResult[R, A],
ReaderReaderIOResult[R, B],
ReaderReaderIOResult[R, func(A) B], R, A](
RIOE.Ap[B, A],
fa,
)
}
// Chain sequences two computations, where the second depends on the result of the first (Monad operation).
// This is the curried version that returns an operator.
//
//go:inline
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
return readert.Chain[ReaderReaderIOResult[R, A]](
RIOE.Chain[A, B],
f,
)
}
// ChainFirst sequences two computations but returns the result of the first.
// This is the curried version that returns an operator.
//
//go:inline
func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return chain.ChainFirst(
Chain[R, A, A],
Map[R, B, A],
f)
}
// Tap is an alias for ChainFirst.
// Executes a side effect while preserving the original value.
//
//go:inline
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirst(f)
}
// Right creates a ReaderReaderIOResult that succeeds with the given value.
// This is the success constructor for the Result type.
//
//go:inline
func Right[R, A any](a A) ReaderReaderIOResult[R, A] {
return RRIOE.Right[R, context.Context, error](a)
}
// Left creates a ReaderReaderIOResult that fails with the given error.
// This is the failure constructor for the Result type.
//
//go:inline
func Left[R, A any](e error) ReaderReaderIOResult[R, A] {
return RRIOE.Left[R, context.Context, A](e)
}
// Of creates a ReaderReaderIOResult that succeeds with the given value (Pointed operation).
// Alias for Right.
//
//go:inline
func Of[R, A any](a A) ReaderReaderIOResult[R, A] {
return RRIOE.Of[R, context.Context, error](a)
}
// Flatten removes one level of nesting from a nested ReaderReaderIOResult.
// Converts ReaderReaderIOResult[R, ReaderReaderIOResult[R, A]] to ReaderReaderIOResult[R, A].
//
//go:inline
func Flatten[R, A any](mma ReaderReaderIOResult[R, ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
return MonadChain(mma, function.Identity[ReaderReaderIOResult[R, A]])
}
// FromEither lifts an Either into a ReaderReaderIOResult.
//
//go:inline
func FromEither[R, A any](t Either[error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromEither[R, context.Context](t)
}
// FromResult lifts a Result into a ReaderReaderIOResult.
// Alias for FromEither since Result is Either[error, A].
//
//go:inline
func FromResult[R, A any](t Result[A]) ReaderReaderIOResult[R, A] {
return FromEither[R](t)
}
// RightReader lifts a Reader into a ReaderReaderIOResult as a Right (success) value.
//
//go:inline
func RightReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.RightReader[context.Context, error](ma)
}
// LeftReader lifts a Reader that produces an error into a ReaderReaderIOResult as a Left (failure) value.
//
//go:inline
func LeftReader[A, R any](ma Reader[R, error]) ReaderReaderIOResult[R, A] {
return RRIOE.LeftReader[context.Context, A](ma)
}
// FromReader lifts a Reader into a ReaderReaderIOResult.
// The Reader's result is wrapped in a Right (success) value.
//
//go:inline
func FromReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReader[context.Context, error](ma)
}
// RightIO lifts an IO into a ReaderReaderIOResult as a Right (success) value.
//
//go:inline
func RightIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
return RRIOE.RightIO[R, context.Context, error](ma)
}
// LeftIO lifts an IO that produces an error into a ReaderReaderIOResult as a Left (failure) value.
//
//go:inline
func LeftIO[R, A any](ma IO[error]) ReaderReaderIOResult[R, A] {
return RRIOE.LeftIO[R, context.Context, A](ma)
}
// FromIO lifts an IO into a ReaderReaderIOResult.
// The IO's result is wrapped in a Right (success) value.
//
//go:inline
func FromIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromIO[R, context.Context, error](ma)
}
// FromIOEither lifts an IOEither into a ReaderReaderIOResult.
//
//go:inline
func FromIOEither[R, A any](ma IOEither[error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromIOEither[R, context.Context](ma)
}
// FromIOResult lifts an IOResult into a ReaderReaderIOResult.
// Alias for FromIOEither since IOResult is IOEither[error, A].
//
//go:inline
func FromIOResult[R, A any](ma IOResult[A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromIOEither[R, context.Context](ma)
}
// FromReaderEither lifts a ReaderEither into a ReaderReaderIOResult.
//
//go:inline
func FromReaderEither[R, A any](ma RE.ReaderEither[R, error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReaderEither[R, context.Context](ma)
}
// Ask retrieves the outer environment R.
// Returns a ReaderReaderIOResult that succeeds with the environment value.
//
//go:inline
func Ask[R any]() ReaderReaderIOResult[R, R] {
return RRIOE.Ask[R, context.Context, error]()
}
// Asks retrieves a value derived from the outer environment R using the provided function.
//
//go:inline
func Asks[R, A any](r Reader[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.Asks[context.Context, error](r)
}
// FromOption converts an Option to a ReaderReaderIOResult.
// If the option is None, it uses the provided onNone function to generate an error.
// Returns a function that takes an Option and returns a ReaderReaderIOResult.
//
//go:inline
func FromOption[R, A any](onNone Lazy[error]) func(Option[A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromOption[R, context.Context, A](onNone)
}
// FromPredicate creates a ReaderReaderIOResult from a predicate.
// If the predicate returns true, the value is wrapped in Right.
// If false, onFalse is called to generate an error wrapped in Left.
//
//go:inline
func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) error) Kleisli[R, A, A] {
return RRIOE.FromPredicate[R, context.Context](pred, onFalse)
}
// MonadAlt provides alternative/fallback behavior.
// If the first computation fails, it tries the second (lazy-evaluated).
// This is the monadic version that takes both computations as parameters.
//
//go:inline
func MonadAlt[R, A any](first ReaderReaderIOResult[R, A], second Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
return RRIOE.MonadAlt(first, second)
}
// Alt provides alternative/fallback behavior.
// If the first computation fails, it tries the second (lazy-evaluated).
// This is the curried version that returns an operator.
//
//go:inline
func Alt[R, A any](second Lazy[ReaderReaderIOResult[R, A]]) Operator[R, A, A] {
return RRIOE.Alt(second)
}
// MonadFlap applies a value to a function wrapped in a ReaderReaderIOResult.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadFlap[R, B, A any](fab ReaderReaderIOResult[R, func(A) B], a A) ReaderReaderIOResult[R, B] {
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
}
// Flap applies a value to a function wrapped in a ReaderReaderIOResult.
// This is the curried version that returns an operator.
//
//go:inline
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
return functor.Flap(Map[R, func(A) B, B], a)
}
// MonadMapLeft transforms the error value if the computation fails.
// Has no effect if the computation succeeds.
// 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] {
return RRIOE.MonadMapLeft(fa, f)
}
// MapLeft transforms the error value if the computation fails.
// Has no effect if the computation succeeds.
// This is the curried version that returns an operator.
//
//go:inline
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
return RRIOE.MapLeft[R, context.Context, A](f)
}
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
//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)
}
// Read provides a specific outer environment value to a computation.
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
//
//go:inline
func Read[A, R any](r R) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
return RRIOE.Read[context.Context, error, A](r)
}
// ReadIOEither provides an outer environment value from an IOEither to a computation.
//
//go:inline
func ReadIOEither[A, R any](rio IOEither[error, R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
return RRIOE.ReadIOEither[A, R, context.Context](rio)
}
// ReadIO provides an outer environment value from an IO to a computation.
//
//go:inline
func ReadIO[A, R any](rio IO[R]) func(ReaderReaderIOResult[R, A]) ReaderIOResult[context.Context, A] {
return RRIOE.ReadIO[context.Context, error, A](rio)
}
// MonadChainLeft handles errors by chaining a recovery computation.
// If the computation fails, the error is passed to f for recovery.
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadChainLeft[R, A any](fa ReaderReaderIOResult[R, A], f Kleisli[R, error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.MonadChainLeft(fa, f)
}
// ChainLeft handles errors by chaining a recovery computation.
// If the computation fails, the error is passed to f for recovery.
// This is the curried version that returns an operator.
//
//go:inline
func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.ChainLeft(f)
}
// Delay adds a time delay before executing the computation.
// Useful for rate limiting, retry backoff, or scheduled execution.
//
//go:inline
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
return reader.Map[R](RIOE.Delay[A](delay))
}

View File

@@ -0,0 +1,718 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"errors"
"testing"
"time"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestOf(t *testing.T) {
computation := Of[AppConfig](42)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestRight(t *testing.T) {
computation := Right[AppConfig](42)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestLeft(t *testing.T) {
err := errors.New("test error")
computation := Left[AppConfig, int](err)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Left[int](err), outcome)
}
func TestMonadMap(t *testing.T) {
computation := MonadMap(
Of[AppConfig](21),
N.Mul(2),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestMap(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
Map[AppConfig](N.Mul(2)),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestMonadMapTo(t *testing.T) {
computation := MonadMapTo(Of[AppConfig](21), 99)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
}
func TestMapTo(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
MapTo[AppConfig, int](99),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
}
func TestMonadChain(t *testing.T) {
computation := MonadChain(
Of[AppConfig](21),
func(n int) ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](n * 2)
},
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestChain(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
Chain(func(n int) ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](n * 2)
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestMonadChainFirst(t *testing.T) {
sideEffect := 0
computation := MonadChainFirst(
Of[AppConfig](42),
func(n int) ReaderReaderIOResult[AppConfig, string] {
sideEffect = n
return Of[AppConfig]("ignored")
},
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 42, sideEffect)
}
func TestChainFirst(t *testing.T) {
sideEffect := 0
computation := F.Pipe1(
Of[AppConfig](42),
ChainFirst(func(n int) ReaderReaderIOResult[AppConfig, string] {
sideEffect = n
return Of[AppConfig]("ignored")
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 42, sideEffect)
}
func TestTap(t *testing.T) {
sideEffect := 0
computation := F.Pipe1(
Of[AppConfig](42),
Tap(func(n int) ReaderReaderIOResult[AppConfig, string] {
sideEffect = n
return Of[AppConfig]("ignored")
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 42, sideEffect)
}
func TestFlatten(t *testing.T) {
nested := Of[AppConfig](Of[AppConfig](42))
computation := Flatten(nested)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestFromEither(t *testing.T) {
t.Run("right", func(t *testing.T) {
computation := FromEither[AppConfig](either.Right[error](42))
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("left", func(t *testing.T) {
err := errors.New("test error")
computation := FromEither[AppConfig](either.Left[int](err))
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestFromResult(t *testing.T) {
t.Run("success", func(t *testing.T) {
computation := FromResult[AppConfig](result.Of(42))
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("error", func(t *testing.T) {
err := errors.New("test error")
computation := FromResult[AppConfig](result.Left[int](err))
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestFromReader(t *testing.T) {
computation := FromReader(func(cfg AppConfig) int {
return len(cfg.DatabaseURL)
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome) // len("postgres://localhost")
}
func TestRightReader(t *testing.T) {
computation := RightReader(func(cfg AppConfig) int {
return len(cfg.LogLevel)
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(4), outcome) // len("info")
}
func TestLeftReader(t *testing.T) {
err := errors.New("test error")
computation := LeftReader[int](func(cfg AppConfig) error {
return err
})
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestFromIO(t *testing.T) {
computation := FromIO[AppConfig](func() int { return 42 })
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestRightIO(t *testing.T) {
computation := RightIO[AppConfig](func() int { return 42 })
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestLeftIO(t *testing.T) {
err := errors.New("test error")
computation := LeftIO[AppConfig, int](func() error { return err })
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestFromIOEither(t *testing.T) {
t.Run("right", func(t *testing.T) {
computation := FromIOEither[AppConfig](ioeither.Of[error](42))
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("left", func(t *testing.T) {
err := errors.New("test error")
computation := FromIOEither[AppConfig](ioeither.Left[int](err))
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestFromIOResult(t *testing.T) {
t.Run("success", func(t *testing.T) {
computation := FromIOResult[AppConfig](func() result.Result[int] {
return result.Of(42)
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("error", func(t *testing.T) {
err := errors.New("test error")
computation := FromIOResult[AppConfig](func() result.Result[int] {
return result.Left[int](err)
})
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestFromReaderIO(t *testing.T) {
computation := FromReaderIO(func(cfg AppConfig) io.IO[int] {
return func() int { return len(cfg.DatabaseURL) }
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
}
func TestRightReaderIO(t *testing.T) {
computation := RightReaderIO(func(cfg AppConfig) io.IO[int] {
return func() int { return len(cfg.LogLevel) }
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(4), outcome)
}
func TestLeftReaderIO(t *testing.T) {
err := errors.New("test error")
computation := LeftReaderIO[int](func(cfg AppConfig) io.IO[error] {
return func() error { return err }
})
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestFromReaderEither(t *testing.T) {
t.Run("right", func(t *testing.T) {
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
return either.Right[error](len(cfg.DatabaseURL))
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("left", func(t *testing.T) {
err := errors.New("test error")
computation := FromReaderEither(func(cfg AppConfig) either.Either[error, int] {
return either.Left[int](err)
})
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestAsk(t *testing.T) {
computation := Ask[AppConfig]()
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(defaultConfig), outcome)
}
func TestAsks(t *testing.T) {
computation := Asks(func(cfg AppConfig) string {
return cfg.DatabaseURL
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("postgres://localhost"), outcome)
}
func TestFromOption(t *testing.T) {
err := errors.New("none error")
t.Run("some", func(t *testing.T) {
computation := FromOption[AppConfig, int](func() error { return err })(option.Some(42))
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("none", func(t *testing.T) {
computation := FromOption[AppConfig, int](func() error { return err })(option.None[int]())
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestFromPredicate(t *testing.T) {
isPositive := func(n int) bool { return n > 0 }
onFalse := func(n int) error { return errors.New("not positive") }
t.Run("predicate true", func(t *testing.T) {
computation := FromPredicate[AppConfig](isPositive, onFalse)(42)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("predicate false", func(t *testing.T) {
computation := FromPredicate[AppConfig](isPositive, onFalse)(-5)
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestMonadAlt(t *testing.T) {
err := errors.New("first error")
t.Run("first succeeds", func(t *testing.T) {
first := Of[AppConfig](42)
second := func() ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](99)
}
computation := MonadAlt(first, second)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("first fails, second succeeds", func(t *testing.T) {
first := Left[AppConfig, int](err)
second := func() ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](99)
}
computation := MonadAlt(first, second)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
})
t.Run("both fail", func(t *testing.T) {
first := Left[AppConfig, int](err)
second := func() ReaderReaderIOResult[AppConfig, int] {
return Left[AppConfig, int](errors.New("second error"))
}
computation := MonadAlt(first, second)
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestAlt(t *testing.T) {
err := errors.New("first error")
computation := F.Pipe1(
Left[AppConfig, int](err),
Alt(func() ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](99)
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
}
func TestMonadFlap(t *testing.T) {
fab := Of[AppConfig](N.Mul(2))
computation := MonadFlap(fab, 21)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestFlap(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](N.Mul(2)),
Flap[AppConfig, int](21),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestMonadMapLeft(t *testing.T) {
err := errors.New("original error")
computation := MonadMapLeft(
Left[AppConfig, int](err),
func(e error) error { return errors.New("mapped: " + e.Error()) },
)
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
result.Fold(
func(e error) any {
assert.Contains(t, e.Error(), "mapped:")
return nil
},
func(v int) any {
t.Fatal("should be left")
return nil
},
)(outcome)
}
func TestMapLeft(t *testing.T) {
err := errors.New("original error")
computation := F.Pipe1(
Left[AppConfig, int](err),
MapLeft[AppConfig, int](func(e error) error {
return errors.New("mapped: " + e.Error())
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestLocal(t *testing.T) {
type OtherConfig struct {
URL string
}
computation := F.Pipe1(
Asks(func(cfg AppConfig) string {
return cfg.DatabaseURL
}),
Local[string](func(other OtherConfig) AppConfig {
return AppConfig{DatabaseURL: other.URL, LogLevel: "debug"}
}),
)
outcome := computation(OtherConfig{URL: "test-url"})(t.Context())()
assert.Equal(t, result.Of("test-url"), outcome)
}
func TestRead(t *testing.T) {
computation := Asks(func(cfg AppConfig) string {
return cfg.DatabaseURL
})
reader := Read[string](defaultConfig)
outcome := reader(computation)(t.Context())()
assert.Equal(t, result.Of("postgres://localhost"), outcome)
}
func TestReadIOEither(t *testing.T) {
computation := Asks(func(cfg AppConfig) string {
return cfg.DatabaseURL
})
rio := ioeither.Of[error](defaultConfig)
reader := ReadIOEither[string](rio)
outcome := reader(computation)(t.Context())()
assert.Equal(t, result.Of("postgres://localhost"), outcome)
}
func TestReadIO(t *testing.T) {
computation := Asks(func(cfg AppConfig) string {
return cfg.DatabaseURL
})
rio := func() AppConfig { return defaultConfig }
reader := ReadIO[string](rio)
outcome := reader(computation)(t.Context())()
assert.Equal(t, result.Of("postgres://localhost"), outcome)
}
func TestMonadChainLeft(t *testing.T) {
err := errors.New("original error")
computation := MonadChainLeft(
Left[AppConfig, int](err),
func(e error) ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](99)
},
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
}
func TestChainLeft(t *testing.T) {
err := errors.New("original error")
computation := F.Pipe1(
Left[AppConfig, int](err),
ChainLeft(func(e error) ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](99)
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
}
func TestDelay(t *testing.T) {
start := time.Now()
computation := F.Pipe1(
Of[AppConfig](42),
Delay[AppConfig, int](50*time.Millisecond),
)
outcome := computation(defaultConfig)(t.Context())()
elapsed := time.Since(start)
assert.Equal(t, result.Of(42), outcome)
assert.GreaterOrEqual(t, elapsed, 50*time.Millisecond)
}
func TestChainEitherK(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
ChainEitherK[AppConfig](func(n int) either.Either[error, int] {
return either.Right[error](n * 2)
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestChainReaderK(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](10),
ChainReaderK(func(n int) reader.Reader[AppConfig, int] {
return func(cfg AppConfig) int {
return n + len(cfg.LogLevel)
}
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(14), outcome) // 10 + len("info")
}
func TestChainReaderIOK(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](10),
ChainReaderIOK(func(n int) readerio.ReaderIO[AppConfig, int] {
return func(cfg AppConfig) io.IO[int] {
return func() int {
return n + len(cfg.DatabaseURL)
}
}
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(30), outcome) // 10 + 20
}
func TestChainReaderEitherK(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](10),
ChainReaderEitherK(func(n int) RE.ReaderEither[AppConfig, error, int] {
return func(cfg AppConfig) either.Either[error, int] {
return either.Right[error](n + len(cfg.LogLevel))
}
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(14), outcome)
}
func TestChainReaderOptionK(t *testing.T) {
onNone := func() error { return errors.New("none") }
t.Run("some", func(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](10),
ChainReaderOptionK[AppConfig, int, int](onNone)(func(n int) readeroption.ReaderOption[AppConfig, int] {
return func(cfg AppConfig) option.Option[int] {
return option.Some(n + len(cfg.LogLevel))
}
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(14), outcome)
})
t.Run("none", func(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](10),
ChainReaderOptionK[AppConfig, int, int](onNone)(func(n int) readeroption.ReaderOption[AppConfig, int] {
return func(cfg AppConfig) option.Option[int] {
return option.None[int]()
}
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestChainIOEitherK(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
ChainIOEitherK[AppConfig](func(n int) ioeither.IOEither[error, int] {
return ioeither.Of[error](n * 2)
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestChainIOK(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
ChainIOK[AppConfig](func(n int) io.IO[int] {
return func() int { return n * 2 }
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestChainOptionK(t *testing.T) {
onNone := func() error { return errors.New("none") }
t.Run("some", func(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
ChainOptionK[AppConfig, int, int](onNone)(func(n int) option.Option[int] {
return option.Some(n * 2)
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("none", func(t *testing.T) {
computation := F.Pipe1(
Of[AppConfig](21),
ChainOptionK[AppConfig, int, int](onNone)(func(n int) option.Option[int] {
return option.None[int]()
}),
)
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestFromReaderIOResult(t *testing.T) {
computation := FromReaderIOResult(func(cfg AppConfig) ioresult.IOResult[int] {
return func() result.Result[int] {
return result.Of(len(cfg.DatabaseURL))
}
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
}
func TestFromReaderOption(t *testing.T) {
onNone := func() error { return errors.New("none") }
t.Run("some", func(t *testing.T) {
computation := FromReaderOption[AppConfig, int](onNone)(func(cfg AppConfig) option.Option[int] {
return option.Some(len(cfg.DatabaseURL))
})
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("none", func(t *testing.T) {
computation := FromReaderOption[AppConfig, int](onNone)(func(cfg AppConfig) option.Option[int] {
return option.None[int]()
})
outcome := computation(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestMonadAp(t *testing.T) {
fab := Of[AppConfig](N.Mul(2))
fa := Of[AppConfig](21)
computation := MonadAp(fab, fa)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestAp(t *testing.T) {
fa := Of[AppConfig](21)
computation := F.Pipe1(
Of[AppConfig](N.Mul(2)),
Ap[int](fa),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/retry"
)
// Retrying executes an action with automatic retry logic based on a retry policy.
// It retries the action when it fails or when the check predicate returns false.
//
// This function is useful for handling transient failures in operations like:
// - Network requests that may temporarily fail
// - Database operations that may encounter locks
// - External service calls that may be temporarily unavailable
//
// Parameters:
// - policy: Defines the retry behavior (number of retries, delays, backoff strategy)
// - action: The computation to retry, receives retry status information
// - check: Predicate to determine if the result should trigger a retry (returns true to continue, false to retry)
//
// The action receives a retry.RetryStatus that contains:
// - IterNumber: Current iteration number (0-based)
// - CumulativeDelay: Total delay accumulated so far
// - PreviousDelay: Delay from the previous iteration
//
// Returns:
// - A ReaderReaderIOResult that executes the action with retry logic
//
// Example:
//
// import (
// "errors"
// "time"
// "github.com/IBM/fp-go/v2/retry"
// )
//
// type Config struct {
// MaxRetries int
// BaseDelay time.Duration
// }
//
// // Create a retry policy with exponential backoff
// policy := retry.ExponentialBackoff(100*time.Millisecond, 5*time.Second)
// policy = retry.LimitRetries(3, policy)
//
// // Action that may fail transiently
// action := func(status retry.RetryStatus) ReaderReaderIOResult[Config, string] {
// return func(cfg Config) ReaderIOResult[context.Context, string] {
// return func(ctx context.Context) IOResult[string] {
// return func() Either[error, string] {
// // Simulate transient failure
// if status.IterNumber < 2 {
// return either.Left[string](errors.New("transient error"))
// }
// return either.Right[error]("success")
// }
// }
// }
// }
//
// // Check if we should retry (retry on any error)
// check := func(result Result[string]) bool {
// return either.IsRight(result) // Continue only if successful
// }
//
// // Execute with retry logic
// result := Retrying(policy, action, check)
//
//go:inline
func Retrying[R, A any](
policy retry.RetryPolicy,
action Kleisli[R, retry.RetryStatus, A],
check Predicate[Result[A]],
) ReaderReaderIOResult[R, A] {
// get an implementation for the types
return F.Flow4(
reader.Read[RIOE.ReaderIOResult[A]],
reader.Map[retry.RetryStatus],
reader.Read[RIOE.Kleisli[retry.RetryStatus, A]](action),
F.Bind13of3(RIOE.Retrying[A])(policy, check),
)
}

View File

@@ -0,0 +1,255 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"errors"
"testing"
"time"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
func TestRetryingSuccess(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
attempts := 0
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
if attempts < 3 {
return result.Left[int](errors.New("temporary error"))
}
return result.Of(42)
}
}
}
}
check := result.IsLeft[int]
policy := retry.LimitRetries(5)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 3, attempts)
}
func TestRetryingFailureExhaustsRetries(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
attempts := 0
testErr := errors.New("persistent error")
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
return result.Left[int](testErr)
}
}
}
}
check := result.IsLeft[int]
policy := retry.LimitRetries(3)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsLeft(outcome))
assert.Equal(t, 4, attempts) // Initial attempt + 3 retries
}
func TestRetryingNoRetryNeeded(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
attempts := 0
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
return result.Of(42)
}
}
}
}
check := result.IsLeft[int]
policy := retry.LimitRetries(5)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 1, attempts) // Only initial attempt
}
func TestRetryingWithDelay(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
attempts := 0
start := time.Now()
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
if attempts < 2 {
return result.Left[int](errors.New("temporary error"))
}
return result.Of(42)
}
}
}
}
check := result.IsLeft[int]
// Policy with delay
policy := retry.CapDelay(
100*time.Millisecond,
retry.LimitRetries(3),
)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
elapsed := time.Since(start)
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 2, attempts)
// The delay might be very short in tests, so just check it completed
_ = elapsed
}
func TestRetryingAccessesConfig(t *testing.T) {
cfg := AppConfig{DatabaseURL: "test-db", LogLevel: "debug"}
ctx := t.Context()
attempts := 0
var capturedURL string
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
capturedURL = c.DatabaseURL
if attempts < 2 {
return result.Left[int](errors.New("temporary error"))
}
return result.Of(len(c.DatabaseURL))
}
}
}
}
check := result.IsLeft[int]
policy := retry.LimitRetries(3)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of(7), outcome) // len("test-db")
assert.Equal(t, "test-db", capturedURL)
assert.Equal(t, 2, attempts)
}
func TestRetryingWithExponentialBackoff(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
attempts := 0
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
if attempts < 3 {
return result.Left[int](errors.New("temporary error"))
}
return result.Of(42)
}
}
}
}
check := result.IsLeft[int]
// Exponential backoff policy
policy := retry.CapDelay(
200*time.Millisecond,
retry.LimitRetries(5),
)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 3, attempts)
}
func TestRetryingCheckFunction(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
attempts := 0
action := func(status retry.RetryStatus) ReaderReaderIOResult[AppConfig, int] {
return func(c AppConfig) ReaderIOResult[context.Context, int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
attempts++
return result.Of(attempts)
}
}
}
}
// Retry while result is less than 3
check := func(r Result[int]) bool {
return result.Fold(
reader.Of[error](true),
N.LessThan(3),
)(r)
}
policy := retry.LimitRetries(10)
computation := Retrying(policy, action, check)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of(3), outcome)
assert.Equal(t, 3, attempts)
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) 2024 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"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/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/traversal/result"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/readerreaderioeither"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
// Option represents an optional value that may or may not be present.
// It's an alias for option.Option[A].
Option[A any] = option.Option[A]
// Lazy represents a lazily evaluated computation that produces a value of type A.
// It's an alias for lazy.Lazy[A].
Lazy[A any] = lazy.Lazy[A]
// Reader represents a computation that depends on an environment of type R
// and produces a value of type A.
// It's an alias for reader.Reader[R, A].
Reader[R, A any] = reader.Reader[R, A]
// ReaderOption represents a computation that depends on an environment of type R
// and produces an optional value of type A.
// It's an alias for readeroption.ReaderOption[R, A].
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// ReaderIO represents a computation that depends on an environment of type R
// and performs side effects to produce a value of type A.
// It's an alias for readerio.ReaderIO[R, A].
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// ReaderIOResult represents a computation that depends on an environment of type R,
// performs side effects, and may fail with an error.
// It's an alias for readerioresult.ReaderIOResult[R, A].
ReaderIOResult[R, A any] = readerioresult.ReaderIOResult[R, A]
// Either represents a value that can be one of two types: Left (error) or Right (success).
// It's an alias for either.Either[E, A].
Either[E, A any] = either.Either[E, A]
// Result is a specialized Either with error as the left type.
// It's an alias for result.Result[A] which is Either[error, A].
Result[A any] = result.Result[A]
// IOEither represents a side-effecting computation that may fail with an error of type E
// or succeed with a value of type A.
// It's an alias for ioeither.IOEither[E, A].
IOEither[E, A any] = ioeither.IOEither[E, A]
// IOResult represents a side-effecting computation that may fail with an error
// or succeed with a value of type A.
// It's an alias for ioresult.IOResult[A] which is IOEither[error, A].
IOResult[A any] = ioresult.IOResult[A]
// IO represents a side-effecting computation that produces a value of type A.
// It's an alias for io.IO[A].
IO[A any] = io.IO[A]
// ReaderReaderIOEither is the base monad transformer that combines:
// - Reader[R, ...] for outer dependency injection
// - Reader[C, ...] for inner dependency injection (typically context.Context)
// - IO for side effects
// - Either[E, A] for error handling
// It's an alias for readerreaderioeither.ReaderReaderIOEither[R, C, E, A].
ReaderReaderIOEither[R, C, E, A any] = readerreaderioeither.ReaderReaderIOEither[R, C, E, A]
// ReaderReaderIOResult is the main type of this package, specializing ReaderReaderIOEither
// with context.Context as the inner reader type and error as the error type.
//
// Type structure:
// ReaderReaderIOResult[R, A] = R -> context.Context -> IO[Either[error, A]]
//
// This represents a computation that:
// 1. Depends on an outer environment of type R (e.g., application config)
// 2. Depends on a context.Context for cancellation and request-scoped values
// 3. Performs side effects (IO)
// 4. May fail with an error or succeed with a value of type A
//
// This is the primary type used throughout the package for composing
// context-aware, effectful computations with error handling.
ReaderReaderIOResult[R, A any] = ReaderReaderIOEither[R, context.Context, error, A]
// Kleisli represents a function from A to a monadic value ReaderReaderIOResult[R, B].
// It's used for composing monadic functions using Kleisli composition.
//
// Type structure:
// Kleisli[R, A, B] = A -> ReaderReaderIOResult[R, B]
//
// Kleisli arrows can be composed using Chain operations to build complex
// data transformation pipelines.
Kleisli[R, A, B any] = Reader[A, ReaderReaderIOResult[R, B]]
// Operator is a specialized Kleisli arrow that operates on monadic values.
// It takes a ReaderReaderIOResult[R, A] and produces a ReaderReaderIOResult[R, B].
//
// Type structure:
// Operator[R, A, B] = ReaderReaderIOResult[R, A] -> ReaderReaderIOResult[R, B]
//
// Operators are useful for transforming monadic computations, such as
// adding retry logic, logging, or error recovery.
Operator[R, A, B any] = Kleisli[R, ReaderReaderIOResult[R, A], B]
// Lens represents an optic for focusing on a part of a data structure.
// It provides a way to get and set a field T within a structure S.
// It's an alias for lens.Lens[S, T].
Lens[S, T any] = lens.Lens[S, T]
// Trampoline is used for stack-safe recursion through tail call optimization.
// It's an alias for tailrec.Trampoline[L, B].
Trampoline[L, B any] = tailrec.Trampoline[L, B]
// Predicate represents a function that tests whether a value of type A
// satisfies some condition.
// It's an alias for predicate.Predicate[A].
Predicate[A any] = predicate.Predicate[A]
// Endmorphism represents a function from type A to type A.
// It's an alias for endomorphism.Endomorphism[A].
Endmorphism[A any] = endomorphism.Endomorphism[A]
Void = function.Void
)

View File

@@ -0,0 +1,246 @@
# Why Combining IO Operations with ReaderResult Makes Sense
## Overview
The `context/readerresult` package provides functions that combine IO operations (like `FromIO`, `ChainIOK`, `TapIOK`, etc.) with ReaderResult computations. This document explains why this combination is natural and appropriate, despite IO operations being side-effectful.
## Key Insight: ReaderResult is Already Effectful
**IMPORTANT**: Unlike pure functional Reader monads, `ReaderResult[A]` in this package is **already side-effectful** because it depends on `context.Context`.
### Why context.Context is Effectful
The `context.Context` type in Go is inherently effectful because it:
1. **Can be cancelled**: `ctx.Done()` returns a channel that closes when the context is cancelled
2. **Has deadlines**: `ctx.Deadline()` returns a time when the context expires
3. **Carries values**: `ctx.Value(key)` retrieves request-scoped values
4. **Propagates signals**: Cancellation signals propagate across goroutines
5. **Has observable state**: The context's state can change over time (e.g., when cancelled)
### Type Definition
```go
type ReaderResult[A any] = func(context.Context) Result[A]
```
This is **not** a pure function because:
- The behavior can change based on the context's state
- The context can be cancelled during execution
- The context carries mutable, observable state
## Comparison with Pure Reader Monads
### Pure Reader (from `readerresult` package)
```go
type ReaderResult[R, A any] = func(R) Result[A]
```
- `R` can be any type (config, state, etc.)
- The function is **pure** if `R` is immutable
- No side effects unless explicitly introduced
### Effectful Reader (from `context/readerresult` package)
```go
type ReaderResult[A any] = func(context.Context) Result[A]
```
- Always depends on `context.Context`
- **Inherently effectful** due to context's nature
- Side effects are part of the design
## Why IO Operations Fit Naturally
Since `ReaderResult` is already effectful, combining it with IO operations is a natural fit:
### 1. Both Represent Side Effects
```go
// IO operation - side effectful
io := func() int {
fmt.Println("Performing IO")
return 42
}
// ReaderResult - also side effectful (depends on context)
rr := func(ctx context.Context) Result[int] {
// Can check if context is cancelled (side effect)
if ctx.Err() != nil {
return result.Error[int](ctx.Err())
}
return result.Of(42)
}
// Combining them is natural
combined := FromIO(io)
```
### 2. Context-Aware IO Operations
The combination allows IO operations to respect context cancellation:
```go
// IO operation that should respect cancellation
readFile := func(path string) ReaderResult[[]byte] {
return func(ctx context.Context) Result[[]byte] {
// Check cancellation before expensive IO
if ctx.Err() != nil {
return result.Error[[]byte](ctx.Err())
}
// Perform IO operation
data, err := os.ReadFile(path)
if err != nil {
return result.Error[[]byte](err)
}
return result.Of(data)
}
}
```
### 3. Practical Use Cases
#### Logging with Side Effects
```go
// Log to external system (IO operation)
logMetric := func(value int) func() string {
return func() string {
// Side effect: write to metrics system
metrics.Record("value", value)
return "logged"
}
}
// Use with ReaderResult
pipeline := F.Pipe1(
readerresult.Of(42),
readerresult.TapIOK(logMetric),
)
```
#### Database Operations
```go
// Database query (IO operation with context)
queryDB := func(id int) ReaderResult[User] {
return func(ctx context.Context) Result[User] {
// Context used for timeout/cancellation
user, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", id)
if err != nil {
return result.Error[User](err)
}
return result.Of(user)
}
}
// Chain with other operations
pipeline := F.Pipe2(
readerresult.Of(123),
readerresult.Chain(queryDB),
readerresult.TapIOK(func(user User) func() string {
return func() string {
log.Printf("Retrieved user: %s", user.Name)
return "logged"
}
}),
)
```
#### HTTP Requests
```go
// HTTP request (IO operation)
fetchData := func(url string) ReaderResult[Response] {
return func(ctx context.Context) Result[Response] {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return result.Error[Response](err)
}
return result.Of(resp)
}
}
```
## Functions That Combine IO with ReaderResult
### Lifting Functions
- **`FromIO[A]`**: Lifts a pure IO computation into ReaderResult
- **`FromIOResult[A]`**: Lifts an IOResult (IO with error handling) into ReaderResult
### Chaining Functions
- **`ChainIOK[A, B]`**: Sequences a ReaderResult with an IO computation
- **`ChainIOEitherK[A, B]`**: Sequences with an IOResult computation
- **`ChainIOResultK[A, B]`**: Alias for ChainIOEitherK
### Tapping Functions (Side Effects)
- **`TapIOK[A, B]`**: Executes IO for side effects, preserves original value
- **`ChainFirstIOK[A, B]`**: Same as TapIOK
- **`MonadTapIOK[A, B]`**: Monadic version of TapIOK
- **`MonadChainFirstIOK[A, B]`**: Monadic version of ChainFirstIOK
### Error Handling with IO
- **`TapLeftIOK[A, B]`**: Executes IO on error for side effects (logging, metrics)
- **`ChainFirstLeftIOK[A, B]`**: Same as TapLeftIOK
### Reading Context from IO
- **`ReadIO[A]`**: Executes ReaderResult with context from IO
- **`ReadIOEither[A]`**: Executes with context from IOResult
- **`ReadIOResult[A]`**: Alias for ReadIOEither
## Design Philosophy
### Embrace Effectfulness
Rather than trying to maintain purity (which is impossible with `context.Context`), this package embraces the effectful nature of Go's context and provides tools to work with it safely and composably.
### Composition Over Isolation
The package allows you to compose effectful operations (ReaderResult + IO) in a type-safe, functional way, rather than isolating them.
### Practical Go Idioms
This approach aligns with Go's pragmatic philosophy:
- Context is used everywhere in Go for cancellation and timeouts
- IO operations are common and necessary
- Combining them in a type-safe way improves code quality
## Contrast with Pure Functional Packages
### When to Use `context/readerresult` (This Package)
Use when you need:
- ✅ Context cancellation and timeouts
- ✅ Request-scoped values
- ✅ Integration with Go's standard library (http, database/sql, etc.)
- ✅ IO operations with error handling
- ✅ Practical, idiomatic Go code
### When to Use `readerresult` (Pure Package)
Use when you need:
- ✅ Pure dependency injection
- ✅ Testable computations with simple config objects
- ✅ No context propagation
- ✅ Generic environment types (not limited to context.Context)
- ✅ Purely functional composition
## Conclusion
Combining IO operations with ReaderResult in the `context/readerresult` package makes sense because:
1. **ReaderResult is already effectful** due to its dependency on `context.Context`
2. **IO operations are also effectful**, making them a natural fit
3. **The combination provides practical benefits** for real-world Go applications
4. **It aligns with Go's pragmatic philosophy** of embracing side effects when necessary
5. **It enables type-safe composition** of effectful operations
The key insight is that `context.Context` itself is a side effect, so adding more side effects (IO operations) doesn't violate any purity constraints—because there were none to begin with. This package provides tools to work with these side effects in a safe, composable, and type-safe manner.

View File

@@ -16,7 +16,6 @@
package readerresult
import (
"context"
"testing"
E "github.com/IBM/fp-go/v2/either"
@@ -42,7 +41,7 @@ func TestBind(t *testing.T) {
Map(utils.GetFullName),
)
assert.Equal(t, res(context.Background()), E.Of[error]("John Doe"))
assert.Equal(t, res(t.Context()), E.Of[error]("John Doe"))
}
func TestApS(t *testing.T) {
@@ -54,5 +53,5 @@ func TestApS(t *testing.T) {
Map(utils.GetFullName),
)
assert.Equal(t, res(context.Background()), E.Of[error]("John Doe"))
assert.Equal(t, res(t.Context()), E.Of[error]("John Doe"))
}

View File

@@ -22,7 +22,75 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
// WithContext wraps an existing ReaderResult and performs a context check for cancellation
// before delegating to the wrapped computation. This provides early cancellation detection,
// allowing computations to fail fast when the context has been cancelled or has exceeded
// its deadline.
//
// IMPORTANT: This function checks for context cancellation BEFORE executing the wrapped
// ReaderResult. If the context is already cancelled or has exceeded its deadline, the
// computation returns immediately with the cancellation error without executing the
// wrapped ReaderResult.
//
// The function uses context.Cause(ctx) to extract the cancellation reason, which may be:
// - context.Canceled: The context was explicitly cancelled
// - context.DeadlineExceeded: The context's deadline was exceeded
// - A custom error: If the context was cancelled with a cause (Go 1.20+)
//
// Type Parameters:
// - A: The success type of the ReaderResult
//
// Parameters:
// - ma: The ReaderResult to wrap with cancellation checking
//
// Returns:
// - A ReaderResult that checks for cancellation before executing ma
//
// Example:
//
// // Create a long-running computation
// longComputation := func(ctx context.Context) result.Result[int] {
// time.Sleep(5 * time.Second)
// return result.Of(42)
// }
//
// // Wrap with cancellation check
// safeLongComputation := readerresult.WithContext(longComputation)
//
// // Cancel the context before execution
// ctx, cancel := context.WithCancel(t.Context())
// cancel()
//
// // The computation returns immediately with cancellation error
// result := safeLongComputation(ctx)
// // result is Left(context.Canceled) - longComputation never executes
//
// Example with timeout:
//
// fetchData := func(ctx context.Context) result.Result[string] {
// // Simulate slow operation
// time.Sleep(2 * time.Second)
// return result.Of("data")
// }
//
// safeFetch := readerresult.WithContext(fetchData)
//
// // Context with 1 second timeout
// ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
// defer cancel()
//
// time.Sleep(1500 * time.Millisecond) // Wait for timeout
//
// result := safeFetch(ctx)
// // result is Left(context.DeadlineExceeded) - fetchData never executes
//
// Use cases:
// - Wrapping expensive computations to enable early cancellation
// - Preventing unnecessary work when context is already cancelled
// - Implementing timeout-aware operations
// - Building cancellation-aware pipelines
//
//go:inline
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) E.Either[error, A] {
if ctx.Err() != nil {
@@ -32,6 +100,81 @@ func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
}
}
// WithContextK wraps a Kleisli arrow with context cancellation checking.
// This is a higher-order function that takes a Kleisli arrow and returns a new
// Kleisli arrow that checks for context cancellation before executing.
//
// IMPORTANT: This function composes the Kleisli arrow with WithContext, ensuring
// that the resulting ReaderResult checks for cancellation before execution. This
// is particularly useful when building pipelines of Kleisli arrows where you want
// cancellation checking at each step.
//
// Type Parameters:
// - A: The input type of the Kleisli arrow
// - B: The output type of the Kleisli arrow
//
// Parameters:
// - f: The Kleisli arrow to wrap with cancellation checking
//
// Returns:
// - A new Kleisli arrow that checks for cancellation before executing f
//
// Example:
//
// // Define a Kleisli arrow
// processUser := func(id int) readerresult.ReaderResult[User] {
// return func(ctx context.Context) result.Result[User] {
// // Expensive database operation
// return fetchUserFromDB(ctx, id)
// }
// }
//
// // Wrap with cancellation checking
// safeProcessUser := readerresult.WithContextK(processUser)
//
// // Use in a pipeline
// pipeline := F.Pipe1(
// readerresult.Of(123),
// readerresult.Chain(safeProcessUser),
// )
//
// // If context is cancelled, processUser never executes
// ctx, cancel := context.WithCancel(t.Context())
// cancel()
// result := pipeline(ctx) // Left(context.Canceled)
//
// Example with multiple steps:
//
// getUserK := readerresult.WithContextK(func(id int) readerresult.ReaderResult[User] {
// return func(ctx context.Context) result.Result[User] {
// return fetchUser(ctx, id)
// }
// })
//
// getOrdersK := readerresult.WithContextK(func(user User) readerresult.ReaderResult[[]Order] {
// return func(ctx context.Context) result.Result[[]Order] {
// return fetchOrders(ctx, user.ID)
// }
// })
//
// // Each step checks for cancellation
// pipeline := F.Pipe2(
// readerresult.Of(123),
// readerresult.Chain(getUserK),
// readerresult.Chain(getOrdersK),
// )
//
// // If context is cancelled at any point, remaining steps don't execute
// ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
// defer cancel()
// result := pipeline(ctx)
//
// Use cases:
// - Building cancellation-aware pipelines
// - Ensuring each step in a chain respects cancellation
// - Implementing timeout-aware multi-step operations
// - Preventing cascading failures in long pipelines
//
//go:inline
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
return F.Flow2(

View File

@@ -0,0 +1,418 @@
// 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 readerresult
import (
"context"
"errors"
"testing"
"time"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
// TestWithContext tests the WithContext function
func TestWithContext(t *testing.T) {
t.Run("executes wrapped ReaderResult when context is not cancelled", func(t *testing.T) {
executed := false
computation := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
wrapped := WithContext(computation)
result := wrapped(t.Context())
assert.True(t, executed, "computation should be executed")
assert.Equal(t, E.Of[error](42), result)
})
t.Run("returns cancellation error when context is cancelled", func(t *testing.T) {
executed := false
computation := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
wrapped := WithContext(computation)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := wrapped(ctx)
assert.False(t, executed, "computation should not be executed when context is cancelled")
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, context.Canceled, err)
})
t.Run("returns deadline exceeded error when context times out", func(t *testing.T) {
executed := false
computation := func(ctx context.Context) E.Either[error, int] {
executed = true
time.Sleep(100 * time.Millisecond)
return E.Of[error](42)
}
wrapped := WithContext(computation)
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
defer cancel()
time.Sleep(20 * time.Millisecond) // Wait for timeout
result := wrapped(ctx)
assert.False(t, executed, "computation should not be executed when context has timed out")
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("preserves errors from wrapped computation", func(t *testing.T) {
testErr := errors.New("computation error")
computation := func(ctx context.Context) E.Either[error, int] {
return E.Left[int](testErr)
}
wrapped := WithContext(computation)
result := wrapped(t.Context())
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, testErr, err)
})
t.Run("prevents expensive computation when context is already cancelled", func(t *testing.T) {
expensiveExecuted := false
expensiveComputation := func(ctx context.Context) E.Either[error, int] {
expensiveExecuted = true
// Simulate expensive operation
time.Sleep(1 * time.Second)
return E.Of[error](42)
}
wrapped := WithContext(expensiveComputation)
ctx, cancel := context.WithCancel(t.Context())
cancel()
start := time.Now()
result := wrapped(ctx)
duration := time.Since(start)
assert.False(t, expensiveExecuted, "expensive computation should not execute")
assert.True(t, E.IsLeft(result))
assert.Less(t, duration, 100*time.Millisecond, "should return immediately")
})
t.Run("works with context.WithCancelCause", func(t *testing.T) {
executed := false
computation := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
wrapped := WithContext(computation)
customErr := errors.New("custom cancellation reason")
ctx, cancel := context.WithCancelCause(t.Context())
cancel(customErr)
result := wrapped(ctx)
assert.False(t, executed, "computation should not be executed")
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, customErr, err)
})
t.Run("can be nested for multiple cancellation checks", func(t *testing.T) {
executed := false
computation := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
doubleWrapped := WithContext(WithContext(computation))
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := doubleWrapped(ctx)
assert.False(t, executed, "computation should not be executed")
assert.True(t, E.IsLeft(result))
})
}
// TestWithContextK tests the WithContextK function
func TestWithContextK(t *testing.T) {
t.Run("wraps Kleisli arrow with cancellation checking", func(t *testing.T) {
executed := false
processUser := func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
executed = true
return E.Of[error]("user-" + string(rune(id+48)))
}
}
safeProcessUser := WithContextK(processUser)
result := safeProcessUser(123)(t.Context())
assert.True(t, executed, "Kleisli should be executed")
assert.True(t, E.IsRight(result))
})
t.Run("prevents Kleisli execution when context is cancelled", func(t *testing.T) {
executed := false
processUser := func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
executed = true
return E.Of[error]("user")
}
}
safeProcessUser := WithContextK(processUser)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := safeProcessUser(123)(ctx)
assert.False(t, executed, "Kleisli should not be executed when context is cancelled")
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, context.Canceled, err)
})
t.Run("works in Chain pipeline", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
getUser := WithContextK(func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
firstExecuted = true
return E.Of[error]("Alice")
}
})
getOrders := WithContextK(func(name string) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
secondExecuted = true
return E.Of[error](5)
}
})
pipeline := F.Pipe2(
Of(123),
Chain(getUser),
Chain(getOrders),
)
result := pipeline(t.Context())
assert.True(t, firstExecuted, "first step should execute")
assert.True(t, secondExecuted, "second step should execute")
assert.Equal(t, E.Of[error](5), result)
})
t.Run("stops pipeline on cancellation", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
getUser := WithContextK(func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
firstExecuted = true
return E.Of[error]("Alice")
}
})
getOrders := WithContextK(func(name string) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
secondExecuted = true
return E.Of[error](5)
}
})
pipeline := F.Pipe2(
Of(123),
Chain(getUser),
Chain(getOrders),
)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := pipeline(ctx)
assert.False(t, firstExecuted, "first step should not execute")
assert.False(t, secondExecuted, "second step should not execute")
assert.True(t, E.IsLeft(result))
})
t.Run("respects timeout in multi-step pipeline", func(t *testing.T) {
step1Executed := false
step2Executed := false
step1 := WithContextK(func(x int) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
step1Executed = true
time.Sleep(50 * time.Millisecond)
return E.Of[error](x * 2)
}
})
step2 := WithContextK(func(x int) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
step2Executed = true
return E.Of[error](x + 10)
}
})
pipeline := F.Pipe2(
Of(5),
Chain(step1),
Chain(step2),
)
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
defer cancel()
time.Sleep(20 * time.Millisecond) // Wait for timeout
result := pipeline(ctx)
assert.False(t, step1Executed, "step1 should not execute after timeout")
assert.False(t, step2Executed, "step2 should not execute after timeout")
assert.True(t, E.IsLeft(result))
})
t.Run("preserves errors from Kleisli computation", func(t *testing.T) {
testErr := errors.New("kleisli error")
failingKleisli := func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
return E.Left[string](testErr)
}
}
safeKleisli := WithContextK(failingKleisli)
result := safeKleisli(123)(t.Context())
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, testErr, err)
})
}
// TestWithContextIntegration tests integration scenarios
func TestWithContextIntegration(t *testing.T) {
t.Run("WithContext in complex pipeline with multiple operations", func(t *testing.T) {
step1Executed := false
step2Executed := false
step3Executed := false
step1 := WithContext(func(ctx context.Context) E.Either[error, int] {
step1Executed = true
return E.Of[error](10)
})
step2 := WithContextK(func(x int) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
step2Executed = true
return E.Of[error](x * 2)
}
})
step3 := WithContext(func(ctx context.Context) E.Either[error, string] {
step3Executed = true
return E.Of[error]("done")
})
pipeline := F.Pipe2(
step1,
Chain(step2),
ChainTo[int](step3),
)
result := pipeline(t.Context())
assert.True(t, step1Executed)
assert.True(t, step2Executed)
assert.True(t, step3Executed)
assert.Equal(t, E.Of[error]("done"), result)
})
t.Run("early cancellation prevents all subsequent operations", func(t *testing.T) {
step1Executed := false
step2Executed := false
step3Executed := false
step1 := WithContext(func(ctx context.Context) E.Either[error, int] {
step1Executed = true
return E.Of[error](10)
})
step2 := WithContextK(func(x int) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
step2Executed = true
return E.Of[error](x * 2)
}
})
step3 := WithContext(func(ctx context.Context) E.Either[error, string] {
step3Executed = true
return E.Of[error]("done")
})
pipeline := F.Pipe2(
step1,
Chain(step2),
ChainTo[int](step3),
)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := pipeline(ctx)
assert.False(t, step1Executed, "no steps should execute")
assert.False(t, step2Executed, "no steps should execute")
assert.False(t, step3Executed, "no steps should execute")
assert.True(t, E.IsLeft(result))
})
t.Run("WithContext with Map and Chain", func(t *testing.T) {
computation := WithContext(func(ctx context.Context) E.Either[error, int] {
return E.Of[error](42)
})
pipeline := F.Pipe2(
computation,
Map(N.Mul(2)),
Map(reader.Of[int]("result")),
)
result := pipeline(t.Context())
assert.Equal(t, E.Of[error]("result"), result)
})
}

View File

@@ -21,33 +21,299 @@ import (
"github.com/IBM/fp-go/v2/readereither"
)
// these functions curry a golang function with the context as the firsr parameter into a either reader with the context as the last parameter
// this goes back to the advice in https://pkg.go.dev/context to put the context as a first parameter as a convention
// Curry and Uncurry functions convert between idiomatic Go functions (with context.Context as the first parameter)
// and functional ReaderResult/Kleisli compositions (with context.Context as the last parameter).
//
// This follows the Go convention from https://pkg.go.dev/context to put context as the first parameter,
// while enabling functional composition where context is typically the last parameter.
//
// The curry functions transform:
// func(context.Context, T1, T2, ...) (A, error) → func(T1) func(T2) ... ReaderResult[A]
//
// The uncurry functions transform:
// func(T1) func(T2) ... ReaderResult[A] → func(context.Context, T1, T2, ...) (A, error)
// Curry0 converts a Go function with context and no additional parameters into a ReaderResult.
// This is useful for adapting context-aware functions to the ReaderResult monad.
//
// Type Parameters:
// - A: The return type of the function
//
// Parameters:
// - f: A function that takes a context and returns a value and error
//
// Returns:
// - A ReaderResult that wraps the function
//
// Example:
//
// // Idiomatic Go function
// getConfig := func(ctx context.Context) (Config, error) {
// // Check context cancellation
// if ctx.Err() != nil {
// return Config{}, ctx.Err()
// }
// return Config{Value: 42}, nil
// }
//
// // Convert to ReaderResult for functional composition
// configRR := readerresult.Curry0(getConfig)
// result := configRR(t.Context()) // Right(Config{Value: 42})
//
//go:inline
func Curry0[A any](f func(context.Context) (A, error)) ReaderResult[A] {
return readereither.Curry0(f)
}
// Curry1 converts a Go function with context and one parameter into a Kleisli arrow.
// This enables functional composition of single-parameter functions.
//
// Type Parameters:
// - T1: The type of the first parameter
// - A: The return type of the function
//
// Parameters:
// - f: A function that takes a context and one parameter, returning a value and error
//
// Returns:
// - A Kleisli arrow that can be composed with other ReaderResult operations
//
// Example:
//
// // Idiomatic Go function
// getUserByID := func(ctx context.Context, id int) (User, error) {
// if ctx.Err() != nil {
// return User{}, ctx.Err()
// }
// return User{ID: id, Name: "Alice"}, nil
// }
//
// // Convert to Kleisli for functional composition
// getUserKleisli := readerresult.Curry1(getUserByID)
//
// // Use in a pipeline
// pipeline := F.Pipe1(
// readerresult.Of(123),
// readerresult.Chain(getUserKleisli),
// )
// result := pipeline(t.Context()) // Right(User{ID: 123, Name: "Alice"})
//
//go:inline
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
return readereither.Curry1(f)
}
// Curry2 converts a Go function with context and two parameters into a curried function.
// This enables partial application and functional composition of two-parameter functions.
//
// Type Parameters:
// - T1: The type of the first parameter
// - T2: The type of the second parameter
// - A: The return type of the function
//
// Parameters:
// - f: A function that takes a context and two parameters, returning a value and error
//
// Returns:
// - A curried function that takes T1 and returns a Kleisli arrow for T2
//
// Example:
//
// // Idiomatic Go function
// updateUser := func(ctx context.Context, id int, name string) (User, error) {
// if ctx.Err() != nil {
// return User{}, ctx.Err()
// }
// return User{ID: id, Name: name}, nil
// }
//
// // Convert to curried form
// updateUserCurried := readerresult.Curry2(updateUser)
//
// // Partial application
// updateUser123 := updateUserCurried(123)
//
// // Use in a pipeline
// pipeline := F.Pipe1(
// readerresult.Of("Bob"),
// readerresult.Chain(updateUser123),
// )
// result := pipeline(t.Context()) // Right(User{ID: 123, Name: "Bob"})
//
//go:inline
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) Kleisli[T2, A] {
return readereither.Curry2(f)
}
// Curry3 converts a Go function with context and three parameters into a curried function.
// This enables partial application and functional composition of three-parameter functions.
//
// Type Parameters:
// - T1: The type of the first parameter
// - T2: The type of the second parameter
// - T3: The type of the third parameter
// - A: The return type of the function
//
// Parameters:
// - f: A function that takes a context and three parameters, returning a value and error
//
// Returns:
// - A curried function that takes T1, T2, and returns a Kleisli arrow for T3
//
// Example:
//
// // Idiomatic Go function
// createOrder := func(ctx context.Context, userID int, productID int, quantity int) (Order, error) {
// if ctx.Err() != nil {
// return Order{}, ctx.Err()
// }
// return Order{UserID: userID, ProductID: productID, Quantity: quantity}, nil
// }
//
// // Convert to curried form
// createOrderCurried := readerresult.Curry3(createOrder)
//
// // Partial application
// createOrderForUser := createOrderCurried(123)
// createOrderForProduct := createOrderForUser(456)
//
// // Use in a pipeline
// pipeline := F.Pipe1(
// readerresult.Of(2),
// readerresult.Chain(createOrderForProduct),
// )
// result := pipeline(t.Context()) // Right(Order{UserID: 123, ProductID: 456, Quantity: 2})
//
//go:inline
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) Kleisli[T3, A] {
return readereither.Curry3(f)
}
// Uncurry1 converts a Kleisli arrow back into an idiomatic Go function with context as the first parameter.
// This is useful for interfacing with code that expects standard Go function signatures.
//
// Type Parameters:
// - T1: The type of the parameter
// - A: The return type
//
// Parameters:
// - f: A Kleisli arrow
//
// Returns:
// - A Go function with context as the first parameter
//
// Example:
//
// // Kleisli arrow
// getUserKleisli := func(id int) readerresult.ReaderResult[User] {
// return func(ctx context.Context) result.Result[User] {
// if ctx.Err() != nil {
// return result.Error[User](ctx.Err())
// }
// return result.Of(User{ID: id, Name: "Alice"})
// }
// }
//
// // Convert back to idiomatic Go function
// getUserByID := readerresult.Uncurry1(getUserKleisli)
//
// // Use as a normal Go function
// user, err := getUserByID(t.Context(), 123)
// if err != nil {
// log.Fatal(err)
// }
// fmt.Println(user.Name) // "Alice"
//
//go:inline
func Uncurry1[T1, A any](f Kleisli[T1, A]) func(context.Context, T1) (A, error) {
return readereither.Uncurry1(f)
}
// Uncurry2 converts a curried function back into an idiomatic Go function with context as the first parameter.
// This is useful for interfacing with code that expects standard Go function signatures.
//
// Type Parameters:
// - T1: The type of the first parameter
// - T2: The type of the second parameter
// - A: The return type
//
// Parameters:
// - f: A curried function
//
// Returns:
// - A Go function with context as the first parameter
//
// Example:
//
// // Curried function
// updateUserCurried := func(id int) func(name string) readerresult.ReaderResult[User] {
// return func(name string) readerresult.ReaderResult[User] {
// return func(ctx context.Context) result.Result[User] {
// if ctx.Err() != nil {
// return result.Error[User](ctx.Err())
// }
// return result.Of(User{ID: id, Name: name})
// }
// }
// }
//
// // Convert back to idiomatic Go function
// updateUser := readerresult.Uncurry2(updateUserCurried)
//
// // Use as a normal Go function
// user, err := updateUser(t.Context(), 123, "Bob")
// if err != nil {
// log.Fatal(err)
// }
// fmt.Println(user.Name) // "Bob"
//
//go:inline
func Uncurry2[T1, T2, A any](f func(T1) Kleisli[T2, A]) func(context.Context, T1, T2) (A, error) {
return readereither.Uncurry2(f)
}
// Uncurry3 converts a curried function back into an idiomatic Go function with context as the first parameter.
// This is useful for interfacing with code that expects standard Go function signatures.
//
// Type Parameters:
// - T1: The type of the first parameter
// - T2: The type of the second parameter
// - T3: The type of the third parameter
// - A: The return type
//
// Parameters:
// - f: A curried function
//
// Returns:
// - A Go function with context as the first parameter
//
// Example:
//
// // Curried function
// createOrderCurried := func(userID int) func(productID int) func(quantity int) readerresult.ReaderResult[Order] {
// return func(productID int) func(quantity int) readerresult.ReaderResult[Order] {
// return func(quantity int) readerresult.ReaderResult[Order] {
// return func(ctx context.Context) result.Result[Order] {
// if ctx.Err() != nil {
// return result.Error[Order](ctx.Err())
// }
// return result.Of(Order{UserID: userID, ProductID: productID, Quantity: quantity})
// }
// }
// }
// }
//
// // Convert back to idiomatic Go function
// createOrder := readerresult.Uncurry3(createOrderCurried)
//
// // Use as a normal Go function
// order, err := createOrder(t.Context(), 123, 456, 2)
// if err != nil {
// log.Fatal(err)
// }
// fmt.Printf("Order: User=%d, Product=%d, Qty=%d\n", order.UserID, order.ProductID, order.Quantity)
//
//go:inline
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) Kleisli[T3, A]) func(context.Context, T1, T2, T3) (A, error) {
return readereither.Uncurry3(f)
}

View File

@@ -0,0 +1,564 @@
// 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 readerresult
import (
"context"
"errors"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestCurry0 tests the Curry0 function
func TestCurry0(t *testing.T) {
t.Run("converts Go function to ReaderResult on success", func(t *testing.T) {
// Idiomatic Go function
getConfig := func(ctx context.Context) (int, error) {
return 42, nil
}
// Convert to ReaderResult
configRR := Curry0(getConfig)
result := configRR(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
t.Run("converts Go function to ReaderResult on error", func(t *testing.T) {
testErr := errors.New("config error")
getConfig := func(ctx context.Context) (int, error) {
return 0, testErr
}
configRR := Curry0(getConfig)
result := configRR(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
})
t.Run("respects context cancellation", func(t *testing.T) {
getConfig := func(ctx context.Context) (int, error) {
if ctx.Err() != nil {
return 0, ctx.Err()
}
return 42, nil
}
configRR := Curry0(getConfig)
// Test with cancelled context
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := configRR(ctx)
assert.True(t, E.IsLeft(result))
})
t.Run("can be used in functional composition", func(t *testing.T) {
getConfig := func(ctx context.Context) (int, error) {
return 42, nil
}
pipeline := F.Pipe1(
Curry0(getConfig),
Map(func(x int) string {
return "value"
}),
)
result := pipeline(t.Context())
assert.True(t, E.IsRight(result))
})
}
// TestCurry1 tests the Curry1 function
func TestCurry1(t *testing.T) {
t.Run("converts Go function to Kleisli on success", func(t *testing.T) {
getUserByID := func(ctx context.Context, id int) (string, error) {
return "Alice", nil
}
getUserKleisli := Curry1(getUserByID)
// Use in a pipeline
pipeline := F.Pipe1(
Of(123),
Chain(getUserKleisli),
)
result := pipeline(t.Context())
assert.Equal(t, E.Of[error]("Alice"), result)
})
t.Run("converts Go function to Kleisli on error", func(t *testing.T) {
testErr := errors.New("user not found")
getUserByID := func(ctx context.Context, id int) (string, error) {
return "", testErr
}
getUserKleisli := Curry1(getUserByID)
result := getUserKleisli(123)(t.Context())
assert.Equal(t, E.Left[string](testErr), result)
})
t.Run("respects context cancellation", func(t *testing.T) {
getUserByID := func(ctx context.Context, id int) (string, error) {
if ctx.Err() != nil {
return "", ctx.Err()
}
return "Alice", nil
}
getUserKleisli := Curry1(getUserByID)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := getUserKleisli(123)(ctx)
assert.True(t, E.IsLeft(result))
})
t.Run("can be composed with other operations", func(t *testing.T) {
getUserByID := func(ctx context.Context, id int) (string, error) {
return "Alice", nil
}
pipeline := F.Pipe2(
Of(123),
Chain(Curry1(getUserByID)),
Map(func(name string) int {
return len(name)
}),
)
result := pipeline(t.Context())
assert.Equal(t, E.Of[error](5), result) // len("Alice") = 5
})
}
// TestCurry2 tests the Curry2 function
func TestCurry2(t *testing.T) {
t.Run("converts Go function to curried form on success", func(t *testing.T) {
updateUser := func(ctx context.Context, id int, name string) (string, error) {
return name, nil
}
updateUserCurried := Curry2(updateUser)
// Partial application
updateUser123 := updateUserCurried(123)
// Use in a pipeline
pipeline := F.Pipe1(
Of("Bob"),
Chain(updateUser123),
)
result := pipeline(t.Context())
assert.Equal(t, E.Of[error]("Bob"), result)
})
t.Run("converts Go function to curried form on error", func(t *testing.T) {
testErr := errors.New("update failed")
updateUser := func(ctx context.Context, id int, name string) (string, error) {
return "", testErr
}
updateUserCurried := Curry2(updateUser)
result := updateUserCurried(123)("Bob")(t.Context())
assert.Equal(t, E.Left[string](testErr), result)
})
t.Run("supports partial application", func(t *testing.T) {
concat := func(ctx context.Context, a string, b string) (string, error) {
return a + b, nil
}
concatCurried := Curry2(concat)
// Partial application
prependHello := concatCurried("Hello, ")
result1 := prependHello("World")(t.Context())
result2 := prependHello("Alice")(t.Context())
assert.Equal(t, E.Of[error]("Hello, World"), result1)
assert.Equal(t, E.Of[error]("Hello, Alice"), result2)
})
t.Run("respects context cancellation", func(t *testing.T) {
updateUser := func(ctx context.Context, id int, name string) (string, error) {
if ctx.Err() != nil {
return "", ctx.Err()
}
return name, nil
}
updateUserCurried := Curry2(updateUser)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := updateUserCurried(123)("Bob")(ctx)
assert.True(t, E.IsLeft(result))
})
}
// TestCurry3 tests the Curry3 function
func TestCurry3(t *testing.T) {
t.Run("converts Go function to curried form on success", func(t *testing.T) {
createOrder := func(ctx context.Context, userID int, productID int, quantity int) (int, error) {
return userID + productID + quantity, nil
}
createOrderCurried := Curry3(createOrder)
// Partial application
createOrderForUser := createOrderCurried(100)
createOrderForProduct := createOrderForUser(200)
// Use in a pipeline
pipeline := F.Pipe1(
Of(3),
Chain(createOrderForProduct),
)
result := pipeline(t.Context())
assert.Equal(t, E.Of[error](303), result) // 100 + 200 + 3
})
t.Run("converts Go function to curried form on error", func(t *testing.T) {
testErr := errors.New("order creation failed")
createOrder := func(ctx context.Context, userID int, productID int, quantity int) (int, error) {
return 0, testErr
}
createOrderCurried := Curry3(createOrder)
result := createOrderCurried(100)(200)(3)(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
})
t.Run("supports multiple levels of partial application", func(t *testing.T) {
sum3 := func(ctx context.Context, a int, b int, c int) (int, error) {
return a + b + c, nil
}
sum3Curried := Curry3(sum3)
// First level partial application
add10 := sum3Curried(10)
// Second level partial application
add10And20 := add10(20)
result1 := add10And20(5)(t.Context())
result2 := add10And20(15)(t.Context())
assert.Equal(t, E.Of[error](35), result1) // 10 + 20 + 5
assert.Equal(t, E.Of[error](45), result2) // 10 + 20 + 15
})
t.Run("respects context cancellation", func(t *testing.T) {
createOrder := func(ctx context.Context, userID int, productID int, quantity int) (int, error) {
if ctx.Err() != nil {
return 0, ctx.Err()
}
return userID + productID + quantity, nil
}
createOrderCurried := Curry3(createOrder)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := createOrderCurried(100)(200)(3)(ctx)
assert.True(t, E.IsLeft(result))
})
}
// TestUncurry1 tests the Uncurry1 function
func TestUncurry1(t *testing.T) {
t.Run("converts Kleisli back to Go function on success", func(t *testing.T) {
getUserKleisli := func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
return E.Of[error]("Alice")
}
}
getUserByID := Uncurry1(getUserKleisli)
user, err := getUserByID(t.Context(), 123)
assert.NoError(t, err)
assert.Equal(t, "Alice", user)
})
t.Run("converts Kleisli back to Go function on error", func(t *testing.T) {
testErr := errors.New("user not found")
getUserKleisli := func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
return E.Left[string](testErr)
}
}
getUserByID := Uncurry1(getUserKleisli)
user, err := getUserByID(t.Context(), 123)
assert.Error(t, err)
assert.Equal(t, testErr, err)
assert.Equal(t, "", user)
})
t.Run("respects context in uncurried function", func(t *testing.T) {
getUserKleisli := func(id int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
if ctx.Err() != nil {
return E.Left[string](ctx.Err())
}
return E.Of[error]("Alice")
}
}
getUserByID := Uncurry1(getUserKleisli)
ctx, cancel := context.WithCancel(t.Context())
cancel()
user, err := getUserByID(ctx, 123)
assert.Error(t, err)
assert.Equal(t, "", user)
})
t.Run("round-trip with Curry1", func(t *testing.T) {
// Original Go function
original := func(ctx context.Context, id int) (string, error) {
return "Alice", nil
}
// Curry then uncurry
roundTrip := Uncurry1(Curry1(original))
user, err := roundTrip(t.Context(), 123)
assert.NoError(t, err)
assert.Equal(t, "Alice", user)
})
}
// TestUncurry2 tests the Uncurry2 function
func TestUncurry2(t *testing.T) {
t.Run("converts curried function back to Go function on success", func(t *testing.T) {
updateUserCurried := func(id int) func(name string) ReaderResult[string] {
return func(name string) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
return E.Of[error](name)
}
}
}
updateUser := Uncurry2(updateUserCurried)
result, err := updateUser(t.Context(), 123, "Bob")
assert.NoError(t, err)
assert.Equal(t, "Bob", result)
})
t.Run("converts curried function back to Go function on error", func(t *testing.T) {
testErr := errors.New("update failed")
updateUserCurried := func(id int) func(name string) ReaderResult[string] {
return func(name string) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
return E.Left[string](testErr)
}
}
}
updateUser := Uncurry2(updateUserCurried)
result, err := updateUser(t.Context(), 123, "Bob")
assert.Error(t, err)
assert.Equal(t, testErr, err)
assert.Equal(t, "", result)
})
t.Run("respects context in uncurried function", func(t *testing.T) {
updateUserCurried := func(id int) func(name string) ReaderResult[string] {
return func(name string) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
if ctx.Err() != nil {
return E.Left[string](ctx.Err())
}
return E.Of[error](name)
}
}
}
updateUser := Uncurry2(updateUserCurried)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result, err := updateUser(ctx, 123, "Bob")
assert.Error(t, err)
assert.Equal(t, "", result)
})
t.Run("round-trip with Curry2", func(t *testing.T) {
// Original Go function
original := func(ctx context.Context, a string, b string) (string, error) {
return a + b, nil
}
// Curry then uncurry
roundTrip := Uncurry2(Curry2(original))
result, err := roundTrip(t.Context(), "Hello, ", "World")
assert.NoError(t, err)
assert.Equal(t, "Hello, World", result)
})
}
// TestUncurry3 tests the Uncurry3 function
func TestUncurry3(t *testing.T) {
t.Run("converts curried function back to Go function on success", func(t *testing.T) {
createOrderCurried := func(userID int) func(productID int) func(quantity int) ReaderResult[int] {
return func(productID int) func(quantity int) ReaderResult[int] {
return func(quantity int) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
return E.Of[error](userID + productID + quantity)
}
}
}
}
createOrder := Uncurry3(createOrderCurried)
result, err := createOrder(t.Context(), 100, 200, 3)
assert.NoError(t, err)
assert.Equal(t, 303, result) // 100 + 200 + 3
})
t.Run("converts curried function back to Go function on error", func(t *testing.T) {
testErr := errors.New("order creation failed")
createOrderCurried := func(userID int) func(productID int) func(quantity int) ReaderResult[int] {
return func(productID int) func(quantity int) ReaderResult[int] {
return func(quantity int) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
return E.Left[int](testErr)
}
}
}
}
createOrder := Uncurry3(createOrderCurried)
result, err := createOrder(t.Context(), 100, 200, 3)
assert.Error(t, err)
assert.Equal(t, testErr, err)
assert.Equal(t, 0, result)
})
t.Run("respects context in uncurried function", func(t *testing.T) {
createOrderCurried := func(userID int) func(productID int) func(quantity int) ReaderResult[int] {
return func(productID int) func(quantity int) ReaderResult[int] {
return func(quantity int) ReaderResult[int] {
return func(ctx context.Context) E.Either[error, int] {
if ctx.Err() != nil {
return E.Left[int](ctx.Err())
}
return E.Of[error](userID + productID + quantity)
}
}
}
}
createOrder := Uncurry3(createOrderCurried)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result, err := createOrder(ctx, 100, 200, 3)
assert.Error(t, err)
assert.Equal(t, 0, result)
})
t.Run("round-trip with Curry3", func(t *testing.T) {
// Original Go function
original := func(ctx context.Context, a int, b int, c int) (int, error) {
return a + b + c, nil
}
// Curry then uncurry
roundTrip := Uncurry3(Curry3(original))
result, err := roundTrip(t.Context(), 10, 20, 5)
assert.NoError(t, err)
assert.Equal(t, 35, result) // 10 + 20 + 5
})
}
// TestCurryUncurryIntegration tests integration between curry and uncurry functions
func TestCurryUncurryIntegration(t *testing.T) {
t.Run("Curry1 and Uncurry1 are inverses", func(t *testing.T) {
original := func(ctx context.Context, x int) (int, error) {
return x * 2, nil
}
// Curry then uncurry should give back equivalent function
roundTrip := Uncurry1(Curry1(original))
result1, err1 := original(t.Context(), 21)
result2, err2 := roundTrip(t.Context(), 21)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, result1, result2)
})
t.Run("Curry2 and Uncurry2 are inverses", func(t *testing.T) {
original := func(ctx context.Context, x int, y int) (int, error) {
return x + y, nil
}
roundTrip := Uncurry2(Curry2(original))
result1, err1 := original(t.Context(), 10, 20)
result2, err2 := roundTrip(t.Context(), 10, 20)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, result1, result2)
})
t.Run("Curry3 and Uncurry3 are inverses", func(t *testing.T) {
original := func(ctx context.Context, x int, y int, z int) (int, error) {
return x * y * z, nil
}
roundTrip := Uncurry3(Curry3(original))
result1, err1 := original(t.Context(), 2, 3, 4)
result2, err2 := roundTrip(t.Context(), 2, 3, 4)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, result1, result2)
})
}

View File

@@ -43,7 +43,7 @@ import (
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
//
// filter := readerresult.FilterOrElse(isPositive, onNegative)
// result := filter(readerresult.Right(42))(context.Background())
// result := filter(readerresult.Right(42))(t.Context())
//
//go:inline
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {

View File

@@ -63,7 +63,7 @@ import (
// // Sequenced: takes context first, then Database
// sequenced := SequenceReader(original)
//
// ctx := context.Background()
// ctx := t.Context()
// db := Database{ConnectionString: "localhost:5432"}
//
// // Apply context first to get a function that takes database
@@ -135,7 +135,7 @@ func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[cont
//
// // Now we can provide Config first, then context
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
// ctx := t.Context()
//
// result := flipped(cfg)(ctx)
// // result is Result[string] containing "Value: 42, MaxRetries: 3"

View File

@@ -96,7 +96,7 @@ func curriedLog(
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
//
// // Use in a pipeline
// ctx := context.Background()
// ctx := t.Context()
// user := result.Of(User{ID: 123, Name: "Alice"})
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
// // logged still contains the User value
@@ -149,7 +149,7 @@ func SLogWithCallback[A any](
//
// Example - Logging a successful computation:
//
// ctx := context.Background()
// ctx := t.Context()
//
// // Simple value logging
// res := result.Of(42)
@@ -172,7 +172,7 @@ func SLogWithCallback[A any](
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
// }
//
// ctx := context.Background()
// ctx := t.Context()
//
// // Log at each step
// userResult := fetchUser(123)
@@ -195,7 +195,7 @@ func SLogWithCallback[A any](
//
// // Set up a custom logger in the context
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// ctx := logging.WithLogger(logger)(context.Background())
// ctx := logging.WithLogger(logger)(t.Context())
//
// res := result.Of("important data")
// logged := SLog[string]("Critical operation")(res)(ctx)

View File

@@ -37,7 +37,7 @@ func TestSLogLogsSuccessValue(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
// Create a Result and log it
res1 := result.Of(42)
@@ -59,7 +59,7 @@ func TestSLogLogsErrorValue(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("test error")
// Create an error Result and log it
@@ -83,7 +83,7 @@ func TestSLogInPipeline(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
// SLog takes a Result[A] and returns ReaderResult[A]
// So we need to start with a Result, apply SLog, then execute with context
@@ -104,7 +104,7 @@ func TestSLogWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
ctx := logging.WithLogger(contextLogger)(t.Context())
res1 := result.Of("test value")
logged := SLog[string]("Context logger test")(res1)(ctx)
@@ -126,7 +126,7 @@ func TestSLogDisabled(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
res1 := result.Of(42)
logged := SLog[int]("This should not be logged")(res1)(ctx)
@@ -152,7 +152,7 @@ func TestSLogWithStruct(t *testing.T) {
Name string
}
ctx := context.Background()
ctx := t.Context()
user := User{ID: 123, Name: "Alice"}
res1 := result.Of(user)
@@ -177,7 +177,7 @@ func TestSLogWithCallbackCustomLevel(t *testing.T) {
return logger
}
ctx := context.Background()
ctx := t.Context()
// Create a Result and log it with custom callback
res1 := result.Of(42)
@@ -202,7 +202,7 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
return logger
}
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("warning error")
// Create an error Result and log it with custom callback
@@ -227,7 +227,7 @@ func TestSLogChainedOperations(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
// First log step 1
res1 := result.Of(5)
@@ -255,7 +255,7 @@ func TestSLogPreservesError(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("original error")
res1 := result.Left[int](testErr)
@@ -280,7 +280,7 @@ func TestSLogMultipleValues(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
// Test with different types
intRes := SLog[int]("Integer")(result.Of(42))(ctx)

View File

@@ -41,7 +41,7 @@ func TestPromapBasic(t *testing.T) {
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())
result := adapted(t.Context())
assert.Equal(t, R.Of("42"), result)
})
@@ -63,7 +63,7 @@ func TestContramapBasic(t *testing.T) {
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())
result := adapted(t.Context())
assert.Equal(t, R.Of(100), result)
})
@@ -85,7 +85,7 @@ func TestLocalBasic(t *testing.T) {
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())
result := adapted(t.Context())
assert.Equal(t, R.Of("Alice"), result)
})

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