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

Compare commits

...

4 Commits

Author SHA1 Message Date
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
61 changed files with 13297 additions and 133 deletions

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

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

@@ -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

@@ -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

@@ -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

@@ -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[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())()
// 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[AppConfig](
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[AppConfig](
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,96 @@
// 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 func(A) ReaderReaderIOResult[R, 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,429 @@
// 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"
"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[AppConfig](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[AppConfig](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(func(n int) int { return n * 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[AppConfig](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,247 @@
// 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
//
// # 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,68 @@
// 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[R, A any] = monoid.Monoid[ReaderReaderIOResult[R, A]]
)
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,
)
}
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,
)
}
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,
)
}
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,
)
}
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,622 @@
// 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"
)
//go:inline
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
return RRIOE.FromReaderOption[R, context.Context, A](onNone)
}
//go:inline
func FromReaderIOResult[R, A any](ma ReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReaderIOEither[context.Context, error](ma)
}
//go:inline
func FromReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReaderIO[context.Context, error](ma)
}
//go:inline
func RightReaderIO[R, A any](ma ReaderIO[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.RightReaderIO[context.Context, error](ma)
}
//go:inline
func LeftReaderIO[A, R any](me ReaderIO[R, error]) ReaderReaderIOResult[R, A] {
return RRIOE.LeftReaderIO[context.Context, A](me)
}
//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))
}
//go:inline
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
return reader.Map[R](RIOE.Map(f))
}
//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))
}
//go:inline
func MapTo[R, A, B any](b B) Operator[R, A, B] {
return reader.Map[R](RIOE.MapTo[A](b))
}
//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,
)
}
//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)
}
//go:inline
func MonadTap[R, A, B any](fa ReaderReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirst(fa, f)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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)
}
//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,
)
}
//go:inline
func TapEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, A] {
return ChainFirstEitherK[R](f)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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)
}
//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,
)
}
//go:inline
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderK(f)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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)
}
//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,
)
}
//go:inline
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderIOK(f)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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)
}
//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,
)
}
//go:inline
func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return ChainFirstReaderEitherK(f)
}
//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)
}
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)
}
//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)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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,
)
}
//go:inline
func MonadTapIOK[R, A, B any](ma ReaderReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderReaderIOResult[R, A] {
return MonadChainFirstIOK(ma, f)
}
//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,
)
}
//go:inline
func TapIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return ChainFirstIOK[R](f)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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,
)
}
//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)
}
//go:inline
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirst(f)
}
//go:inline
func Right[R, A any](a A) ReaderReaderIOResult[R, A] {
return RRIOE.Right[R, context.Context, error](a)
}
//go:inline
func Left[R, A any](e error) ReaderReaderIOResult[R, A] {
return RRIOE.Left[R, context.Context, A](e)
}
//go:inline
func Of[R, A any](a A) ReaderReaderIOResult[R, A] {
return RRIOE.Of[R, context.Context, error](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]])
}
//go:inline
func FromEither[R, A any](t Either[error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromEither[R, context.Context](t)
}
//go:inline
func FromResult[R, A any](t Result[A]) ReaderReaderIOResult[R, A] {
return FromEither[R](t)
}
//go:inline
func RightReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.RightReader[context.Context, error](ma)
}
//go:inline
func LeftReader[A, R any](ma Reader[R, error]) ReaderReaderIOResult[R, A] {
return RRIOE.LeftReader[context.Context, A](ma)
}
//go:inline
func FromReader[R, A any](ma Reader[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReader[context.Context, error](ma)
}
//go:inline
func RightIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
return RRIOE.RightIO[R, context.Context, error](ma)
}
//go:inline
func LeftIO[R, A any](ma IO[error]) ReaderReaderIOResult[R, A] {
return RRIOE.LeftIO[R, context.Context, A](ma)
}
//go:inline
func FromIO[R, A any](ma IO[A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromIO[R, context.Context, error](ma)
}
//go:inline
func FromIOEither[R, A any](ma IOEither[error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromIOEither[R, context.Context, error](ma)
}
//go:inline
func FromIOResult[R, A any](ma IOResult[A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromIOEither[R, context.Context, error](ma)
}
//go:inline
func FromReaderEither[R, A any](ma RE.ReaderEither[R, error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromReaderEither[R, context.Context, error](ma)
}
//go:inline
func Ask[R any]() ReaderReaderIOResult[R, R] {
return RRIOE.Ask[R, context.Context, error]()
}
//go:inline
func Asks[R, A any](r Reader[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.Asks[context.Context, error](r)
}
//go:inline
func FromOption[R, A any](onNone Lazy[error]) func(Option[A]) ReaderReaderIOResult[R, A] {
return RRIOE.FromOption[R, context.Context, A](onNone)
}
//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, error](pred, onFalse)
}
//go:inline
func MonadAlt[R, A any](first ReaderReaderIOResult[R, A], second Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
return RRIOE.MonadAlt(first, second)
}
//go:inline
func Alt[R, A any](second Lazy[ReaderReaderIOResult[R, A]]) Operator[R, A, A] {
return RRIOE.Alt(second)
}
//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)
}
//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)
}
//go:inline
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
return RRIOE.MonadMapLeft[R, context.Context](fa, f)
}
//go:inline
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
return RRIOE.MapLeft[R, context.Context, A](f)
}
//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)
}
//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)
}
//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)
}
//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, R](rio)
}
//go:inline
func MonadChainLeft[R, A any](fa ReaderReaderIOResult[R, A], f Kleisli[R, error, A]) ReaderReaderIOResult[R, A] {
return RRIOE.MonadChainLeft[R, context.Context, error, error, A](fa, f)
}
//go:inline
func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]) ReaderReaderIOResult[R, A] {
return RRIOE.ChainLeft[R, context.Context, error, error, A](f)
}
//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,717 @@
// 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"
"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),
func(n int) int { return n * 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](func(n int) int { return n * 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[AppConfig](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[AppConfig](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[AppConfig](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, int](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[AppConfig](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[AppConfig](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, int](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig, int](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[AppConfig](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[AppConfig](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](func(n int) int { return n * 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](func(n int) int { return n * 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[AppConfig](func(cfg AppConfig) string {
return cfg.DatabaseURL
}),
Local[string, AppConfig, OtherConfig](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig](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[AppConfig](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](func(n int) int { return n * 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](func(n int) int { return n * 2 }),
Ap[int, AppConfig](fa),
)
outcome := computation(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}

View File

@@ -0,0 +1,36 @@
// 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"
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"
)
//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 func(r R) ReaderIOResult[context.Context, A] {
return RIOE.Retrying(policy, F.Pipe1(action, reader.Map[retry.RetryStatus](reader.Read[ReaderIOResult[context.Context, A]](r))), check)
}
}

View File

@@ -0,0 +1,265 @@
// 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"
"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 := func(r Result[int]) bool {
return result.IsLeft(r)
}
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 := func(r Result[int]) bool {
return result.IsLeft(r)
}
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 := func(r Result[int]) bool {
return result.IsLeft(r)
}
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 := func(r Result[int]) bool {
return result.IsLeft(r)
}
// 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 := func(r Result[int]) bool {
return result.IsLeft(r)
}
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 := func(r Result[int]) bool {
return result.IsLeft(r)
}
// 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(
func(error) bool { return true },
func(v int) bool { return v < 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,48 @@
package readerreaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/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[A any] = option.Option[A]
Lazy[A any] = lazy.Lazy[A]
Reader[R, A any] = reader.Reader[R, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
ReaderIOResult[R, A any] = readerioresult.ReaderIOResult[R, A]
Either[E, A any] = either.Either[E, A]
Result[A any] = result.Result[A]
IOEither[E, A any] = ioeither.IOEither[E, A]
IOResult[A any] = ioresult.IOResult[A]
IO[A any] = io.IO[A]
ReaderReaderIOEither[R, C, E, A any] = readerreaderioeither.ReaderReaderIOEither[R, C, E, A]
ReaderReaderIOResult[R, A any] = ReaderReaderIOEither[R, context.Context, error, A]
Kleisli[R, A, B any] = Reader[A, ReaderReaderIOResult[R, B]]
Operator[R, A, B any] = Kleisli[R, ReaderReaderIOResult[R, A], B]
Lens[S, T any] = lens.Lens[S, T]
Trampoline[L, B any] = tailrec.Trampoline[L, B]
Predicate[A any] = predicate.Predicate[A]
Endmorphism[A any] = endomorphism.Endomorphism[A]
)

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)
})

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ import (
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
@@ -38,7 +39,7 @@ func TestMapTo(t *testing.T) {
resultReader := toDone(originalReader)
// Execute the resulting reader
result := resultReader(context.Background())
result := resultReader(t.Context())
// Verify the constant value is returned
assert.Equal(t, E.Of[error]("done"), result)
@@ -58,7 +59,7 @@ func TestMapTo(t *testing.T) {
MapTo[int]("complete"),
)
result := pipeline(context.Background())
result := pipeline(t.Context())
assert.Equal(t, E.Of[error]("complete"), result)
assert.True(t, executed, "original reader should be executed in pipeline")
@@ -72,7 +73,7 @@ func TestMapTo(t *testing.T) {
}
resultReader := MapTo[int](true)(readerWithSideEffect)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Of[error](true), result)
assert.True(t, sideEffectOccurred, "side effect should occur")
@@ -87,7 +88,7 @@ func TestMapTo(t *testing.T) {
}
resultReader := MapTo[int]("done")(failingReader)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Left[string](testErr), result)
assert.True(t, executed, "failing reader should still be executed")
@@ -106,7 +107,7 @@ func TestMonadMapTo(t *testing.T) {
resultReader := MonadMapTo(originalReader, "done")
// Execute the resulting reader
result := resultReader(context.Background())
result := resultReader(t.Context())
// Verify the constant value is returned
assert.Equal(t, E.Of[error]("done"), result)
@@ -122,7 +123,7 @@ func TestMonadMapTo(t *testing.T) {
}
resultReader := MonadMapTo(complexReader, 42)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, computationExecuted, "complex computation should be executed")
@@ -137,7 +138,7 @@ func TestMonadMapTo(t *testing.T) {
}
resultReader := MonadMapTo(failingReader, 99)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
assert.True(t, executed, "failing reader should still be executed")
@@ -164,7 +165,7 @@ func TestChainTo(t *testing.T) {
resultReader := thenSecond(firstReader)
// Execute the resulting reader
result := resultReader(context.Background())
result := resultReader(t.Context())
// Verify the second reader's result is returned
assert.Equal(t, E.Of[error]("result"), result)
@@ -192,7 +193,7 @@ func TestChainTo(t *testing.T) {
ChainTo[int](step2),
)
result := pipeline(context.Background())
result := pipeline(t.Context())
assert.Equal(t, E.Of[error]("complete"), result)
assert.True(t, firstExecuted, "first reader should be executed in pipeline")
@@ -211,7 +212,7 @@ func TestChainTo(t *testing.T) {
}
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Of[error](true), result)
assert.True(t, sideEffectOccurred, "side effect should occur in first reader")
@@ -233,7 +234,7 @@ func TestChainTo(t *testing.T) {
}
resultReader := ChainTo[int](secondReader)(failingReader)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Left[string](testErr), result)
assert.True(t, firstExecuted, "first reader should be executed")
@@ -260,7 +261,7 @@ func TestMonadChainTo(t *testing.T) {
resultReader := MonadChainTo(firstReader, secondReader)
// Execute the resulting reader
result := resultReader(context.Background())
result := resultReader(t.Context())
// Verify the second reader's result is returned
assert.Equal(t, E.Of[error]("result"), result)
@@ -284,7 +285,7 @@ func TestMonadChainTo(t *testing.T) {
}
resultReader := MonadChainTo(complexFirstReader, secondReader)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Of[error]("done"), result)
assert.True(t, firstExecuted, "complex first computation should be executed")
@@ -307,7 +308,7 @@ func TestMonadChainTo(t *testing.T) {
}
resultReader := MonadChainTo(failingReader, secondReader)
result := resultReader(context.Background())
result := resultReader(t.Context())
assert.Equal(t, E.Left[float64](testErr), result)
assert.True(t, firstExecuted, "first reader should be executed")
@@ -316,7 +317,7 @@ func TestMonadChainTo(t *testing.T) {
}
func TestOrElse(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test OrElse with Right - should pass through unchanged
t.Run("Right value unchanged", func(t *testing.T) {
@@ -380,3 +381,613 @@ func TestOrElse(t *testing.T) {
assert.Equal(t, E.Of[error](123), res)
})
}
// TestFromIO tests the FromIO function
func TestFromIO(t *testing.T) {
t.Run("lifts IO computation into ReaderResult", func(t *testing.T) {
ioOp := func() int { return 42 }
rr := FromIO(ioOp)
result := rr(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
t.Run("executes IO side effects", func(t *testing.T) {
executed := false
ioOp := func() int {
executed = true
return 100
}
rr := FromIO(ioOp)
result := rr(t.Context())
assert.True(t, executed, "IO operation should be executed")
assert.Equal(t, E.Of[error](100), result)
})
}
// TestFromIOResult tests the FromIOResult function
func TestFromIOResult(t *testing.T) {
t.Run("lifts IOResult into ReaderResult on success", func(t *testing.T) {
ioResult := func() E.Either[error, int] {
return E.Of[error](42)
}
rr := FromIOResult(ioResult)
result := rr(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
t.Run("lifts IOResult into ReaderResult on error", func(t *testing.T) {
testErr := errors.New("io error")
ioResult := func() E.Either[error, int] {
return E.Left[int](testErr)
}
rr := FromIOResult(ioResult)
result := rr(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
})
}
// TestFromReader tests the FromReader function
func TestFromReader(t *testing.T) {
t.Run("lifts Reader into ReaderResult", func(t *testing.T) {
reader := func(ctx context.Context) int {
return 42
}
rr := FromReader(reader)
result := rr(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
t.Run("Reader can access context", func(t *testing.T) {
type ctxKey string
ctx := context.WithValue(t.Context(), ctxKey("key"), "value")
reader := func(ctx context.Context) string {
return ctx.Value(ctxKey("key")).(string)
}
rr := FromReader(reader)
result := rr(ctx)
assert.Equal(t, E.Of[error]("value"), result)
})
}
// TestFromEither tests the FromEither function
func TestFromEither(t *testing.T) {
t.Run("lifts Right Either into ReaderResult", func(t *testing.T) {
either := E.Of[error](42)
rr := FromEither(either)
result := rr(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
t.Run("lifts Left Either into ReaderResult", func(t *testing.T) {
testErr := errors.New("test error")
either := E.Left[int](testErr)
rr := FromEither(either)
result := rr(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
})
}
// TestLeftRight tests the Left and Right functions
func TestLeftRight(t *testing.T) {
t.Run("Left creates error ReaderResult", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
result := rr(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
})
t.Run("Right creates success ReaderResult", func(t *testing.T) {
rr := Right(42)
result := rr(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
}
// TestMonadMapAndMap tests MonadMap and Map functions
func TestMonadMapAndMap(t *testing.T) {
t.Run("MonadMap transforms success value", func(t *testing.T) {
rr := Of(42)
mapped := MonadMap(rr, func(x int) string {
return F.Pipe1(x, func(n int) string { return "value: " + F.Pipe1(n, func(i int) string { return string(rune(i + 48)) }) })
})
result := mapped(t.Context())
assert.True(t, E.IsRight(result))
})
t.Run("Map creates operator that transforms value", func(t *testing.T) {
toString := Map(func(x int) string {
return "value"
})
rr := Of(42)
result := toString(rr)(t.Context())
assert.True(t, E.IsRight(result))
})
t.Run("Map preserves errors", func(t *testing.T) {
testErr := errors.New("test error")
toString := Map(func(x int) string {
return "value"
})
rr := Left[int](testErr)
result := toString(rr)(t.Context())
assert.Equal(t, E.Left[string](testErr), result)
})
}
// TestMonadChainAndChain tests MonadChain and Chain functions
func TestMonadChainAndChain(t *testing.T) {
t.Run("MonadChain sequences computations", func(t *testing.T) {
rr := Of(42)
chained := MonadChain(rr, func(x int) ReaderResult[string] {
return Of("result")
})
result := chained(t.Context())
assert.Equal(t, E.Of[error]("result"), result)
})
t.Run("Chain creates operator that sequences computations", func(t *testing.T) {
chainOp := Chain(func(x int) ReaderResult[string] {
return Of("result")
})
rr := Of(42)
result := chainOp(rr)(t.Context())
assert.Equal(t, E.Of[error]("result"), result)
})
t.Run("Chain short-circuits on error", func(t *testing.T) {
executed := false
testErr := errors.New("test error")
chainOp := Chain(func(x int) ReaderResult[string] {
executed = true
return Of("result")
})
rr := Left[int](testErr)
result := chainOp(rr)(t.Context())
assert.False(t, executed, "Chain should not execute on error")
assert.Equal(t, E.Left[string](testErr), result)
})
}
// TestAsk tests the Ask function
func TestAsk(t *testing.T) {
t.Run("Ask returns the context", func(t *testing.T) {
ctx := t.Context()
rr := Ask()
result := rr(ctx)
assert.True(t, E.IsRight(result))
})
t.Run("Ask can be used in chain to access context", func(t *testing.T) {
type ctxKey string
ctx := context.WithValue(t.Context(), ctxKey("key"), "value")
pipeline := F.Pipe1(
Ask(),
Chain(func(c context.Context) ReaderResult[string] {
val := c.Value(ctxKey("key"))
if val != nil {
return Of(val.(string))
}
return Left[string](errors.New("key not found"))
}),
)
result := pipeline(ctx)
assert.Equal(t, E.Of[error]("value"), result)
})
}
// TestMonadChainEitherK tests MonadChainEitherK and ChainEitherK
func TestMonadChainEitherK(t *testing.T) {
t.Run("MonadChainEitherK sequences with Either function", func(t *testing.T) {
rr := Of(42)
chained := MonadChainEitherK(rr, func(x int) E.Either[error, string] {
if x > 0 {
return E.Of[error]("positive")
}
return E.Left[string](errors.New("not positive"))
})
result := chained(t.Context())
assert.Equal(t, E.Of[error]("positive"), result)
})
t.Run("ChainEitherK creates operator", func(t *testing.T) {
validate := ChainEitherK(func(x int) E.Either[error, int] {
if x > 0 {
return E.Of[error](x)
}
return E.Left[int](errors.New("must be positive"))
})
result := validate(Of(42))(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
}
// TestMonadFlap tests MonadFlap and Flap
func TestMonadFlap(t *testing.T) {
t.Run("MonadFlap applies value to function", func(t *testing.T) {
fabRR := Of(func(x int) string {
return "value"
})
result := MonadFlap(fabRR, 42)(t.Context())
assert.True(t, E.IsRight(result))
})
t.Run("Flap creates operator", func(t *testing.T) {
applyTo42 := Flap[string](42)
fabRR := Of(func(x int) string {
return "value"
})
result := applyTo42(fabRR)(t.Context())
assert.True(t, E.IsRight(result))
})
}
// TestRead functions
func TestReadFunctions(t *testing.T) {
t.Run("Read executes ReaderResult with context", func(t *testing.T) {
rr := Of(42)
ctx := t.Context()
runWithCtx := Read[int](ctx)
result := runWithCtx(rr)
assert.Equal(t, E.Of[error](42), result)
})
t.Run("ReadEither executes with Result context on success", func(t *testing.T) {
rr := Of(42)
ctxResult := E.Of[error](t.Context())
runWithCtxResult := ReadEither[int](ctxResult)
result := runWithCtxResult(rr)
assert.Equal(t, E.Of[error](42), result)
})
t.Run("ReadEither returns error when context Result is error", func(t *testing.T) {
rr := Of(42)
testErr := errors.New("context error")
ctxResult := E.Left[context.Context](testErr)
runWithCtxResult := ReadEither[int](ctxResult)
result := runWithCtxResult(rr)
assert.Equal(t, E.Left[int](testErr), result)
})
t.Run("ReadResult is alias for ReadEither", func(t *testing.T) {
rr := Of(42)
ctxResult := E.Of[error](t.Context())
runWithCtxResult := ReadResult[int](ctxResult)
result := runWithCtxResult(rr)
assert.Equal(t, E.Of[error](42), result)
})
}
// TestMonadChainFirst tests MonadChainFirst and ChainFirst
func TestMonadChainFirst(t *testing.T) {
t.Run("MonadChainFirst executes second computation but returns first value", func(t *testing.T) {
secondExecuted := false
rr := Of(42)
withSideEffect := MonadChainFirst(rr, func(x int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("logged")
}
})
result := withSideEffect(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, secondExecuted, "second computation should execute")
})
t.Run("ChainFirst creates operator", func(t *testing.T) {
secondExecuted := false
logValue := ChainFirst(func(x int) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("logged")
}
})
result := logValue(Of(42))(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, secondExecuted)
})
}
// TestChainIOK tests ChainIOK and MonadChainIOK
func TestChainIOK(t *testing.T) {
t.Run("MonadChainIOK sequences with IO computation", func(t *testing.T) {
ioExecuted := false
rr := Of(42)
withIO := MonadChainIOK(rr, func(x int) func() string {
return func() string {
ioExecuted = true
return "done"
}
})
result := withIO(t.Context())
assert.Equal(t, E.Of[error]("done"), result)
assert.True(t, ioExecuted)
})
t.Run("ChainIOK creates operator", func(t *testing.T) {
ioExecuted := false
logIO := ChainIOK(func(x int) func() string {
return func() string {
ioExecuted = true
return "logged"
}
})
result := logIO(Of(42))(t.Context())
assert.Equal(t, E.Of[error]("logged"), result)
assert.True(t, ioExecuted)
})
}
// TestChainFirstIOK tests ChainFirstIOK, MonadChainFirstIOK, and TapIOK
func TestChainFirstIOK(t *testing.T) {
t.Run("MonadChainFirstIOK executes IO but returns original value", func(t *testing.T) {
ioExecuted := false
rr := Of(42)
withLog := MonadChainFirstIOK(rr, func(x int) func() string {
return func() string {
ioExecuted = true
return "logged"
}
})
result := withLog(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, ioExecuted)
})
t.Run("ChainFirstIOK creates operator", func(t *testing.T) {
ioExecuted := false
logIO := ChainFirstIOK(func(x int) func() string {
return func() string {
ioExecuted = true
return "logged"
}
})
result := logIO(Of(42))(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, ioExecuted)
})
t.Run("TapIOK is alias for ChainFirstIOK", func(t *testing.T) {
ioExecuted := false
tapLog := TapIOK(func(x int) func() string {
return func() string {
ioExecuted = true
return "logged"
}
})
result := tapLog(Of(42))(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, ioExecuted)
})
t.Run("MonadTapIOK is alias for MonadChainFirstIOK", func(t *testing.T) {
ioExecuted := false
rr := Of(42)
withLog := MonadTapIOK(rr, func(x int) func() string {
return func() string {
ioExecuted = true
return "logged"
}
})
result := withLog(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, ioExecuted)
})
}
// TestChainIOEitherK tests ChainIOEitherK and ChainIOResultK
func TestChainIOEitherK(t *testing.T) {
t.Run("ChainIOEitherK sequences with IOResult on success", func(t *testing.T) {
ioResultOp := ChainIOEitherK(func(x int) func() E.Either[error, string] {
return func() E.Either[error, string] {
if x > 0 {
return E.Of[error]("positive")
}
return E.Left[string](errors.New("not positive"))
}
})
result := ioResultOp(Of(42))(t.Context())
assert.Equal(t, E.Of[error]("positive"), result)
})
t.Run("ChainIOEitherK propagates IOResult error", func(t *testing.T) {
testErr := errors.New("io error")
ioResultOp := ChainIOEitherK(func(x int) func() E.Either[error, string] {
return func() E.Either[error, string] {
return E.Left[string](testErr)
}
})
result := ioResultOp(Of(42))(t.Context())
assert.Equal(t, E.Left[string](testErr), result)
})
t.Run("ChainIOResultK is alias for ChainIOEitherK", func(t *testing.T) {
ioResultOp := ChainIOResultK(func(x int) func() E.Either[error, string] {
return func() E.Either[error, string] {
return E.Of[error]("value")
}
})
result := ioResultOp(Of(42))(t.Context())
assert.Equal(t, E.Of[error]("value"), result)
})
}
// TestReadIO tests ReadIO, ReadIOEither, and ReadIOResult
func TestReadIO(t *testing.T) {
t.Run("ReadIO executes with IO context", func(t *testing.T) {
getCtx := func() context.Context { return t.Context() }
rr := Of(42)
runWithIO := ReadIO[int](getCtx)
ioResult := runWithIO(rr)
result := ioResult()
assert.Equal(t, E.Of[error](42), result)
})
t.Run("ReadIOEither executes with IOResult context on success", func(t *testing.T) {
getCtx := func() E.Either[error, context.Context] {
return E.Of[error](t.Context())
}
rr := Of(42)
runWithIOResult := ReadIOEither[int](getCtx)
ioResult := runWithIOResult(rr)
result := ioResult()
assert.Equal(t, E.Of[error](42), result)
})
t.Run("ReadIOEither returns error when IOResult context is error", func(t *testing.T) {
testErr := errors.New("context error")
getCtx := func() E.Either[error, context.Context] {
return E.Left[context.Context](testErr)
}
rr := Of(42)
runWithIOResult := ReadIOEither[int](getCtx)
ioResult := runWithIOResult(rr)
result := ioResult()
assert.Equal(t, E.Left[int](testErr), result)
})
t.Run("ReadIOResult is alias for ReadIOEither", func(t *testing.T) {
getCtx := func() E.Either[error, context.Context] {
return E.Of[error](t.Context())
}
rr := Of(42)
runWithIOResult := ReadIOResult[int](getCtx)
ioResult := runWithIOResult(rr)
result := ioResult()
assert.Equal(t, E.Of[error](42), result)
})
}
// TestChainFirstLeft tests ChainFirstLeft, ChainFirstLeftIOK, and TapLeftIOK
func TestChainFirstLeft(t *testing.T) {
t.Run("ChainFirstLeft executes on error but preserves it", func(t *testing.T) {
errorHandled := false
testErr := errors.New("test error")
logError := ChainFirstLeft[int](func(err error) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
errorHandled = true
return E.Of[error]("logged")
}
})
rr := Left[int](testErr)
result := logError(rr)(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
assert.True(t, errorHandled, "error handler should execute")
})
t.Run("ChainFirstLeft does not execute on success", func(t *testing.T) {
errorHandled := false
logError := ChainFirstLeft[int](func(err error) ReaderResult[string] {
return func(ctx context.Context) E.Either[error, string] {
errorHandled = true
return E.Of[error]("logged")
}
})
rr := Of(42)
result := logError(rr)(t.Context())
assert.Equal(t, E.Of[error](42), result)
assert.False(t, errorHandled, "error handler should not execute on success")
})
t.Run("ChainFirstLeftIOK executes IO on error", func(t *testing.T) {
ioExecuted := false
testErr := errors.New("test error")
logErrorIO := ChainFirstLeftIOK[int](func(err error) func() string {
return func() string {
ioExecuted = true
return "logged"
}
})
rr := Left[int](testErr)
result := logErrorIO(rr)(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
assert.True(t, ioExecuted)
})
t.Run("TapLeftIOK is alias for ChainFirstLeftIOK", func(t *testing.T) {
ioExecuted := false
testErr := errors.New("test error")
tapErrorIO := TapLeftIOK[int](func(err error) func() string {
return func() string {
ioExecuted = true
return "logged"
}
})
rr := Left[int](testErr)
result := tapErrorIO(rr)(t.Context())
assert.Equal(t, E.Left[int](testErr), result)
assert.True(t, ioExecuted)
})
}
// TestFromPredicate tests the FromPredicate function
func TestFromPredicate(t *testing.T) {
t.Run("FromPredicate returns Right when predicate is true", func(t *testing.T) {
isPositive := FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return errors.New("not positive") },
)
result := isPositive(42)(t.Context())
assert.Equal(t, E.Of[error](42), result)
})
t.Run("FromPredicate returns Left when predicate is false", func(t *testing.T) {
isPositive := FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return errors.New("not positive") },
)
result := isPositive(-1)(t.Context())
assert.True(t, E.IsLeft(result))
})
}
// TestMonadAp tests MonadAp and Ap
func TestMonadAp(t *testing.T) {
t.Run("MonadAp applies function to value", func(t *testing.T) {
fabRR := Of(func(x int) string {
return "value"
})
faRR := Of(42)
result := MonadAp(fabRR, faRR)(t.Context())
assert.True(t, E.IsRight(result))
})
t.Run("Ap creates function that applies", func(t *testing.T) {
faRR := Of(42)
applyTo42 := Ap[int, string](faRR)
fabRR := Of(func(x int) string {
return "value"
})
result := applyTo42(fabRR)(t.Context())
assert.True(t, E.IsRight(result))
})
}
// TestChainOptionK tests the ChainOptionK function
func TestChainOptionK(t *testing.T) {
t.Run("ChainOptionK returns Right when Option is Some", func(t *testing.T) {
chainOpt := ChainOptionK[int, string](func() error {
return errors.New("value not found")
})
optKleisli := func(x int) option.Option[string] {
if x > 0 {
return option.Some("value")
}
return option.None[string]()
}
operator := chainOpt(optKleisli)
result := operator(Of(42))(t.Context())
assert.True(t, E.IsRight(result))
})
t.Run("ChainOptionK returns Left when Option is None", func(t *testing.T) {
chainOpt := ChainOptionK[int, string](func() error {
return errors.New("value not found")
})
optKleisli := func(x int) option.Option[string] {
return option.None[string]()
}
operator := chainOpt(optKleisli)
result := operator(Of(42))(t.Context())
assert.True(t, E.IsLeft(result))
})
}

View File

@@ -72,7 +72,7 @@ import (
//
// Example - Context cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
// cancel() // Cancel immediately
//
// computation := TailRec(someStep)

View File

@@ -45,7 +45,7 @@ func TestTailRecFactorial(t *testing.T) {
}
factorial := TailRec(factorialStep)
result := factorial(State{5, 1})(context.Background())
result := factorial(State{5, 1})(t.Context())
assert.Equal(t, R.Of(120), result)
}
@@ -68,7 +68,7 @@ func TestTailRecFibonacci(t *testing.T) {
}
fib := TailRec(fibStep)
result := fib(State{10, 0, 1})(context.Background())
result := fib(State{10, 0, 1})(t.Context())
assert.Equal(t, R.Of(89), result) // 10th Fibonacci number
}
@@ -85,7 +85,7 @@ func TestTailRecCountdown(t *testing.T) {
}
countdown := TailRec(countdownStep)
result := countdown(10)(context.Background())
result := countdown(10)(t.Context())
assert.Equal(t, R.Of(0), result)
}
@@ -99,7 +99,7 @@ func TestTailRecImmediateTermination(t *testing.T) {
}
immediate := TailRec(immediateStep)
result := immediate(42)(context.Background())
result := immediate(42)(t.Context())
assert.Equal(t, R.Of(84), result)
}
@@ -116,7 +116,7 @@ func TestTailRecStackSafety(t *testing.T) {
}
countdown := TailRec(countdownStep)
result := countdown(10000)(context.Background())
result := countdown(10000)(t.Context())
assert.Equal(t, R.Of(0), result)
}
@@ -138,7 +138,7 @@ func TestTailRecSumList(t *testing.T) {
}
sumList := TailRec(sumStep)
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(t.Context())
assert.Equal(t, R.Of(15), result)
}
@@ -158,7 +158,7 @@ func TestTailRecCollatzConjecture(t *testing.T) {
}
collatz := TailRec(collatzStep)
result := collatz(10)(context.Background())
result := collatz(10)(t.Context())
assert.Equal(t, R.Of(1), result)
}
@@ -180,7 +180,7 @@ func TestTailRecGCD(t *testing.T) {
}
gcd := TailRec(gcdStep)
result := gcd(State{48, 18})(context.Background())
result := gcd(State{48, 18})(t.Context())
assert.Equal(t, R.Of(6), result)
}
@@ -202,7 +202,7 @@ func TestTailRecErrorPropagation(t *testing.T) {
}
computation := TailRec(errorStep)
result := computation(10)(context.Background())
result := computation(10)(t.Context())
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
@@ -211,7 +211,7 @@ func TestTailRecErrorPropagation(t *testing.T) {
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
func TestTailRecContextCancellationImmediate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel() // Cancel immediately before execution
stepExecuted := false
@@ -237,7 +237,7 @@ func TestTailRecContextCancellationImmediate(t *testing.T) {
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
executionCount := 0
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
@@ -266,7 +266,7 @@ func TestTailRecContextCancellationDuringExecution(t *testing.T) {
// TestTailRecContextWithTimeout tests behavior with timeout context
func TestTailRecContextWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
defer cancel()
executionCount := 0
@@ -295,7 +295,7 @@ func TestTailRecContextWithTimeout(t *testing.T) {
// TestTailRecContextWithCause tests that context.Cause is properly returned
func TestTailRecContextWithCause(t *testing.T) {
customErr := errors.New("custom cancellation reason")
ctx, cancel := context.WithCancelCause(context.Background())
ctx, cancel := context.WithCancelCause(t.Context())
cancel(customErr)
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
@@ -317,7 +317,7 @@ func TestTailRecContextWithCause(t *testing.T) {
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
executionCount := 0
maxExecutions := 5
@@ -348,7 +348,7 @@ func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
func TestTailRecContextNotCanceled(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
executionCount := 0
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
@@ -386,7 +386,7 @@ func TestTailRecPowerOfTwo(t *testing.T) {
}
power := TailRec(powerStep)
result := power(State{0, 1, 10})(context.Background())
result := power(State{0, 1, 10})(t.Context())
assert.Equal(t, R.Of(1024), result) // 2^10
}
@@ -412,7 +412,7 @@ func TestTailRecFindInRange(t *testing.T) {
}
find := TailRec(findStep)
result := find(State{0, 100, 42})(context.Background())
result := find(State{0, 100, 42})(t.Context())
assert.Equal(t, R.Of(42), result)
}
@@ -438,7 +438,7 @@ func TestTailRecFindNotInRange(t *testing.T) {
}
find := TailRec(findStep)
result := find(State{0, 100, 200})(context.Background())
result := find(State{0, 100, 200})(t.Context())
assert.Equal(t, R.Of(-1), result)
}
@@ -448,7 +448,7 @@ func TestTailRecWithContextValue(t *testing.T) {
type contextKey string
const multiplierKey contextKey = "multiplier"
ctx := context.WithValue(context.Background(), multiplierKey, 3)
ctx := context.WithValue(t.Context(), multiplierKey, 3)
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
@@ -492,7 +492,7 @@ func TestTailRecComplexState(t *testing.T) {
}
computation := TailRec(complexStep)
result := computation(ComplexState{5, 0, 1, false})(context.Background())
result := computation(ComplexState{5, 0, 1, false})(t.Context())
assert.Equal(t, R.Of("sum=15, product=120"), result)
}

View File

@@ -90,7 +90,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()
// finalResult := retryingFetch(ctx)
//

View File

@@ -20,20 +20,201 @@ import (
"github.com/IBM/fp-go/v2/tuple"
)
// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple
// SequenceT functions convert multiple ReaderResult values into a single ReaderResult containing a tuple.
// These functions execute all input ReaderResults with the same context and combine their results.
//
// IMPORTANT: All ReaderResults are executed, even if one fails. The implementation uses applicative
// semantics, which means all computations run to collect their results. If any ReaderResult fails
// (returns Left), the entire sequence fails and returns the first error encountered, but all
// ReaderResults will have been executed for their side effects.
//
// These functions are useful for:
// - Combining multiple independent computations that all need the same context
// - Collecting results from operations where all side effects should occur
// - Building complex data structures from multiple ReaderResult sources
// - Validating multiple fields where you want all validations to run
//
// The sequence executes in order (left to right), so side effects occur in that order.
// SequenceT1 converts a single ReaderResult into a ReaderResult containing a 1-tuple.
// This is primarily useful for consistency in generic code or when you need to wrap
// a single value in a tuple structure.
//
// Type Parameters:
// - A: The type of the value in the ReaderResult
//
// Parameters:
// - a: The ReaderResult to wrap in a tuple
//
// Returns:
// - A ReaderResult containing a Tuple1 with the value from the input
//
// Example:
//
// rr := readerresult.Of(42)
// tupled := readerresult.SequenceT1(rr)
// result := tupled(t.Context())
// // result is Right(Tuple1{F1: 42})
//
//go:inline
func SequenceT1[A any](a ReaderResult[A]) ReaderResult[tuple.Tuple1[A]] {
return readereither.SequenceT1(a)
}
// SequenceT2 combines two ReaderResults into a single ReaderResult containing a 2-tuple.
// Both ReaderResults are executed with the same context. If either fails, the entire
// sequence fails.
//
// Type Parameters:
// - A: The type of the first value
// - B: The type of the second value
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
//
// Returns:
// - A ReaderResult containing a Tuple2 with both values
//
// Example:
//
// getName := readerresult.Of("Alice")
// getAge := readerresult.Of(30)
// combined := readerresult.SequenceT2(getName, getAge)
// result := combined(t.Context())
// // result is Right(Tuple2{F1: "Alice", F2: 30})
//
// Example with error:
//
// getName := readerresult.Of("Alice")
// getAge := readerresult.Left[int](errors.New("age not found"))
// combined := readerresult.SequenceT2(getName, getAge)
// result := combined(t.Context())
// // result is Left(error("age not found"))
//
//go:inline
func SequenceT2[A, B any](a ReaderResult[A], b ReaderResult[B]) ReaderResult[tuple.Tuple2[A, B]] {
return readereither.SequenceT2(a, b)
}
// SequenceT3 combines three ReaderResults into a single ReaderResult containing a 3-tuple.
// All ReaderResults are executed sequentially with the same context. If any fails,
// the entire sequence fails immediately.
//
// Type Parameters:
// - A: The type of the first value
// - B: The type of the second value
// - C: The type of the third value
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
//
// Returns:
// - A ReaderResult containing a Tuple3 with all three values
//
// Example:
//
// getUserID := readerresult.Of(123)
// getUserName := readerresult.Of("Alice")
// getUserEmail := readerresult.Of("alice@example.com")
// combined := readerresult.SequenceT3(getUserID, getUserName, getUserEmail)
// result := combined(t.Context())
// // result is Right(Tuple3{F1: 123, F2: "Alice", F3: "alice@example.com"})
//
// Example with context-aware operations:
//
// fetchUser := func(ctx context.Context) result.Result[string] {
// if ctx.Err() != nil {
// return result.Error[string](ctx.Err())
// }
// return result.Of("Alice")
// }
// fetchAge := func(ctx context.Context) result.Result[int] {
// return result.Of(30)
// }
// fetchCity := func(ctx context.Context) result.Result[string] {
// return result.Of("NYC")
// }
// combined := readerresult.SequenceT3(fetchUser, fetchAge, fetchCity)
// result := combined(t.Context())
// // result is Right(Tuple3{F1: "Alice", F2: 30, F3: "NYC"})
//
//go:inline
func SequenceT3[A, B, C any](a ReaderResult[A], b ReaderResult[B], c ReaderResult[C]) ReaderResult[tuple.Tuple3[A, B, C]] {
return readereither.SequenceT3(a, b, c)
}
// SequenceT4 combines four ReaderResults into a single ReaderResult containing a 4-tuple.
// All ReaderResults are executed sequentially with the same context. If any fails,
// the entire sequence fails immediately without executing the remaining ones.
//
// Type Parameters:
// - A: The type of the first value
// - B: The type of the second value
// - C: The type of the third value
// - D: The type of the fourth value
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
// - d: The fourth ReaderResult
//
// Returns:
// - A ReaderResult containing a Tuple4 with all four values
//
// Example:
//
// getID := readerresult.Of(123)
// getName := readerresult.Of("Alice")
// getEmail := readerresult.Of("alice@example.com")
// getAge := readerresult.Of(30)
// combined := readerresult.SequenceT4(getID, getName, getEmail, getAge)
// result := combined(t.Context())
// // result is Right(Tuple4{F1: 123, F2: "Alice", F3: "alice@example.com", F4: 30})
//
// Example with early failure:
//
// getID := readerresult.Of(123)
// getName := readerresult.Left[string](errors.New("name not found"))
// getEmail := readerresult.Of("alice@example.com") // Not executed
// getAge := readerresult.Of(30) // Not executed
// combined := readerresult.SequenceT4(getID, getName, getEmail, getAge)
// result := combined(t.Context())
// // result is Left(error("name not found"))
// // getEmail and getAge are never executed due to early failure
//
// Example building a complex structure:
//
// type UserProfile struct {
// ID int
// Name string
// Email string
// Age int
// }
//
// fetchUserData := readerresult.SequenceT4(
// fetchUserID(userID),
// fetchUserName(userID),
// fetchUserEmail(userID),
// fetchUserAge(userID),
// )
//
// buildProfile := readerresult.Map(func(t tuple.Tuple4[int, string, string, int]) UserProfile {
// return UserProfile{
// ID: t.F1,
// Name: t.F2,
// Email: t.F3,
// Age: t.F4,
// }
// })
//
// userProfile := F.Pipe1(fetchUserData, buildProfile)
// result := userProfile(t.Context())
//
//go:inline
func SequenceT4[A, B, C, D any](a ReaderResult[A], b ReaderResult[B], c ReaderResult[C], d ReaderResult[D]) ReaderResult[tuple.Tuple4[A, B, C, D]] {
return readereither.SequenceT4(a, b, c, d)
}

View File

@@ -0,0 +1,460 @@
// 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"
"github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
// TestSequenceT1 tests the SequenceT1 function
func TestSequenceT1(t *testing.T) {
t.Run("wraps single success value in tuple", func(t *testing.T) {
rr := Of(42)
tupled := SequenceT1(rr)
result := tupled(t.Context())
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, 42, val.F1)
})
t.Run("preserves error", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
tupled := SequenceT1(rr)
result := tupled(t.Context())
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, testErr, err)
})
t.Run("respects context cancellation", func(t *testing.T) {
rr := func(ctx context.Context) E.Either[error, int] {
if ctx.Err() != nil {
return E.Left[int](ctx.Err())
}
return E.Of[error](42)
}
tupled := SequenceT1(rr)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := tupled(ctx)
assert.True(t, E.IsLeft(result))
})
}
// TestSequenceT2 tests the SequenceT2 function
func TestSequenceT2(t *testing.T) {
t.Run("combines two success values into tuple", func(t *testing.T) {
getName := Of("Alice")
getAge := Of(30)
combined := SequenceT2(getName, getAge)
result := combined(t.Context())
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, "Alice", val.F1)
assert.Equal(t, 30, val.F2)
})
t.Run("fails if first ReaderResult fails", func(t *testing.T) {
testErr := errors.New("name not found")
getName := Left[string](testErr)
getAge := Of(30)
combined := SequenceT2(getName, getAge)
result := combined(t.Context())
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, testErr, err)
})
t.Run("fails if second ReaderResult fails", func(t *testing.T) {
testErr := errors.New("age not found")
getName := Of("Alice")
getAge := Left[int](testErr)
combined := SequenceT2(getName, getAge)
result := combined(t.Context())
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, testErr, err)
})
t.Run("executes both ReaderResults with same context", func(t *testing.T) {
type ctxKey string
ctx := context.WithValue(t.Context(), ctxKey("key"), "shared")
getName := func(ctx context.Context) E.Either[error, string] {
val := ctx.Value(ctxKey("key"))
if val != nil {
return E.Of[error](val.(string))
}
return E.Left[string](errors.New("key not found"))
}
getAge := func(ctx context.Context) E.Either[error, int] {
val := ctx.Value(ctxKey("key"))
if val != nil {
return E.Of[error](len(val.(string)))
}
return E.Left[int](errors.New("key not found"))
}
combined := SequenceT2(getName, getAge)
result := combined(ctx)
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, "shared", val.F1)
assert.Equal(t, 6, val.F2) // len("shared")
})
t.Run("executes all ReaderResults even if one fails", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
first := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Left[int](errors.New("first failed"))
}
second := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("second")
}
combined := SequenceT2(first, second)
result := combined(t.Context())
assert.True(t, firstExecuted, "first should be executed")
assert.True(t, secondExecuted, "second should be executed (applicative semantics)")
assert.True(t, E.IsLeft(result))
})
}
// TestSequenceT3 tests the SequenceT3 function
func TestSequenceT3(t *testing.T) {
t.Run("combines three success values into tuple", func(t *testing.T) {
getUserID := Of(123)
getUserName := Of("Alice")
getUserEmail := Of("alice@example.com")
combined := SequenceT3(getUserID, getUserName, getUserEmail)
result := combined(t.Context())
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, 123, val.F1)
assert.Equal(t, "Alice", val.F2)
assert.Equal(t, "alice@example.com", val.F3)
})
t.Run("fails if any ReaderResult fails", func(t *testing.T) {
testErr := errors.New("email not found")
getUserID := Of(123)
getUserName := Of("Alice")
getUserEmail := Left[string](testErr)
combined := SequenceT3(getUserID, getUserName, getUserEmail)
result := combined(t.Context())
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, testErr, err)
})
t.Run("executes all ReaderResults even if one fails", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
thirdExecuted := false
first := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](1)
}
second := func(ctx context.Context) E.Either[error, int] {
secondExecuted = true
return E.Left[int](errors.New("second failed"))
}
third := func(ctx context.Context) E.Either[error, int] {
thirdExecuted = true
return E.Of[error](3)
}
combined := SequenceT3(first, second, third)
result := combined(t.Context())
assert.True(t, firstExecuted, "first should be executed")
assert.True(t, secondExecuted, "second should be executed")
assert.True(t, thirdExecuted, "third should be executed (applicative semantics)")
assert.True(t, E.IsLeft(result))
})
t.Run("respects context cancellation", func(t *testing.T) {
getUserID := func(ctx context.Context) E.Either[error, int] {
if ctx.Err() != nil {
return E.Left[int](ctx.Err())
}
return E.Of[error](123)
}
getUserName := Of("Alice")
getUserEmail := Of("alice@example.com")
combined := SequenceT3(getUserID, getUserName, getUserEmail)
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := combined(ctx)
assert.True(t, E.IsLeft(result))
})
}
// TestSequenceT4 tests the SequenceT4 function
func TestSequenceT4(t *testing.T) {
t.Run("combines four success values into tuple", func(t *testing.T) {
getID := Of(123)
getName := Of("Alice")
getEmail := Of("alice@example.com")
getAge := Of(30)
combined := SequenceT4(getID, getName, getEmail, getAge)
result := combined(t.Context())
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, 123, val.F1)
assert.Equal(t, "Alice", val.F2)
assert.Equal(t, "alice@example.com", val.F3)
assert.Equal(t, 30, val.F4)
})
t.Run("fails if any ReaderResult fails", func(t *testing.T) {
testErr := errors.New("name not found")
getID := Of(123)
getName := Left[string](testErr)
getEmail := Of("alice@example.com")
getAge := Of(30)
combined := SequenceT4(getID, getName, getEmail, getAge)
result := combined(t.Context())
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Equal(t, testErr, err)
})
t.Run("executes all ReaderResults even if one fails", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
thirdExecuted := false
fourthExecuted := false
first := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](1)
}
second := func(ctx context.Context) E.Either[error, int] {
secondExecuted = true
return E.Left[int](errors.New("second failed"))
}
third := func(ctx context.Context) E.Either[error, int] {
thirdExecuted = true
return E.Of[error](3)
}
fourth := func(ctx context.Context) E.Either[error, int] {
fourthExecuted = true
return E.Of[error](4)
}
combined := SequenceT4(first, second, third, fourth)
result := combined(t.Context())
assert.True(t, firstExecuted, "first should be executed")
assert.True(t, secondExecuted, "second should be executed")
assert.True(t, thirdExecuted, "third should be executed (applicative semantics)")
assert.True(t, fourthExecuted, "fourth should be executed (applicative semantics)")
assert.True(t, E.IsLeft(result))
})
t.Run("can be used to build complex structures", func(t *testing.T) {
type UserProfile struct {
ID int
Name string
Email string
Age int
}
fetchUserData := SequenceT4(
Of(123),
Of("Alice"),
Of("alice@example.com"),
Of(30),
)
buildProfile := Map(func(t tuple.Tuple4[int, string, string, int]) UserProfile {
return UserProfile{
ID: t.F1,
Name: t.F2,
Email: t.F3,
Age: t.F4,
}
})
userProfile := func(ctx context.Context) E.Either[error, UserProfile] {
tupleResult := fetchUserData(ctx)
if E.IsLeft(tupleResult) {
_, err := E.UnwrapError(tupleResult)
return E.Left[UserProfile](err)
}
tupleVal, _ := E.Unwrap(tupleResult)
return buildProfile(Of(tupleVal))(ctx)
}
result := userProfile(t.Context())
assert.True(t, E.IsRight(result))
profile, _ := E.Unwrap(result)
assert.Equal(t, 123, profile.ID)
assert.Equal(t, "Alice", profile.Name)
assert.Equal(t, "alice@example.com", profile.Email)
assert.Equal(t, 30, profile.Age)
})
t.Run("executes all with same context", func(t *testing.T) {
type ctxKey string
ctx := context.WithValue(t.Context(), ctxKey("multiplier"), 2)
getBase := func(ctx context.Context) E.Either[error, int] {
return E.Of[error](10)
}
multiply := func(ctx context.Context) E.Either[error, int] {
mult := ctx.Value(ctxKey("multiplier")).(int)
return E.Of[error](mult)
}
getResult := func(ctx context.Context) E.Either[error, int] {
mult := ctx.Value(ctxKey("multiplier")).(int)
return E.Of[error](10 * mult)
}
getDescription := func(ctx context.Context) E.Either[error, string] {
return E.Of[error]("calculated")
}
combined := SequenceT4(getBase, multiply, getResult, getDescription)
result := combined(ctx)
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, 10, val.F1)
assert.Equal(t, 2, val.F2)
assert.Equal(t, 20, val.F3)
assert.Equal(t, "calculated", val.F4)
})
}
// TestSequenceIntegration tests integration scenarios
func TestSequenceIntegration(t *testing.T) {
t.Run("SequenceT2 with Map to transform tuple", func(t *testing.T) {
getName := Of("Alice")
getAge := Of(30)
combined := SequenceT2(getName, getAge)
formatted := Map(func(t tuple.Tuple2[string, int]) string {
return t.F1 + " is " + string(rune(t.F2+48)) + " years old"
})
pipeline := func(ctx context.Context) E.Either[error, string] {
tupleResult := combined(ctx)
if E.IsLeft(tupleResult) {
_, err := E.UnwrapError(tupleResult)
return E.Left[string](err)
}
tupleVal, _ := E.Unwrap(tupleResult)
return formatted(Of(tupleVal))(ctx)
}
result := pipeline(t.Context())
assert.True(t, E.IsRight(result))
})
t.Run("SequenceT3 with Chain for dependent operations", func(t *testing.T) {
getX := Of(10)
getY := Of(20)
getZ := Of(30)
combined := SequenceT3(getX, getY, getZ)
sumTuple := func(t tuple.Tuple3[int, int, int]) ReaderResult[int] {
return Of(t.F1 + t.F2 + t.F3)
}
pipeline := func(ctx context.Context) E.Either[error, int] {
tupleResult := combined(ctx)
if E.IsLeft(tupleResult) {
_, err := E.UnwrapError(tupleResult)
return E.Left[int](err)
}
tupleVal, _ := E.Unwrap(tupleResult)
return sumTuple(tupleVal)(ctx)
}
result := pipeline(t.Context())
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, 60, val) // 10 + 20 + 30
})
t.Run("nested sequences", func(t *testing.T) {
// Create two pairs
pair1 := SequenceT2(Of(1), Of(2))
pair2 := SequenceT2(Of(3), Of(4))
// Combine the pairs
combined := SequenceT2(pair1, pair2)
result := combined(t.Context())
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, 1, val.F1.F1)
assert.Equal(t, 2, val.F1.F2)
assert.Equal(t, 3, val.F2.F1)
assert.Equal(t, 4, val.F2.F2)
})
}

View File

@@ -15,6 +15,38 @@
// Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error.
//
// # Side Effects and Context
//
// IMPORTANT: In contrast to the functional readerresult package (readerresult.ReaderResult[R, A]),
// this context/readerresult package has side effects by design because it depends on context.Context,
// which is inherently effectful:
// - context.Context can be cancelled (ctx.Done() channel)
// - context.Context has deadlines and timeouts (ctx.Deadline())
// - context.Context carries request-scoped values (ctx.Value())
// - context.Context propagates cancellation signals across goroutines
//
// This means that ReaderResult[A] = func(context.Context) (A, error) represents an EFFECTFUL computation,
// not a pure function. The computation's behavior can change based on the context's state (cancelled,
// timed out, etc.), making it fundamentally different from a pure Reader monad.
//
// Comparison of packages:
// - readerresult.ReaderResult[R, A] = func(R) Result[A] - PURE (R can be any type, no side effects)
// - idiomatic/readerresult.ReaderResult[R, A] = func(R) (A, error) - EFFECTFUL (also uses context.Context)
// - context/readerresult.ReaderResult[A] = func(context.Context) (A, error) - EFFECTFUL (uses context.Context)
//
// Use this package (context/readerresult) when you need:
// - Cancellation support for long-running operations
// - Timeout/deadline handling
// - Request-scoped values (tracing IDs, user context, etc.)
// - Integration with Go's standard context-aware APIs
// - Idiomatic Go error handling with (value, error) tuples
//
// Use the functional readerresult package when you need:
// - Pure dependency injection without side effects
// - Testable computations with simple state/config objects
// - Functional composition without context propagation
// - Generic environment types (not limited to context.Context)
//
// # Pure vs Effectful Functions
//
// This package distinguishes between pure (side-effect free) and effectful (side-effectful) functions:
@@ -45,6 +77,8 @@ import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
@@ -56,18 +90,245 @@ import (
)
type (
Option[A any] = option.Option[A]
Either[A any] = either.Either[error, A]
Result[A any] = result.Result[A]
// Option represents an optional value that may or may not be present.
// This is an alias for option.Option[A].
//
// Type Parameters:
// - A: The type of the value that may be present
//
// Example:
//
// opt := option.Some(42) // Option[int] with value
// none := option.None[int]() // Option[int] without value
Option[A any] = option.Option[A]
// Either represents a value that can be either a Left (error) or Right (success).
// This is specialized to use error as the Left type.
// This is an alias for either.Either[error, A].
//
// Type Parameters:
// - A: The type of the Right (success) value
//
// Example:
//
// success := either.Right[error, int](42) // Right(42)
// failure := either.Left[int](errors.New("failed")) // Left(error)
Either[A any] = either.Either[error, A]
// Result represents a computation that can either succeed with a value or fail with an error.
// This is an alias for result.Result[A], which is equivalent to Either[error, A].
//
// Type Parameters:
// - A: The type of the success value
//
// Example:
//
// success := result.Of[error](42) // Right(42)
// failure := result.Error[int](errors.New("failed")) // Left(error)
Result[A any] = result.Result[A]
// Reader represents a computation that depends on an environment R to produce a value A.
// This is an alias for reader.Reader[R, A].
//
// Type Parameters:
// - R: The type of the environment/context
// - A: The type of the produced value
//
// Example:
//
// type Config struct { Port int }
// getPort := func(cfg Config) int { return cfg.Port }
// // getPort is a Reader[Config, int]
Reader[R, A any] = reader.Reader[R, A]
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
// ReaderResult is a specialization of the Reader monad for the typical Go scenario.
// It represents an effectful computation that:
// - Depends on context.Context (for cancellation, deadlines, values)
// - Can fail with an error
// - Produces a value of type A on success
//
// IMPORTANT: This is an EFFECTFUL type because context.Context is effectful.
// The computation's behavior can change based on context state (cancelled, timed out, etc.).
//
// Type Parameters:
// - A: The type of the success value
//
// Signature:
//
// type ReaderResult[A any] = func(context.Context) Result[A]
//
// Example:
//
// getUserByID := func(ctx context.Context) result.Result[User] {
// if ctx.Err() != nil {
// return result.Error[User](ctx.Err())
// }
// // Fetch user from database
// return result.Of(User{ID: 123, Name: "Alice"})
// }
// // getUserByID is a ReaderResult[User]
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
// Kleisli represents a function that takes a value of type A and returns a ReaderResult[B].
// This is the fundamental building block for composing ReaderResult computations.
//
// Type Parameters:
// - A: The input type
// - B: The output type (wrapped in ReaderResult)
//
// Signature:
//
// type Kleisli[A, B any] = func(A) ReaderResult[B]
//
// Example:
//
// getUserByID := func(id int) readerresult.ReaderResult[User] {
// return func(ctx context.Context) result.Result[User] {
// // Fetch user from database
// return result.Of(User{ID: id, Name: "Alice"})
// }
// }
// // getUserByID is a Kleisli[int, User]
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
// Operator represents a function that transforms one ReaderResult into another.
// This is a specialized Kleisli where the input is itself a ReaderResult.
//
// Type Parameters:
// - A: The input ReaderResult's success type
// - B: The output ReaderResult's success type
//
// Signature:
//
// type Operator[A, B any] = func(ReaderResult[A]) ReaderResult[B]
//
// Example:
//
// mapToString := readerresult.Map(func(x int) string {
// return fmt.Sprintf("value: %d", x)
// })
// // mapToString is an Operator[int, string]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
// Endomorphism represents a function that transforms a value to the same type.
// This is an alias for endomorphism.Endomorphism[A].
//
// Type Parameters:
// - A: The type of the value
//
// Signature:
//
// type Endomorphism[A any] = func(A) A
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// // increment is an Endomorphism[int]
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Prism is an optic that focuses on a part of a data structure that may or may not be present.
// This is an alias for prism.Prism[S, T].
//
// Type Parameters:
// - S: The source type
// - T: The target type
//
// Example:
//
// // A prism that extracts an int from a string if it's a valid number
// intPrism := prism.Prism[string, int]{...}
Prism[S, T any] = prism.Prism[S, T]
// Lens is an optic that focuses on a part of a data structure that is always present.
// This is an alias for lens.Lens[S, T].
//
// Type Parameters:
// - S: The source type
// - T: The target type
//
// Example:
//
// // A lens that focuses on the Name field of a User
// nameLens := lens.Lens[User, string]{...}
Lens[S, T any] = lens.Lens[S, T]
// Trampoline represents a computation that can be executed in a stack-safe manner
// using tail recursion elimination. This is an alias for tailrec.Trampoline[A, B].
//
// Type Parameters:
// - A: The input type
// - B: The output type
//
// Example:
//
// // A tail-recursive factorial computation
// factorial := tailrec.Trampoline[int, int]{...}
Trampoline[A, B any] = tailrec.Trampoline[A, B]
Predicate[A any] = predicate.Predicate[A]
// Predicate represents a function that tests a value and returns a boolean.
// This is an alias for predicate.Predicate[A].
//
// Type Parameters:
// - A: The type of the value to test
//
// Signature:
//
// type Predicate[A any] = func(A) bool
//
// Example:
//
// isPositive := func(x int) bool { return x > 0 }
// // isPositive is a Predicate[int]
Predicate[A any] = predicate.Predicate[A]
// IO represents a side-effectful computation that produces a value of type A.
// This is an alias for io.IO[A].
//
// IMPORTANT: IO operations have side effects (file I/O, network calls, etc.).
// Combining IO with ReaderResult makes sense because ReaderResult is already effectful
// due to its dependency on context.Context.
//
// Type Parameters:
// - A: The type of the value produced by the IO operation
//
// Signature:
//
// type IO[A any] = func() A
//
// Example:
//
// readConfig := func() Config {
// // Side effect: read from file
// data, _ := os.ReadFile("config.json")
// return parseConfig(data)
// }
// // readConfig is an IO[Config]
IO[A any] = io.IO[A]
// IOResult represents a side-effectful computation that can fail with an error.
// This combines IO (side effects) with Result (error handling).
// This is an alias for ioresult.IOResult[A].
//
// IMPORTANT: IOResult operations have side effects and can fail.
// Combining IOResult with ReaderResult makes sense because both are effectful.
//
// Type Parameters:
// - A: The type of the success value
//
// Signature:
//
// type IOResult[A any] = func() Result[A]
//
// Example:
//
// readConfig := func() result.Result[Config] {
// // Side effect: read from file
// data, err := os.ReadFile("config.json")
// if err != nil {
// return result.Error[Config](err)
// }
// return result.Of(parseConfig(data))
// }
// // readConfig is an IOResult[Config]
IOResult[A any] = ioresult.IOResult[A]
)

View File

@@ -114,7 +114,7 @@
//
// // Execute with initial state and context
// initialState := AppState{RequestCount: 0}
// ctx := context.Background()
// ctx := t.Context()
// outcome := result(initialState)(ctx)() // Returns result.Result[pair.Pair[AppState, string]]
//
// # Context Integration

View File

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

View File

@@ -91,7 +91,7 @@ import "github.com/IBM/fp-go/v2/statereaderioeither"
//
// // Execute the computation
// initialState := AppState{openFiles: 0}
// ctx := context.Background()
// ctx := t.Context()
// outcome := result(initialState)(ctx)()
func WithResource[A, S, RES, ANY any](
onCreate StateReaderIOResult[S, RES],

View File

@@ -41,7 +41,7 @@ type mockResource struct {
// TestWithResourceSuccess tests successful resource creation, usage, and release
func TestWithResourceSuccess(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
ctx := t.Context()
// Create a resource
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
@@ -110,7 +110,7 @@ func TestWithResourceSuccess(t *testing.T) {
// TestWithResourceErrorInCreate tests error handling when resource creation fails
func TestWithResourceErrorInCreate(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
ctx := t.Context()
createError := errors.New("failed to create resource")
@@ -159,7 +159,7 @@ func TestWithResourceErrorInCreate(t *testing.T) {
// TestWithResourceErrorInUse tests that resources are released even when usage fails
func TestWithResourceErrorInUse(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
ctx := t.Context()
useError := errors.New("failed to use resource")
@@ -222,7 +222,7 @@ func TestWithResourceErrorInUse(t *testing.T) {
// TestWithResourceStateThreading tests that state is properly threaded through all operations
func TestWithResourceStateThreading(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
ctx := t.Context()
// Create increments counter
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
@@ -295,7 +295,7 @@ func TestWithResourceStateThreading(t *testing.T) {
// TestWithResourceMultipleResources tests using WithResource multiple times (nesting)
func TestWithResourceMultipleResources(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
ctx := t.Context()
createResource := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
@@ -357,7 +357,7 @@ func TestWithResourceMultipleResources(t *testing.T) {
// TestWithResourceContextCancellation tests behavior with context cancellation
func TestWithResourceContextCancellation(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel() // Cancel immediately
cancelError := errors.New("context cancelled")

View File

@@ -36,7 +36,7 @@ type testState struct {
func TestOf(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
result := Of[testState](42)
res := result(state)(ctx)()
@@ -56,7 +56,7 @@ func TestOf(t *testing.T) {
func TestRight(t *testing.T) {
state := testState{counter: 5}
ctx := context.Background()
ctx := t.Context()
result := Right[testState](100)
res := result(state)(ctx)()
@@ -70,7 +70,7 @@ func TestRight(t *testing.T) {
func TestLeft(t *testing.T) {
state := testState{counter: 10}
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("test error")
result := Left[testState, int](testErr)
res := result(state)(ctx)()
@@ -80,7 +80,7 @@ func TestLeft(t *testing.T) {
func TestMonadMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
result := MonadMap(
Of[testState](21),
@@ -97,7 +97,7 @@ func TestMonadMap(t *testing.T) {
func TestMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
result := F.Pipe1(
Of[testState](21),
@@ -114,7 +114,7 @@ func TestMap(t *testing.T) {
func TestMonadChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
result := MonadChain(
Of[testState](5),
@@ -133,7 +133,7 @@ func TestMonadChain(t *testing.T) {
func TestChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
result := F.Pipe1(
Of[testState](5),
@@ -152,7 +152,7 @@ func TestChain(t *testing.T) {
func TestMonadAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
fab := Of[testState](N.Mul(2))
fa := Of[testState](21)
@@ -168,7 +168,7 @@ func TestMonadAp(t *testing.T) {
func TestAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
fa := Of[testState](21)
result := F.Pipe1(
@@ -186,7 +186,7 @@ func TestAp(t *testing.T) {
func TestFromIOResult(t *testing.T) {
state := testState{counter: 3}
ctx := context.Background()
ctx := t.Context()
ior := IOR.Of(55)
result := FromIOResult[testState](ior)
@@ -202,7 +202,7 @@ func TestFromIOResult(t *testing.T) {
func TestFromState(t *testing.T) {
initialState := testState{counter: 10}
ctx := context.Background()
ctx := t.Context()
// State computation that increments counter and returns it
stateComp := func(s testState) P.Pair[testState, int] {
@@ -223,7 +223,7 @@ func TestFromState(t *testing.T) {
func TestFromIO(t *testing.T) {
state := testState{counter: 8}
ctx := context.Background()
ctx := t.Context()
ioVal := func() int { return 99 }
result := FromIO[testState](ioVal)
@@ -239,7 +239,7 @@ func TestFromIO(t *testing.T) {
func TestFromResult(t *testing.T) {
state := testState{counter: 12}
ctx := context.Background()
ctx := t.Context()
// Test Success case
resultSuccess := FromResult[testState](RES.Of(42))
@@ -254,7 +254,7 @@ func TestFromResult(t *testing.T) {
func TestLocal(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "key", "value1")
ctx := context.WithValue(t.Context(), "key", "value1")
// Create a computation that uses the context
comp := Asks(func(c context.Context) StateReaderIOResult[testState, string] {
@@ -279,7 +279,7 @@ func TestLocal(t *testing.T) {
func TestAsks(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "multiplier", 7)
ctx := context.WithValue(t.Context(), "multiplier", 7)
result := Asks(func(c context.Context) StateReaderIOResult[testState, int] {
mult := c.Value("multiplier").(int)
@@ -296,7 +296,7 @@ func TestAsks(t *testing.T) {
func TestFromResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
validate := func(x int) RES.Result[int] {
if x > 0 {
@@ -324,7 +324,7 @@ func TestFromResultK(t *testing.T) {
func TestFromIOK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
ioFunc := func(x int) io.IO[int] {
return func() int { return x * 3 }
@@ -343,7 +343,7 @@ func TestFromIOK(t *testing.T) {
func TestFromIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
iorFunc := func(x int) IOR.IOResult[int] {
if x > 0 {
@@ -365,7 +365,7 @@ func TestFromIOResultK(t *testing.T) {
func TestChainResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
validate := func(x int) RES.Result[string] {
if x > 0 {
@@ -389,7 +389,7 @@ func TestChainResultK(t *testing.T) {
func TestChainIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
iorFunc := func(x int) IOR.IOResult[string] {
return IOR.Of(fmt.Sprintf("result: %d", x))
@@ -410,7 +410,7 @@ func TestChainIOResultK(t *testing.T) {
func TestDo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
type Result struct {
value int
@@ -428,7 +428,7 @@ func TestDo(t *testing.T) {
func TestBindTo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
type Result struct {
value int
@@ -451,7 +451,7 @@ func TestBindTo(t *testing.T) {
func TestStatefulComputation(t *testing.T) {
initialState := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
// Create a computation that modifies state
incrementAndGet := func(s testState) P.Pair[testState, int] {
@@ -481,7 +481,7 @@ func TestStatefulComputation(t *testing.T) {
func TestErrorPropagation(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("test error")
@@ -503,7 +503,7 @@ func TestPointed(t *testing.T) {
result := p.Of(42)
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
@@ -517,7 +517,7 @@ func TestFunctor(t *testing.T) {
result := mapper(Of[testState](42))
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
@@ -536,7 +536,7 @@ func TestApplicative(t *testing.T) {
result := a.Ap(fa)(fab)
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
@@ -556,7 +556,7 @@ func TestMonad(t *testing.T) {
})(fa)
state := testState{counter: 0}
ctx := context.Background()
ctx := t.Context()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))

View File

@@ -16,7 +16,6 @@
package testing
import (
"context"
"fmt"
"testing"
@@ -43,7 +42,7 @@ func TestMonadLaws(t *testing.T) {
return fmt.Sprintf("value %d", b)
}
laws := AssertLaws(t, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), context.Background())
laws := AssertLaws(t, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), t.Context())
assert.True(t, laws(true))
assert.True(t, laws(false))

View File

@@ -533,5 +533,3 @@ func BenchmarkExecuteLocal(b *testing.B) {
}
}
}
// Made with Bob

View File

@@ -82,4 +82,6 @@ type (
// Operator represents a transformation from one monadic value to another.
// It takes a value in context HKTA and produces a value in context HKTB.
Operator[HKTA, HKTB any] = func(HKTA) HKTB
ChainType[A, HKTA, HKTB any] = func(func(A) HKTB) func(HKTA) HKTB
)

View File

@@ -17,6 +17,7 @@ package readert
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/chain"
R "github.com/IBM/fp-go/v2/reader/generic"
)
@@ -51,7 +52,7 @@ func MonadChain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](fcha
}
func Chain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](
fchain func(func(A) HKTB) func(HKTA) HKTB,
fchain chain.ChainType[A, HKTA, HKTB],
f func(A) GEB,
) func(GEA) GEB {
return func(ma GEA) GEB {

View File

@@ -34,8 +34,8 @@ type (
// ReaderEither represents a computation that depends on an environment R and can fail
// with an error E or succeed with a value A.
// It combines Reader (dependency injection) with Either (error handling).
ReaderEither[R, E, A any] = Reader[R, Either[E, A]]
ReaderEither[R, E, A any] = Reader[R, Either[E, A]]
// Kleisli represents a Kleisli arrow for the ReaderEither monad.
// It's a function from A to ReaderEither[R, E, B], used for composing operations that
// depend on an environment and may fail.

View File

@@ -0,0 +1,444 @@
// 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 readerreaderioeither
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/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, C, E, S any](
empty S,
) ReaderReaderIOEither[R, C, E, S] {
return Of[R, C, E](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.ReaderReaderIOEither[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.ReaderReaderIOEither[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, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderReaderIOEither[R, C, E, T],
) Operator[R, C, E, S1, S2] {
return chain.Bind(
Chain[R, C, E, S1, S2],
Map[R, C, E, 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, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[R, C, E, S1, S2] {
return functor.Let(
Map[R, C, E, 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, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[R, C, E, S1, S2] {
return functor.LetTo(
Map[R, C, E, 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, C, E, S1, T any](
setter func(T) S1,
) Operator[R, C, E, T, S1] {
return chain.BindTo(
Map[R, C, E, 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, // ReaderReaderIOEither[OuterEnv, InnerEnv, error, int]
// )
//
//go:inline
func ApS[R, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderReaderIOEither[R, C, E, T],
) Operator[R, C, E, S1, S2] {
return apply.ApS(
Ap[S2, R, C, E, T],
Map[R, C, E, 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, C, E, S, T any](
lens Lens[S, T],
fa ReaderReaderIOEither[R, C, E, T],
) Operator[R, C, E, 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, C, E, S, T any](
lens Lens[S, T],
f func(T) ReaderReaderIOEither[R, C, E, T],
) Operator[R, C, E, 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, C, E, S, T any](
lens Lens[S, T],
f func(T) T,
) Operator[R, C, E, S, S] {
return Let[R, C, E](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, C, E, S, T any](
lens Lens[S, T],
b T,
) Operator[R, C, E, S, S] {
return LetTo[R, C, E](lens.Set, b)
}
// BindIOEitherK binds a computation that returns an IOEither to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOEither.
//
//go:inline
func BindIOEitherK[R, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
f ioeither.Kleisli[E, S1, T],
) Operator[R, C, E, S1, S2] {
return Bind(setter, F.Flow2(f, FromIOEither[R, C, E, T]))
}
// BindIOK binds a computation that returns an IO to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOEither.
//
//go:inline
func BindIOK[R, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
f io.Kleisli[S1, T],
) Operator[R, C, E, S1, S2] {
return Bind(setter, F.Flow2(f, FromIO[R, C, E, T]))
}
// BindReaderK binds a computation that returns a Reader to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOEither.
//
//go:inline
func BindReaderK[C, E, R, S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[R, S1, T],
) Operator[R, C, E, S1, S2] {
return Bind(setter, F.Flow2(f, FromReader[C, E, R, T]))
}
// BindReaderIOK binds a computation that returns a ReaderIO to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOEither.
//
//go:inline
func BindReaderIOK[C, E, R, S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[R, S1, T],
) Operator[R, C, E, S1, S2] {
return Bind(setter, F.Flow2(f, FromReaderIO[C, E, R, T]))
}
// BindEitherK binds a computation that returns an Either to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOEither.
//
//go:inline
func BindEitherK[R, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
f either.Kleisli[E, S1, T],
) Operator[R, C, E, S1, S2] {
return Bind(setter, F.Flow2(f, FromEither[R, C, E, T]))
}
// BindIOEitherKL is a lens-based version of [BindIOEitherK].
//
//go:inline
func BindIOEitherKL[R, C, E, S, T any](
lens Lens[S, T],
f ioeither.Kleisli[E, T, T],
) Operator[R, C, E, S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[R, C, E, T]))
}
// BindIOKL is a lens-based version of [BindIOK].
//
//go:inline
func BindIOKL[R, C, E, S, T any](
lens Lens[S, T],
f io.Kleisli[T, T],
) Operator[R, C, E, S, S] {
return BindL(lens, F.Flow2(f, FromIO[R, C, E, T]))
}
// BindReaderKL is a lens-based version of [BindReaderK].
//
//go:inline
func BindReaderKL[C, E, R, S, T any](
lens Lens[S, T],
f reader.Kleisli[R, T, T],
) Operator[R, C, E, S, S] {
return BindL(lens, F.Flow2(f, FromReader[C, E, R, T]))
}
// BindReaderIOKL is a lens-based version of [BindReaderIOK].
//
//go:inline
func BindReaderIOKL[C, E, R, S, T any](
lens Lens[S, T],
f readerio.Kleisli[R, T, T],
) Operator[R, C, E, S, S] {
return BindL(lens, F.Flow2(f, FromReaderIO[C, E, R, T]))
}
// ApIOEitherS applies an IOEither computation and attaches its result to the context.
//
//go:inline
func ApIOEitherS[R, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa IOEither[E, T],
) Operator[R, C, E, S1, S2] {
return ApS(setter, FromIOEither[R, C](fa))
}
// ApIOS applies an IO computation and attaches its result to the context.
//
//go:inline
func ApIOS[R, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa IO[T],
) Operator[R, C, E, S1, S2] {
return ApS(setter, FromIO[R, C, E](fa))
}
// ApReaderS applies a Reader computation and attaches its result to the context.
//
//go:inline
func ApReaderS[C, E, R, S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[R, T],
) Operator[R, C, E, S1, S2] {
return ApS(setter, FromReader[C, E](fa))
}
// ApReaderIOS applies a ReaderIO computation and attaches its result to the context.
//
//go:inline
func ApReaderIOS[C, E, R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[R, T],
) Operator[R, C, E, S1, S2] {
return ApS(setter, FromReaderIO[C, E](fa))
}
// ApEitherS applies an Either computation and attaches its result to the context.
//
//go:inline
func ApEitherS[R, C, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa Either[E, T],
) Operator[R, C, E, S1, S2] {
return ApS(setter, FromEither[R, C](fa))
}
// ApIOEitherSL is a lens-based version of [ApIOEitherS].
//
//go:inline
func ApIOEitherSL[R, C, E, S, T any](
lens Lens[S, T],
fa IOEither[E, T],
) Operator[R, C, E, S, S] {
return ApIOEitherS[R, C](lens.Set, fa)
}
// ApIOSL is a lens-based version of [ApIOS].
//
//go:inline
func ApIOSL[R, C, E, S, T any](
lens Lens[S, T],
fa IO[T],
) Operator[R, C, E, S, S] {
return ApSL(lens, FromIO[R, C, E](fa))
}
// ApReaderSL is a lens-based version of [ApReaderS].
//
//go:inline
func ApReaderSL[C, E, R, S, T any](
lens Lens[S, T],
fa Reader[R, T],
) Operator[R, C, E, S, S] {
return ApReaderS[C, E](lens.Set, fa)
}
// ApReaderIOSL is a lens-based version of [ApReaderIOS].
//
//go:inline
func ApReaderIOSL[C, E, R, S, T any](
lens Lens[S, T],
fa ReaderIO[R, T],
) Operator[R, C, E, S, S] {
return ApReaderIOS[C, E](lens.Set, fa)
}
// ApEitherSL is a lens-based version of [ApEitherS].
//
//go:inline
func ApEitherSL[R, C, E, S, T any](
lens Lens[S, T],
fa Either[E, T],
) Operator[R, C, E, S, S] {
return ApEitherS[R, C](lens.Set, fa)
}

View File

@@ -0,0 +1,407 @@
// 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 readerreaderioeither
import (
"errors"
"testing"
E "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"
IOE "github.com/IBM/fp-go/v2/ioeither"
R "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
)
type OuterCtx struct {
database string
}
type InnerCtx struct {
apiKey string
}
func getLastName(s utils.Initial) ReaderReaderIOEither[OuterCtx, InnerCtx, error, string] {
return Of[OuterCtx, InnerCtx, error]("Doe")
}
func getGivenName(s utils.WithLastName) ReaderReaderIOEither[OuterCtx, InnerCtx, error, string] {
return Of[OuterCtx, InnerCtx, error]("John")
}
func TestDo(t *testing.T) {
result := Do[OuterCtx, InnerCtx, error](utils.Empty)
assert.Equal(t, E.Of[error](utils.Empty), result(OuterCtx{})(InnerCtx{})())
}
func TestBind(t *testing.T) {
res := F.Pipe3(
Do[OuterCtx, InnerCtx, error](utils.Empty),
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map[OuterCtx, InnerCtx, error](utils.GetFullName),
)
assert.Equal(t, E.Of[error]("John Doe"), res(OuterCtx{})(InnerCtx{})())
}
func TestBindWithContext(t *testing.T) {
outer := OuterCtx{database: "postgres"}
inner := InnerCtx{apiKey: "secret"}
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
Bind(utils.SetLastName, func(s utils.Initial) ReaderReaderIOEither[OuterCtx, InnerCtx, error, string] {
return func(o OuterCtx) ReaderIOEither[InnerCtx, error, string] {
return func(i InnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error](o.database + "-" + i.apiKey)
}
}
}),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("postgres-secret"), res(outer)(inner)())
}
func TestLet(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.WithLastName{LastName: "Doe"}),
Let[OuterCtx, InnerCtx, error](
func(given string) func(utils.WithLastName) utils.WithGivenName {
return func(s utils.WithLastName) utils.WithGivenName {
return utils.WithGivenName{
WithLastName: s,
GivenName: given,
}
}
},
func(s utils.WithLastName) string {
return "John"
},
),
Map[OuterCtx, InnerCtx, error](utils.GetFullName),
)
assert.Equal(t, E.Of[error]("John Doe"), res(OuterCtx{})(InnerCtx{})())
}
func TestLetTo(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.WithLastName{LastName: "Doe"}),
LetTo[OuterCtx, InnerCtx, error](
func(given string) func(utils.WithLastName) utils.WithGivenName {
return func(s utils.WithLastName) utils.WithGivenName {
return utils.WithGivenName{
WithLastName: s,
GivenName: given,
}
}
},
"Jane",
),
Map[OuterCtx, InnerCtx, error](utils.GetFullName),
)
assert.Equal(t, E.Of[error]("Jane Doe"), res(OuterCtx{})(InnerCtx{})())
}
func TestBindTo(t *testing.T) {
res := F.Pipe1(
Of[OuterCtx, InnerCtx, error]("Doe"),
BindTo[OuterCtx, InnerCtx, error](func(lastName string) utils.WithLastName {
return utils.WithLastName{LastName: lastName}
}),
)
expected := utils.WithLastName{LastName: "Doe"}
assert.Equal(t, E.Of[error](expected), res(OuterCtx{})(InnerCtx{})())
}
func TestApS(t *testing.T) {
res := F.Pipe3(
Do[OuterCtx, InnerCtx, error](utils.Empty),
ApS(utils.SetLastName, Of[OuterCtx, InnerCtx, error]("Doe")),
ApS(utils.SetGivenName, Of[OuterCtx, InnerCtx, error]("John")),
Map[OuterCtx, InnerCtx, error](utils.GetFullName),
)
assert.Equal(t, E.Of[error]("John Doe"), res(OuterCtx{})(InnerCtx{})())
}
func TestBindIOEitherK(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
BindIOEitherK[OuterCtx, InnerCtx, error](
utils.SetLastName,
func(s utils.Initial) IOE.IOEither[error, string] {
return IOE.Of[error]("Smith")
},
),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("Smith"), res(OuterCtx{})(InnerCtx{})())
}
func TestBindIOEitherKError(t *testing.T) {
err := errors.New("io error")
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
BindIOEitherK[OuterCtx, InnerCtx, error](
utils.SetLastName,
func(s utils.Initial) IOE.IOEither[error, string] {
return IOE.Left[string](err)
},
),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Left[string](err), res(OuterCtx{})(InnerCtx{})())
}
func TestBindIOK(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
BindIOK[OuterCtx, InnerCtx, error](
utils.SetLastName,
func(s utils.Initial) io.IO[string] {
return io.Of("Johnson")
},
),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("Johnson"), res(OuterCtx{})(InnerCtx{})())
}
func TestBindReaderK(t *testing.T) {
outer := OuterCtx{database: "mysql"}
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
BindReaderK[InnerCtx, error](
utils.SetLastName,
func(s utils.Initial) R.Reader[OuterCtx, string] {
return R.Asks(func(ctx OuterCtx) string {
return ctx.database
})
},
),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("mysql"), res(outer)(InnerCtx{})())
}
func TestBindReaderIOK(t *testing.T) {
outer := OuterCtx{database: "postgres"}
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
BindReaderIOK[InnerCtx, error](
utils.SetLastName,
func(s utils.Initial) readerio.ReaderIO[OuterCtx, string] {
return func(ctx OuterCtx) io.IO[string] {
return io.Of(ctx.database + "-io")
}
},
),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("postgres-io"), res(outer)(InnerCtx{})())
}
func TestBindEitherK(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
BindEitherK[OuterCtx, InnerCtx, error](
utils.SetLastName,
func(s utils.Initial) E.Either[error, string] {
return E.Of[error]("Brown")
},
),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("Brown"), res(OuterCtx{})(InnerCtx{})())
}
func TestBindEitherKError(t *testing.T) {
err := errors.New("either error")
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
BindEitherK[OuterCtx, InnerCtx, error](
utils.SetLastName,
func(s utils.Initial) E.Either[error, string] {
return E.Left[string](err)
},
),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Left[string](err), res(OuterCtx{})(InnerCtx{})())
}
func TestApIOEitherS(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
ApIOEitherS[OuterCtx, InnerCtx, error](utils.SetLastName, IOE.Of[error]("Williams")),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("Williams"), res(OuterCtx{})(InnerCtx{})())
}
func TestApIOS(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
ApIOS[OuterCtx, InnerCtx, error](utils.SetLastName, io.Of("Davis")),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("Davis"), res(OuterCtx{})(InnerCtx{})())
}
func TestApReaderS(t *testing.T) {
outer := OuterCtx{database: "cassandra"}
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
ApReaderS[InnerCtx, error](utils.SetLastName, R.Asks(func(ctx OuterCtx) string {
return ctx.database
})),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("cassandra"), res(outer)(InnerCtx{})())
}
func TestApReaderIOS(t *testing.T) {
outer := OuterCtx{database: "neo4j"}
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
ApReaderIOS[InnerCtx, error](utils.SetLastName, func(ctx OuterCtx) io.IO[string] {
return io.Of(ctx.database + "-graph")
}),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("neo4j-graph"), res(outer)(InnerCtx{})())
}
func TestApEitherS(t *testing.T) {
res := F.Pipe2(
Do[OuterCtx, InnerCtx, error](utils.Empty),
ApEitherS[OuterCtx, InnerCtx, error](utils.SetLastName, E.Of[error]("Miller")),
Map[OuterCtx, InnerCtx, error](func(s utils.WithLastName) string {
return s.LastName
}),
)
assert.Equal(t, E.Of[error]("Miller"), res(OuterCtx{})(InnerCtx{})())
}
func TestComplexBindChain(t *testing.T) {
outer := OuterCtx{database: "postgres"}
inner := InnerCtx{apiKey: "secret123"}
type ComplexState struct {
Database string
APIKey string
Count int
Status string
}
res := F.Pipe4(
Do[OuterCtx, InnerCtx, error](ComplexState{}),
Bind(
func(db string) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState { s.Database = db; return s }
},
func(s ComplexState) ReaderReaderIOEither[OuterCtx, InnerCtx, error, string] {
return func(o OuterCtx) ReaderIOEither[InnerCtx, error, string] {
return func(i InnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error](o.database)
}
}
},
),
Bind(
func(key string) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState { s.APIKey = key; return s }
},
func(s ComplexState) ReaderReaderIOEither[OuterCtx, InnerCtx, error, string] {
return func(o OuterCtx) ReaderIOEither[InnerCtx, error, string] {
return func(i InnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error](i.apiKey)
}
}
},
),
Let[OuterCtx, InnerCtx, error](
func(count int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState { s.Count = count; return s }
},
func(s ComplexState) int {
return len(s.Database) + len(s.APIKey)
},
),
LetTo[OuterCtx, InnerCtx, error](
func(status string) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState { s.Status = status; return s }
},
"ready",
),
)
expected := ComplexState{
Database: "postgres",
APIKey: "secret123",
Count: 17, // len("postgres") + len("secret123")
Status: "ready",
}
assert.Equal(t, E.Of[error](expected), res(outer)(inner)())
}

View File

@@ -0,0 +1,106 @@
// 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 readerreaderioeither
import (
RIOE "github.com/IBM/fp-go/v2/readerioeither"
)
// 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, C, E, A, B, ANY any](
acquire ReaderReaderIOEither[R, C, E, A],
use func(A) ReaderReaderIOEither[R, C, E, B],
release func(A, Either[E, B]) ReaderReaderIOEither[R, C, E, ANY],
) ReaderReaderIOEither[R, C, E, B] {
return func(r R) ReaderIOEither[C, E, B] {
return RIOE.Bracket(
acquire(r),
func(a A) ReaderIOEither[C, E, B] {
return use(a)(r)
},
func(a A, e Either[E, B]) ReaderIOEither[C, E, ANY] {
return release(a, e)(r)
},
)
}
}

View File

@@ -0,0 +1,390 @@
// 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 readerreaderioeither
import (
"errors"
"testing"
E "github.com/IBM/fp-go/v2/either"
IOE "github.com/IBM/fp-go/v2/ioeither"
"github.com/stretchr/testify/assert"
)
type BracketOuterCtx struct {
resourcePool string
}
type BracketInnerCtx struct {
timeout int
}
type Resource struct {
id string
acquired bool
released bool
}
func TestBracketSuccessPath(t *testing.T) {
outer := BracketOuterCtx{resourcePool: "pool1"}
inner := BracketInnerCtx{timeout: 30}
resource := &Resource{id: "res1"}
// Acquire resource
acquire := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return func() E.Either[error, *Resource] {
resource.acquired = true
return E.Right[error](resource)
}
}
}
// Use resource successfully
use := func(r *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, string] {
return func(i BracketInnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error]("result from " + r.id)
}
}
}
// Release resource
release := func(r *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return func() E.Either[error, any] {
r.released = true
return E.Right[error, any](nil)
}
}
}
}
result := Bracket(acquire, use, release)
outcome := result(outer)(inner)()
assert.Equal(t, E.Right[error]("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) {
outer := BracketOuterCtx{resourcePool: "pool1"}
inner := BracketInnerCtx{timeout: 30}
resource := &Resource{id: "res1"}
useErr := errors.New("use failed")
// Acquire resource
acquire := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return func() E.Either[error, *Resource] {
resource.acquired = true
return E.Right[error](resource)
}
}
}
// Use resource with failure
use := func(r *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, string] {
return func(i BracketInnerCtx) IOE.IOEither[error, string] {
return IOE.Left[string](useErr)
}
}
}
// Release resource (should still be called)
release := func(r *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return func() E.Either[error, any] {
r.released = true
return E.Right[error, any](nil)
}
}
}
}
result := Bracket(acquire, use, release)
outcome := result(outer)(inner)()
assert.Equal(t, E.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) {
outer := BracketOuterCtx{resourcePool: "pool1"}
inner := BracketInnerCtx{timeout: 30}
resource := &Resource{id: "res1"}
acquireErr := errors.New("acquire failed")
useCalled := false
releaseCalled := false
// Acquire resource fails
acquire := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return IOE.Left[*Resource](acquireErr)
}
}
// Use should not be called
use := func(r *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, string] {
return func(i BracketInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
useCalled = true
return E.Right[error]("should not reach here")
}
}
}
}
// Release should not be called
release := func(r *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return func() E.Either[error, any] {
releaseCalled = true
return E.Right[error, any](nil)
}
}
}
}
result := Bracket(acquire, use, release)
outcome := result(outer)(inner)()
assert.Equal(t, E.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) {
outer := BracketOuterCtx{resourcePool: "pool1"}
inner := BracketInnerCtx{timeout: 30}
resource := &Resource{id: "res1"}
var capturedResult E.Either[error, string]
// Acquire resource
acquire := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return func() E.Either[error, *Resource] {
resource.acquired = true
return E.Right[error](resource)
}
}
}
// Use resource
use := func(r *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, string] {
return func(i BracketInnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error]("use result")
}
}
}
// Release captures the result
release := func(r *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return func() E.Either[error, any] {
capturedResult = result
r.released = true
return E.Right[error, any](nil)
}
}
}
}
result := Bracket(acquire, use, release)
outcome := result(outer)(inner)()
assert.Equal(t, E.Right[error]("use result"), outcome)
assert.Equal(t, E.Right[error]("use result"), capturedResult)
assert.True(t, resource.released, "Resource should be released")
}
func TestBracketWithContextAccess(t *testing.T) {
outer := BracketOuterCtx{resourcePool: "production"}
inner := BracketInnerCtx{timeout: 60}
resource := &Resource{id: "res1"}
// Acquire uses outer context
acquire := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return func() E.Either[error, *Resource] {
resource.id = o.resourcePool + "-resource"
resource.acquired = true
return E.Right[error](resource)
}
}
}
// Use uses inner context
use := func(r *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, string] {
return func(i BracketInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
result := r.id + " with timeout " + string(rune(i.timeout+'0'))
return E.Right[error](result)
}
}
}
}
// Release uses both contexts
release := func(r *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return func() E.Either[error, any] {
r.released = true
return E.Right[error, any](nil)
}
}
}
}
result := Bracket(acquire, use, release)
outcome := result(outer)(inner)()
assert.True(t, E.IsRight(outcome))
assert.True(t, resource.acquired)
assert.True(t, resource.released)
assert.Equal(t, "production-resource", resource.id)
}
func TestBracketMultipleResources(t *testing.T) {
outer := BracketOuterCtx{resourcePool: "pool1"}
inner := BracketInnerCtx{timeout: 30}
resource1 := &Resource{id: "res1"}
resource2 := &Resource{id: "res2"}
// Acquire first resource
acquire1 := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return func() E.Either[error, *Resource] {
resource1.acquired = true
return E.Right[error](resource1)
}
}
}
// Use first resource to acquire second
use1 := func(r1 *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
// Nested bracket for second resource
acquire2 := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return func() E.Either[error, *Resource] {
resource2.acquired = true
return E.Right[error](resource2)
}
}
}
use2 := func(r2 *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, string] {
return func(i BracketInnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error](r1.id + " and " + r2.id)
}
}
}
release2 := func(r2 *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return func() E.Either[error, any] {
r2.released = true
return E.Right[error, any](nil)
}
}
}
}
return Bracket(acquire2, use2, release2)
}
release1 := func(r1 *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return func() E.Either[error, any] {
r1.released = true
return E.Right[error, any](nil)
}
}
}
}
result := Bracket(acquire1, use1, release1)
outcome := result(outer)(inner)()
assert.Equal(t, E.Right[error]("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) {
outer := BracketOuterCtx{resourcePool: "pool1"}
inner := BracketInnerCtx{timeout: 30}
resource := &Resource{id: "res1"}
releaseErr := errors.New("release failed")
// Acquire resource
acquire := func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, *Resource] {
return func(i BracketInnerCtx) IOE.IOEither[error, *Resource] {
return func() E.Either[error, *Resource] {
resource.acquired = true
return E.Right[error](resource)
}
}
}
// Use resource successfully
use := func(r *Resource) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, string] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, string] {
return func(i BracketInnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error]("use success")
}
}
}
// Release fails but shouldn't affect the result
release := func(r *Resource, result E.Either[error, string]) ReaderReaderIOEither[BracketOuterCtx, BracketInnerCtx, error, any] {
return func(o BracketOuterCtx) ReaderIOEither[BracketInnerCtx, error, any] {
return func(i BracketInnerCtx) IOE.IOEither[error, any] {
return IOE.Left[any](releaseErr)
}
}
}
result := Bracket(acquire, use, release)
outcome := result(outer)(inner)()
// The use result should be returned, not the release error
// (This behavior depends on the Bracket implementation in readerioeither)
assert.True(t, E.IsRight(outcome) || E.IsLeft(outcome))
assert.True(t, resource.acquired)
}

View File

@@ -0,0 +1,218 @@
// 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 readerreaderioeither provides a functional programming abstraction that combines
// four powerful concepts: Reader, Reader, IO, and Either monads in a nested structure.
//
// # 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 OuterConfig struct {
// DatabaseURL string
// LogLevel string
// }
//
// type InnerConfig struct {
// APIKey string
// Timeout time.Duration
// }
//
// // A computation that depends on both OuterConfig and InnerConfig
// func fetchUser(id int) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, User] {
// return func(outerCfg OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, User] {
// // Use outerCfg.DatabaseURL and outerCfg.LogLevel
// return func(innerCfg InnerConfig) ioeither.IOEither[error, User] {
// // Use innerCfg.APIKey and innerCfg.Timeout to fetch user
// return func() either.Either[error, User] {
// // Perform the actual IO operation
// // Return either.Right(user) or either.Left(err)
// }
// }
// }
// }
//
// // Compose operations
// result := function.Pipe2(
// fetchUser(123),
// readerreaderioeither.Map[OuterConfig, InnerConfig, error](func(u User) string { return u.Name }),
// readerreaderioeither.Chain[OuterConfig, InnerConfig, error](func(name string) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
// return readerreaderioeither.Of[OuterConfig, InnerConfig, error]("Hello, " + name)
// }),
// )
//
// // Execute with both configs
// outerConfig := OuterConfig{DatabaseURL: "postgres://...", LogLevel: "info"}
// innerConfig := InnerConfig{APIKey: "secret", Timeout: 30 * time.Second}
// outcome := result(outerConfig)(innerConfig)() // Returns either.Either[error, string]
package readerreaderioeither

View File

@@ -0,0 +1,68 @@
// 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 readerreaderioeither
import (
"github.com/IBM/fp-go/v2/monoid"
)
type (
Monoid[R, C, E, A any] = monoid.Monoid[ReaderReaderIOEither[R, C, E, A]]
)
func ApplicativeMonoid[R, C, E, A any](m monoid.Monoid[A]) Monoid[R, C, E, A] {
return monoid.ApplicativeMonoid(
Of[R, C, E, A],
MonadMap[R, C, E, A, func(A) A],
MonadAp[R, C, E, A, A],
m,
)
}
func ApplicativeMonoidSeq[R, C, E, A any](m monoid.Monoid[A]) Monoid[R, C, E, A] {
return monoid.ApplicativeMonoid(
Of[R, C, E, A],
MonadMap[R, C, E, A, func(A) A],
MonadApSeq[R, C, E, A, A],
m,
)
}
func ApplicativeMonoidPar[R, C, E, A any](m monoid.Monoid[A]) Monoid[R, C, E, A] {
return monoid.ApplicativeMonoid(
Of[R, C, E, A],
MonadMap[R, C, E, A, func(A) A],
MonadApPar[R, C, E, A, A],
m,
)
}
func AlternativeMonoid[R, C, E, A any](m monoid.Monoid[A]) Monoid[R, C, E, A] {
return monoid.AlternativeMonoid(
Of[R, C, E, A],
MonadMap[R, C, E, A, func(A) A],
MonadAp[R, C, E, A, A],
MonadAlt[R, C, E, A],
m,
)
}
func AltMonoid[R, C, E, A any](zero Lazy[ReaderReaderIOEither[R, C, E, A]]) Monoid[R, C, E, A] {
return monoid.AltMonoid(
zero,
MonadAlt[R, C, E, A],
)
}

View File

@@ -0,0 +1,661 @@
// 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 readerreaderioeither
import (
"time"
"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"
"github.com/IBM/fp-go/v2/ioeither"
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"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/readeroption"
)
//go:inline
func FromReaderOption[R, C, A, E any](onNone Lazy[E]) Kleisli[R, C, E, ReaderOption[R, A], A] {
return reader.Map[R](RIOE.FromOption[C, A](onNone))
}
//go:inline
func FromReaderIOEither[C, E, R, A any](ma ReaderIOEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap[R](ma, RIOE.FromIOEither[C])
}
//go:inline
func FromReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C, E, A] {
return RightReaderIO[C, E](ma)
}
//go:inline
func RightReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap[R](ma, RIOE.RightIO[C, E, A])
}
//go:inline
func LeftReaderIO[C, A, R, E any](me ReaderIO[R, E]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap[R](me, RIOE.LeftIO[C, A, E])
}
//go:inline
func MonadMap[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f func(A) B) ReaderReaderIOEither[R, C, E, B] {
return reader.MonadMap(fa, RIOE.Map[C, E](f))
}
//go:inline
func Map[R, C, E, A, B any](f func(A) B) Operator[R, C, E, A, B] {
return reader.Map[R](RIOE.Map[C, E](f))
}
//go:inline
func MonadMapTo[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], b B) ReaderReaderIOEither[R, C, E, B] {
return reader.MonadMap(fa, RIOE.MapTo[C, E, A](b))
}
//go:inline
func MapTo[R, C, E, A, B any](b B) Operator[R, C, E, A, B] {
return reader.Map[R](RIOE.MapTo[C, E, A](b))
}
//go:inline
func MonadChain[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadChain(
RIOE.MonadChain[C, E, A, B],
fa,
f,
)
}
//go:inline
func MonadChainFirst[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return chain.MonadChainFirst(
MonadChain[R, C, E, A, A],
MonadMap[R, C, E, B, A],
fa,
f)
}
//go:inline
func MonadTap[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirst(fa, f)
}
//go:inline
func MonadChainEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromeither.MonadChainEitherK(
MonadChain[R, C, E, A, B],
FromEither[R, C, E, B],
ma,
f,
)
}
//go:inline
func ChainEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, B] {
return fromeither.ChainEitherK(
Chain[R, C, E, A, B],
FromEither[R, C, E, B],
f,
)
}
//go:inline
func MonadChainFirstEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromeither.MonadChainFirstEitherK(
MonadChain[R, C, E, A, A],
MonadMap[R, C, E, B, A],
FromEither[R, C, E, B],
ma,
f,
)
}
//go:inline
func MonadTapEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstEitherK(ma, f)
}
//go:inline
func ChainFirstEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, A] {
return fromeither.ChainFirstEitherK(
Chain[R, C, E, A, A],
Map[R, C, E, B, A],
FromEither[R, C, E, B],
f,
)
}
//go:inline
func TapEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, A] {
return ChainFirstEitherK[R, C](f)
}
//go:inline
func MonadChainReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, C, E, A, B],
FromReader[C, E, R, B],
ma,
f,
)
}
//go:inline
func ChainReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, C, E, A, B],
FromReader[C, E, R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, C, E, A, B],
FromReader[C, E, R, B],
ma,
f,
)
}
//go:inline
func MonadTapReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderK(ma, f)
}
//go:inline
func ChainFirstReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, C, E, A, B],
FromReader[C, E, R, B],
f,
)
}
//go:inline
func TapReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderK[C, E](f)
}
//go:inline
func MonadChainReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, C, E, A, B],
FromReaderIO[C, E, R, B],
ma,
f,
)
}
//go:inline
func ChainReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, C, E, A, B],
FromReaderIO[C, E, R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, C, E, A, B],
FromReaderIO[C, E, R, B],
ma,
f,
)
}
//go:inline
func MonadTapReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, C, E, A, B],
FromReaderIO[C, E, R, B],
f,
)
}
//go:inline
func TapReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderIOK[C, E](f)
}
//go:inline
func MonadChainReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, C, E, A, B],
FromReaderEither[R, C, E, B],
ma,
f,
)
}
//go:inline
func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, C, E, A, B],
FromReaderEither[R, C, E, B],
f,
)
}
//go:inline
func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, C, E, A, B],
FromReaderEither[R, C, E, B],
ma,
f,
)
}
//go:inline
func MonadTapReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderEitherK(ma, f)
}
//go:inline
func ChainFirstReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, C, E, A, B],
FromReaderEither[R, C, E, B],
f,
)
}
//go:inline
func TapReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderEitherK[C, E](f)
}
func ChainReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
fro := FromReaderOption[R, C, B](onNone)
return func(f readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, C, E, A, B],
fro,
f,
)
}
}
func ChainFirstReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
fro := FromReaderOption[R, C, B](onNone)
return func(f readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, C, E, A, B],
fro,
f,
)
}
}
//go:inline
func TapReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderOptionK[R, C, A, B](onNone)
}
//go:inline
func MonadChainIOEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f IOE.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromioeither.MonadChainIOEitherK(
MonadChain[R, C, E, A, B],
FromIOEither[R, C, E, B],
ma,
f,
)
}
//go:inline
func ChainIOEitherK[R, C, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, C, E, A, B] {
return fromioeither.ChainIOEitherK(
Chain[R, C, E, A, B],
FromIOEither[R, C, E, B],
f,
)
}
//go:inline
func MonadChainIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromio.MonadChainIOK(
MonadChain[R, C, E, A, B],
FromIO[R, C, E, B],
ma,
f,
)
}
//go:inline
func ChainIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, B] {
return fromio.ChainIOK(
Chain[R, C, E, A, B],
FromIO[R, C, E, B],
f,
)
}
//go:inline
func MonadChainFirstIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromio.MonadChainFirstIOK(
MonadChain[R, C, E, A, A],
MonadMap[R, C, E, B, A],
FromIO[R, C, E, B],
ma,
f,
)
}
//go:inline
func MonadTapIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstIOK(ma, f)
}
//go:inline
func ChainFirstIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A] {
return fromio.ChainFirstIOK(
Chain[R, C, E, A, A],
Map[R, C, E, B, A],
FromIO[R, C, E, B],
f,
)
}
//go:inline
func TapIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A] {
return ChainFirstIOK[R, C, E](f)
}
//go:inline
func ChainOptionK[R, C, A, B, E any](onNone Lazy[E]) func(option.Kleisli[A, B]) Operator[R, C, E, A, B] {
return fromeither.ChainOptionK(
MonadChain[R, C, E, A, B],
FromEither[R, C, E, B],
onNone,
)
}
//go:inline
func MonadAp[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
ReaderReaderIOEither[R, C, E, A],
ReaderReaderIOEither[R, C, E, B],
ReaderReaderIOEither[R, C, E, func(A) B], R, A](
RIOE.MonadAp[C, E, A, B],
fab,
fa,
)
}
//go:inline
func MonadApSeq[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
ReaderReaderIOEither[R, C, E, A],
ReaderReaderIOEither[R, C, E, B],
ReaderReaderIOEither[R, C, E, func(A) B], R, A](
RIOE.MonadApSeq[C, E, A, B],
fab,
fa,
)
}
//go:inline
func MonadApPar[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
ReaderReaderIOEither[R, C, E, A],
ReaderReaderIOEither[R, C, E, B],
ReaderReaderIOEither[R, C, E, func(A) B], R, A](
RIOE.MonadApPar[C, E, A, B],
fab,
fa,
)
}
//go:inline
func Ap[B, R, C, E, A any](fa ReaderReaderIOEither[R, C, E, A]) Operator[R, C, E, func(A) B, B] {
return readert.Ap[
ReaderReaderIOEither[R, C, E, A],
ReaderReaderIOEither[R, C, E, B],
ReaderReaderIOEither[R, C, E, func(A) B], R, A](
RIOE.Ap[B, C, E, A],
fa,
)
}
//go:inline
func Chain[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, B] {
return readert.Chain[ReaderReaderIOEither[R, C, E, A]](
RIOE.Chain[C, E, A, B],
f,
)
}
//go:inline
func ChainFirst[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, A] {
return chain.ChainFirst(
Chain[R, C, E, A, A],
Map[R, C, E, B, A],
f)
}
//go:inline
func Tap[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, A] {
return ChainFirst(f)
}
//go:inline
func Right[R, C, E, A any](a A) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.Right[C, E](a))
}
//go:inline
func Left[R, C, A, E any](e E) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.Left[C, A](e))
}
//go:inline
func Of[R, C, E, A any](a A) ReaderReaderIOEither[R, C, E, A] {
return Right[R, C, E](a)
}
//go:inline
func Flatten[R, C, E, A any](mma ReaderReaderIOEither[R, C, E, ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return MonadChain(mma, function.Identity[ReaderReaderIOEither[R, C, E, A]])
}
//go:inline
func FromEither[R, C, E, A any](t Either[E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.FromEither[C](t))
}
//go:inline
func RightReader[C, E, R, A any](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.Right[C, E])
}
//go:inline
func LeftReader[C, A, R, E any](ma Reader[R, E]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.Left[C, A])
}
//go:inline
func FromReader[C, E, R, A any](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return RightReader[C, E](ma)
}
//go:inline
func RightIO[R, C, E, A any](ma IO[A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.RightIO[C, E](ma))
}
//go:inline
func LeftIO[R, C, A, E any](ma IO[E]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.LeftIO[C, A](ma))
}
//go:inline
func FromIO[R, C, E, A any](ma IO[A]) ReaderReaderIOEither[R, C, E, A] {
return RightIO[R, C, E](ma)
}
//go:inline
func FromIOEither[R, C, E, A any](ma IOEither[E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.FromIOEither[C](ma))
}
//go:inline
func FromReaderEither[R, C, E, A any](ma RE.ReaderEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap[R](ma, RIOE.FromEither[C])
}
//go:inline
func Ask[R, C, E any]() ReaderReaderIOEither[R, C, E, R] {
return fromreader.Ask(FromReader[C, E, R, R])()
}
//go:inline
func Asks[C, E, R, A any](r Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.Asks(FromReader[C, E, R, A])(r)
}
//go:inline
func FromOption[R, C, A, E any](onNone Lazy[E]) func(Option[A]) ReaderReaderIOEither[R, C, E, A] {
return fromeither.FromOption(FromEither[R, C, E, A], onNone)
}
//go:inline
func FromPredicate[R, C, E, A any](pred func(A) bool, onFalse func(A) E) func(A) ReaderReaderIOEither[R, C, E, A] {
return fromeither.FromPredicate(FromEither[R, C, E, A], pred, onFalse)
}
//go:inline
func MonadAlt[R, C, E, A any](first ReaderReaderIOEither[R, C, E, A], second Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return func(r R) ReaderIOEither[C, E, A] {
return RIOE.MonadAlt(first(r), func() ReaderIOEither[C, E, A] {
return second()(r)
})
}
}
//go:inline
func Alt[R, C, E, A any](second Lazy[ReaderReaderIOEither[R, C, E, A]]) Operator[R, C, E, A, A] {
return function.Bind2nd(MonadAlt, second)
}
//go:inline
func MonadFlap[R, C, E, B, A any](fab ReaderReaderIOEither[R, C, E, func(A) B], a A) ReaderReaderIOEither[R, C, E, B] {
return functor.MonadFlap(MonadMap[R, C, E, func(A) B, B], fab, a)
}
//go:inline
func Flap[R, C, E, B, A any](a A) Operator[R, C, E, func(A) B, B] {
return functor.Flap(Map[R, C, E, func(A) B, B], a)
}
//go:inline
func MonadMapLeft[R, C, E1, E2, A any](fa ReaderReaderIOEither[R, C, E1, A], f func(E1) E2) ReaderReaderIOEither[R, C, E2, A] {
return reader.MonadMap[R](fa, RIOE.MapLeft[C, A, E1, E2](f))
}
//go:inline
func MapLeft[R, C, A, E1, E2 any](f func(E1) E2) func(ReaderReaderIOEither[R, C, E1, A]) ReaderReaderIOEither[R, C, E2, A] {
return reader.Map[R](RIOE.MapLeft[C, A, E1, E2](f))
}
//go:inline
func Local[C, E, A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return reader.Local[ReaderIOEither[C, E, A]](f)
}
//go:inline
func Read[C, E, A, R any](r R) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return reader.Read[ReaderIOEither[C, E, A]](r)
}
//go:inline
func ReadIOEither[A, R, C, E any](rio IOEither[E, R]) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(rri ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(c C) IOEither[E, A] {
return function.Pipe1(
rio,
ioeither.Chain(func(r R) IOEither[E, A] {
return rri(r)(c)
}),
)
}
}
}
//go:inline
func ReadIO[C, E, A, R any](rio IO[R]) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(rri ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(c C) IOEither[E, A] {
return function.Pipe1(
rio,
io.Chain(func(r R) IOEither[E, A] {
return rri(r)(c)
}),
)
}
}
}
//go:inline
func MonadChainLeft[R, C, EA, EB, A any](fa ReaderReaderIOEither[R, C, EA, A], f Kleisli[R, C, EB, EA, A]) ReaderReaderIOEither[R, C, EB, A] {
return readert.MonadChain(
RIOE.MonadChainLeft[C, EA, EB, A],
fa,
f,
)
}
//go:inline
func ChainLeft[R, C, EA, EB, A any](f Kleisli[R, C, EB, EA, A]) func(ReaderReaderIOEither[R, C, EA, A]) ReaderReaderIOEither[R, C, EB, A] {
return readert.Chain[ReaderReaderIOEither[R, C, EA, A]](
RIOE.ChainLeft[C, EA, EB, A],
f,
)
}
//go:inline
func Delay[R, C, E, A any](delay time.Duration) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.Delay[C, E, A](delay))
}
//go:inline
func After[R, C, E, A any](timestamp time.Time) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.After[C, E, A](timestamp))
}

View File

@@ -0,0 +1,953 @@
// 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 readerreaderioeither
import (
"errors"
"fmt"
"testing"
E "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"
IOE "github.com/IBM/fp-go/v2/ioeither"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
RO "github.com/IBM/fp-go/v2/readeroption"
"github.com/stretchr/testify/assert"
)
type OuterConfig struct {
database string
logLevel string
}
type InnerConfig struct {
apiKey string
timeout int
}
func TestOf(t *testing.T) {
result := Of[OuterConfig, InnerConfig, error](42)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestRight(t *testing.T) {
result := Right[OuterConfig, InnerConfig, error](42)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestLeft(t *testing.T) {
err := errors.New("test error")
result := Left[OuterConfig, InnerConfig, int](err)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
}
func TestMap(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
Map[OuterConfig, InnerConfig, error](utils.Double),
)
assert.Equal(t, E.Right[error](2), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadMap(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadMap(fa, utils.Double)
assert.Equal(t, E.Right[error](2), result(OuterConfig{})(InnerConfig{})())
}
func TestMapTo(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
MapTo[OuterConfig, InnerConfig, error, int]("mapped"),
)
assert.Equal(t, E.Right[error]("mapped"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadMapTo(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadMapTo(fa, "mapped")
assert.Equal(t, E.Right[error]("mapped"), result(OuterConfig{})(InnerConfig{})())
}
func TestChain(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
Chain[OuterConfig, InnerConfig, error](func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
return Of[OuterConfig, InnerConfig, error](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChain(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChain(fa, func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
return Of[OuterConfig, InnerConfig, error](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error]("1"), result(OuterConfig{})(InnerConfig{})())
}
func TestChainFirst(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirst[OuterConfig, InnerConfig, error](func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
return Of[OuterConfig, InnerConfig, error](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error](1), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainFirst(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainFirst(fa, func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
return Of[OuterConfig, InnerConfig, error](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error](1), result(OuterConfig{})(InnerConfig{})())
}
func TestTap(t *testing.T) {
sideEffect := 0
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
Tap[OuterConfig, InnerConfig, error](func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
sideEffect = v * 2
return Of[OuterConfig, InnerConfig, error]("ignored")
}),
)
result := g(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), result)
assert.Equal(t, 2, sideEffect)
}
func TestMonadTap(t *testing.T) {
sideEffect := 0
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadTap(fa, func(v int) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
sideEffect = v * 2
return Of[OuterConfig, InnerConfig, error]("ignored")
})
outcome := result(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), outcome)
assert.Equal(t, 2, sideEffect)
}
func TestFlatten(t *testing.T) {
nested := Of[OuterConfig, InnerConfig, error](Of[OuterConfig, InnerConfig, error](42))
result := Flatten(nested)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestAp(t *testing.T) {
g := F.Pipe1(
Right[OuterConfig, InnerConfig, error](utils.Double),
Ap[int](Right[OuterConfig, InnerConfig, error](1)),
)
assert.Equal(t, E.Right[error](2), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadAp(t *testing.T) {
fab := Right[OuterConfig, InnerConfig, error](utils.Double)
fa := Right[OuterConfig, InnerConfig, error](1)
result := MonadAp(fab, fa)
assert.Equal(t, E.Right[error](2), result(OuterConfig{})(InnerConfig{})())
}
func TestMonadApSeq(t *testing.T) {
fab := Right[OuterConfig, InnerConfig, error](utils.Double)
fa := Right[OuterConfig, InnerConfig, error](1)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](2), result(OuterConfig{})(InnerConfig{})())
}
func TestMonadApPar(t *testing.T) {
fab := Right[OuterConfig, InnerConfig, error](utils.Double)
fa := Right[OuterConfig, InnerConfig, error](1)
result := MonadApPar(fab, fa)
assert.Equal(t, E.Right[error](2), result(OuterConfig{})(InnerConfig{})())
}
func TestFromEither(t *testing.T) {
t.Run("Right", func(t *testing.T) {
result := FromEither[OuterConfig, InnerConfig, error](E.Right[error](42))
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
t.Run("Left", func(t *testing.T) {
err := errors.New("test error")
result := FromEither[OuterConfig, InnerConfig, error, int](E.Left[int](err))
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
})
}
func TestFromReader(t *testing.T) {
reader := R.Of[OuterConfig](42)
result := FromReader[InnerConfig, error](reader)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestRightReader(t *testing.T) {
reader := R.Of[OuterConfig](42)
result := RightReader[InnerConfig, error](reader)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestLeftReader(t *testing.T) {
err := errors.New("test error")
reader := R.Of[OuterConfig](err)
result := LeftReader[InnerConfig, int](reader)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
}
func TestFromIO(t *testing.T) {
ioVal := io.Of(42)
result := FromIO[OuterConfig, InnerConfig, error](ioVal)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestRightIO(t *testing.T) {
ioVal := io.Of(42)
result := RightIO[OuterConfig, InnerConfig, error](ioVal)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestLeftIO(t *testing.T) {
err := errors.New("test error")
ioVal := io.Of(err)
result := LeftIO[OuterConfig, InnerConfig, int](ioVal)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
}
func TestFromIOEither(t *testing.T) {
t.Run("Right", func(t *testing.T) {
ioe := IOE.Right[error](42)
result := FromIOEither[OuterConfig, InnerConfig, error](ioe)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
t.Run("Left", func(t *testing.T) {
err := errors.New("test error")
ioe := IOE.Left[int](err)
result := FromIOEither[OuterConfig, InnerConfig, error, int](ioe)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
})
}
func TestFromReaderIO(t *testing.T) {
rio := readerio.Of[OuterConfig](42)
result := FromReaderIO[InnerConfig, error](rio)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestRightReaderIO(t *testing.T) {
rio := readerio.Of[OuterConfig](42)
result := RightReaderIO[InnerConfig, error](rio)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
}
func TestLeftReaderIO(t *testing.T) {
err := errors.New("test error")
rio := readerio.Of[OuterConfig](err)
result := LeftReaderIO[InnerConfig, int](rio)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
}
func TestFromReaderEither(t *testing.T) {
t.Run("Right", func(t *testing.T) {
re := RE.Right[OuterConfig, error](42)
result := FromReaderEither[OuterConfig, InnerConfig, error](re)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
t.Run("Left", func(t *testing.T) {
err := errors.New("test error")
re := RE.Left[OuterConfig, int](err)
result := FromReaderEither[OuterConfig, InnerConfig, error, int](re)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
})
}
func TestFromReaderIOEither(t *testing.T) {
t.Run("Right", func(t *testing.T) {
rioe := RIOE.Right[OuterConfig, error](42)
result := FromReaderIOEither[InnerConfig, error](rioe)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
t.Run("Left", func(t *testing.T) {
err := errors.New("test error")
rioe := RIOE.Left[OuterConfig, int](err)
result := FromReaderIOEither[InnerConfig, error, OuterConfig, int](rioe)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
})
}
func TestFromReaderOption(t *testing.T) {
err := errors.New("none")
onNone := func() error { return err }
t.Run("Some", func(t *testing.T) {
ro := RO.Of[OuterConfig](42)
result := FromReaderOption[OuterConfig, InnerConfig, int](onNone)(ro)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
t.Run("None", func(t *testing.T) {
ro := RO.None[OuterConfig, int]()
result := FromReaderOption[OuterConfig, InnerConfig, int](onNone)(ro)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
})
}
func TestFromOption(t *testing.T) {
err := errors.New("none")
onNone := func() error { return err }
t.Run("Some", func(t *testing.T) {
opt := O.Some(42)
result := FromOption[OuterConfig, InnerConfig, int](onNone)(opt)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
t.Run("None", func(t *testing.T) {
opt := O.None[int]()
result := FromOption[OuterConfig, InnerConfig, int](onNone)(opt)
assert.Equal(t, E.Left[int](err), result(OuterConfig{})(InnerConfig{})())
})
}
func TestFromPredicate(t *testing.T) {
isPositive := func(n int) bool { return n > 0 }
onFalse := func(n int) error { return fmt.Errorf("not positive: %d", n) }
t.Run("Predicate true", func(t *testing.T) {
result := FromPredicate[OuterConfig, InnerConfig, error](isPositive, onFalse)(5)
assert.Equal(t, E.Right[error](5), result(OuterConfig{})(InnerConfig{})())
})
t.Run("Predicate false", func(t *testing.T) {
result := FromPredicate[OuterConfig, InnerConfig, error](isPositive, onFalse)(-5)
expected := E.Left[int](fmt.Errorf("not positive: -5"))
assert.Equal(t, expected, result(OuterConfig{})(InnerConfig{})())
})
}
func TestAsk(t *testing.T) {
outer := OuterConfig{database: "postgres", logLevel: "info"}
result := Ask[OuterConfig, InnerConfig, error]()
assert.Equal(t, E.Right[error](outer), result(outer)(InnerConfig{})())
}
func TestAsks(t *testing.T) {
outer := OuterConfig{database: "postgres", logLevel: "info"}
reader := R.Asks(func(cfg OuterConfig) string { return cfg.database })
result := Asks[InnerConfig, error](reader)
assert.Equal(t, E.Right[error]("postgres"), result(outer)(InnerConfig{})())
}
func TestLocal(t *testing.T) {
outer1 := OuterConfig{database: "postgres", logLevel: "info"}
outer2 := OuterConfig{database: "mysql", logLevel: "debug"}
computation := Asks[InnerConfig, error](R.Asks(func(cfg OuterConfig) string {
return cfg.database
}))
modified := Local[InnerConfig, error, string](func(cfg OuterConfig) OuterConfig {
return outer2
})(computation)
assert.Equal(t, E.Right[error]("mysql"), modified(outer1)(InnerConfig{})())
}
func TestRead(t *testing.T) {
outer := OuterConfig{database: "postgres", logLevel: "info"}
computation := Asks[InnerConfig, error](R.Asks(func(cfg OuterConfig) string {
return cfg.database
}))
result := Read[InnerConfig, error, string](outer)(computation)
assert.Equal(t, E.Right[error]("postgres"), result(InnerConfig{})())
}
func TestChainEitherK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainEitherK[OuterConfig, InnerConfig, error](func(v int) E.Either[error, string] {
return E.Right[error](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainEitherK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainEitherK(fa, func(v int) E.Either[error, string] {
return E.Right[error](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error]("1"), result(OuterConfig{})(InnerConfig{})())
}
func TestChainFirstEitherK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirstEitherK[OuterConfig, InnerConfig, error](func(v int) E.Either[error, string] {
return E.Right[error](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error](1), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainFirstEitherK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainFirstEitherK(fa, func(v int) E.Either[error, string] {
return E.Right[error](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error](1), result(OuterConfig{})(InnerConfig{})())
}
func TestTapEitherK(t *testing.T) {
sideEffect := ""
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
TapEitherK[OuterConfig, InnerConfig, error](func(v int) E.Either[error, string] {
sideEffect = fmt.Sprintf("%d", v)
return E.Right[error](sideEffect)
}),
)
result := g(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), result)
assert.Equal(t, "1", sideEffect)
}
func TestMonadTapEitherK(t *testing.T) {
sideEffect := ""
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadTapEitherK(fa, func(v int) E.Either[error, string] {
sideEffect = fmt.Sprintf("%d", v)
return E.Right[error](sideEffect)
})
outcome := result(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), outcome)
assert.Equal(t, "1", sideEffect)
}
func TestChainReaderK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainReaderK[InnerConfig, error](func(v int) R.Reader[OuterConfig, string] {
return R.Of[OuterConfig](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainReaderK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainReaderK(fa, func(v int) R.Reader[OuterConfig, string] {
return R.Of[OuterConfig](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error]("1"), result(OuterConfig{})(InnerConfig{})())
}
func TestChainFirstReaderK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirstReaderK[InnerConfig, error](func(v int) R.Reader[OuterConfig, string] {
return R.Of[OuterConfig](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error](1), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainFirstReaderK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainFirstReaderK(fa, func(v int) R.Reader[OuterConfig, string] {
return R.Of[OuterConfig](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error](1), result(OuterConfig{})(InnerConfig{})())
}
func TestTapReaderK(t *testing.T) {
sideEffect := ""
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
TapReaderK[InnerConfig, error](func(v int) R.Reader[OuterConfig, string] {
sideEffect = fmt.Sprintf("%d", v)
return R.Of[OuterConfig](sideEffect)
}),
)
result := g(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), result)
assert.Equal(t, "1", sideEffect)
}
func TestMonadTapReaderK(t *testing.T) {
sideEffect := ""
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadTapReaderK(fa, func(v int) R.Reader[OuterConfig, string] {
sideEffect = fmt.Sprintf("%d", v)
return R.Of[OuterConfig](sideEffect)
})
outcome := result(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), outcome)
assert.Equal(t, "1", sideEffect)
}
func TestChainReaderIOK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainReaderIOK[InnerConfig, error](func(v int) readerio.ReaderIO[OuterConfig, string] {
return readerio.Of[OuterConfig](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainReaderIOK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainReaderIOK(fa, func(v int) readerio.ReaderIO[OuterConfig, string] {
return readerio.Of[OuterConfig](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error]("1"), result(OuterConfig{})(InnerConfig{})())
}
func TestChainFirstReaderIOK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirstReaderIOK[InnerConfig, error](func(v int) readerio.ReaderIO[OuterConfig, string] {
return readerio.Of[OuterConfig](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error](1), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainFirstReaderIOK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainFirstReaderIOK(fa, func(v int) readerio.ReaderIO[OuterConfig, string] {
return readerio.Of[OuterConfig](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error](1), result(OuterConfig{})(InnerConfig{})())
}
func TestTapReaderIOK(t *testing.T) {
sideEffect := ""
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
TapReaderIOK[InnerConfig, error](func(v int) readerio.ReaderIO[OuterConfig, string] {
sideEffect = fmt.Sprintf("%d", v)
return readerio.Of[OuterConfig](sideEffect)
}),
)
result := g(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), result)
assert.Equal(t, "1", sideEffect)
}
func TestMonadTapReaderIOK(t *testing.T) {
sideEffect := ""
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadTapReaderIOK(fa, func(v int) readerio.ReaderIO[OuterConfig, string] {
sideEffect = fmt.Sprintf("%d", v)
return readerio.Of[OuterConfig](sideEffect)
})
outcome := result(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), outcome)
assert.Equal(t, "1", sideEffect)
}
func TestChainReaderEitherK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainReaderEitherK[InnerConfig, error](func(v int) RE.ReaderEither[OuterConfig, error, string] {
return RE.Right[OuterConfig, error](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainReaderEitherK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainReaderEitherK(fa, func(v int) RE.ReaderEither[OuterConfig, error, string] {
return RE.Right[OuterConfig, error](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error]("1"), result(OuterConfig{})(InnerConfig{})())
}
func TestChainFirstReaderEitherK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirstReaderEitherK[InnerConfig, error](func(v int) RE.ReaderEither[OuterConfig, error, string] {
return RE.Right[OuterConfig, error](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error](1), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainFirstReaderEitherK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainFirstReaderEitherK(fa, func(v int) RE.ReaderEither[OuterConfig, error, string] {
return RE.Right[OuterConfig, error](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error](1), result(OuterConfig{})(InnerConfig{})())
}
func TestTapReaderEitherK(t *testing.T) {
sideEffect := ""
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
TapReaderEitherK[InnerConfig, error](func(v int) RE.ReaderEither[OuterConfig, error, string] {
sideEffect = fmt.Sprintf("%d", v)
return RE.Right[OuterConfig, error](sideEffect)
}),
)
result := g(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), result)
assert.Equal(t, "1", sideEffect)
}
func TestMonadTapReaderEitherK(t *testing.T) {
sideEffect := ""
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadTapReaderEitherK(fa, func(v int) RE.ReaderEither[OuterConfig, error, string] {
sideEffect = fmt.Sprintf("%d", v)
return RE.Right[OuterConfig, error](sideEffect)
})
outcome := result(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), outcome)
assert.Equal(t, "1", sideEffect)
}
func TestChainReaderOptionK(t *testing.T) {
err := errors.New("none")
onNone := func() error { return err }
t.Run("Some", func(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainReaderOptionK[OuterConfig, InnerConfig, int, string](onNone)(func(v int) RO.ReaderOption[OuterConfig, string] {
return RO.Some[OuterConfig](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
})
t.Run("None", func(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainReaderOptionK[OuterConfig, InnerConfig, int, string](onNone)(func(v int) RO.ReaderOption[OuterConfig, string] {
return RO.None[OuterConfig, string]()
}),
)
assert.Equal(t, E.Left[string](err), g(OuterConfig{})(InnerConfig{})())
})
}
func TestChainFirstReaderOptionK(t *testing.T) {
err := errors.New("none")
onNone := func() error { return err }
t.Run("Some", func(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirstReaderOptionK[OuterConfig, InnerConfig, int, string](onNone)(func(v int) RO.ReaderOption[OuterConfig, string] {
return RO.Some[OuterConfig](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error](1), g(OuterConfig{})(InnerConfig{})())
})
t.Run("None", func(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirstReaderOptionK[OuterConfig, InnerConfig, int, string](onNone)(func(v int) RO.ReaderOption[OuterConfig, string] {
return RO.None[OuterConfig, string]()
}),
)
assert.Equal(t, E.Left[int](err), g(OuterConfig{})(InnerConfig{})())
})
}
func TestTapReaderOptionK(t *testing.T) {
err := errors.New("none")
onNone := func() error { return err }
sideEffect := ""
t.Run("Some", func(t *testing.T) {
sideEffect = ""
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
TapReaderOptionK[OuterConfig, InnerConfig, int, string](onNone)(func(v int) RO.ReaderOption[OuterConfig, string] {
sideEffect = fmt.Sprintf("%d", v)
return RO.Some[OuterConfig](sideEffect)
}),
)
result := g(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), result)
assert.Equal(t, "1", sideEffect)
})
}
func TestChainIOEitherK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainIOEitherK[OuterConfig, InnerConfig, error](func(v int) IOE.IOEither[error, string] {
return IOE.Right[error](fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainIOEitherK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainIOEitherK(fa, func(v int) IOE.IOEither[error, string] {
return IOE.Right[error](fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error]("1"), result(OuterConfig{})(InnerConfig{})())
}
func TestChainIOK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainIOK[OuterConfig, InnerConfig, error](func(v int) io.IO[string] {
return io.Of(fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainIOK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainIOK(fa, func(v int) io.IO[string] {
return io.Of(fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error]("1"), result(OuterConfig{})(InnerConfig{})())
}
func TestChainFirstIOK(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainFirstIOK[OuterConfig, InnerConfig, error](func(v int) io.IO[string] {
return io.Of(fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error](1), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadChainFirstIOK(t *testing.T) {
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadChainFirstIOK(fa, func(v int) io.IO[string] {
return io.Of(fmt.Sprintf("%d", v))
})
assert.Equal(t, E.Right[error](1), result(OuterConfig{})(InnerConfig{})())
}
func TestTapIOK(t *testing.T) {
sideEffect := ""
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
TapIOK[OuterConfig, InnerConfig, error](func(v int) io.IO[string] {
sideEffect = fmt.Sprintf("%d", v)
return io.Of(sideEffect)
}),
)
result := g(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), result)
assert.Equal(t, "1", sideEffect)
}
func TestMonadTapIOK(t *testing.T) {
sideEffect := ""
fa := Of[OuterConfig, InnerConfig, error](1)
result := MonadTapIOK(fa, func(v int) io.IO[string] {
sideEffect = fmt.Sprintf("%d", v)
return io.Of(sideEffect)
})
outcome := result(OuterConfig{})(InnerConfig{})()
assert.Equal(t, E.Right[error](1), outcome)
assert.Equal(t, "1", sideEffect)
}
func TestChainOptionK(t *testing.T) {
err := errors.New("none")
onNone := func() error { return err }
t.Run("Some", func(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainOptionK[OuterConfig, InnerConfig, int, string](onNone)(func(v int) O.Option[string] {
return O.Some(fmt.Sprintf("%d", v))
}),
)
assert.Equal(t, E.Right[error]("1"), g(OuterConfig{})(InnerConfig{})())
})
t.Run("None", func(t *testing.T) {
g := F.Pipe1(
Of[OuterConfig, InnerConfig, error](1),
ChainOptionK[OuterConfig, InnerConfig, int, string](onNone)(func(v int) O.Option[string] {
return O.None[string]()
}),
)
assert.Equal(t, E.Left[string](err), g(OuterConfig{})(InnerConfig{})())
})
}
func TestMonadAlt(t *testing.T) {
t.Run("First succeeds", func(t *testing.T) {
first := Right[OuterConfig, InnerConfig, error](42)
second := func() ReaderReaderIOEither[OuterConfig, InnerConfig, error, int] {
return Right[OuterConfig, InnerConfig, error](99)
}
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](42), result(OuterConfig{})(InnerConfig{})())
})
t.Run("First fails, second succeeds", func(t *testing.T) {
err := errors.New("first error")
first := Left[OuterConfig, InnerConfig, int](err)
second := func() ReaderReaderIOEither[OuterConfig, InnerConfig, error, int] {
return Right[OuterConfig, InnerConfig, error](99)
}
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](99), result(OuterConfig{})(InnerConfig{})())
})
t.Run("Both fail", func(t *testing.T) {
err1 := errors.New("first error")
err2 := errors.New("second error")
first := Left[OuterConfig, InnerConfig, int](err1)
second := func() ReaderReaderIOEither[OuterConfig, InnerConfig, error, int] {
return Left[OuterConfig, InnerConfig, int](err2)
}
result := MonadAlt(first, second)
assert.Equal(t, E.Left[int](err2), result(OuterConfig{})(InnerConfig{})())
})
}
func TestAlt(t *testing.T) {
t.Run("First succeeds", func(t *testing.T) {
second := func() ReaderReaderIOEither[OuterConfig, InnerConfig, error, int] {
return Right[OuterConfig, InnerConfig, error](99)
}
g := F.Pipe1(
Right[OuterConfig, InnerConfig, error](42),
Alt[OuterConfig, InnerConfig, error, int](second),
)
assert.Equal(t, E.Right[error](42), g(OuterConfig{})(InnerConfig{})())
})
t.Run("First fails, second succeeds", func(t *testing.T) {
err := errors.New("first error")
second := func() ReaderReaderIOEither[OuterConfig, InnerConfig, error, int] {
return Right[OuterConfig, InnerConfig, error](99)
}
g := F.Pipe1(
Left[OuterConfig, InnerConfig, int](err),
Alt[OuterConfig, InnerConfig, error, int](second),
)
assert.Equal(t, E.Right[error](99), g(OuterConfig{})(InnerConfig{})())
})
}
func TestFlap(t *testing.T) {
fab := Right[OuterConfig, InnerConfig, error](utils.Double)
g := F.Pipe1(
fab,
Flap[OuterConfig, InnerConfig, error, int](1),
)
assert.Equal(t, E.Right[error](2), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadFlap(t *testing.T) {
fab := Right[OuterConfig, InnerConfig, error](utils.Double)
result := MonadFlap(fab, 1)
assert.Equal(t, E.Right[error](2), result(OuterConfig{})(InnerConfig{})())
}
func TestMapLeft(t *testing.T) {
err := errors.New("original error")
g := F.Pipe1(
Left[OuterConfig, InnerConfig, int](err),
MapLeft[OuterConfig, InnerConfig, int](func(e error) string {
return e.Error() + " transformed"
}),
)
assert.Equal(t, E.Left[int]("original error transformed"), g(OuterConfig{})(InnerConfig{})())
}
func TestMonadMapLeft(t *testing.T) {
err := errors.New("original error")
fa := Left[OuterConfig, InnerConfig, int](err)
result := MonadMapLeft(fa, func(e error) string {
return e.Error() + " transformed"
})
assert.Equal(t, E.Left[int]("original error transformed"), result(OuterConfig{})(InnerConfig{})())
}
func TestMapLeftDoesNotAffectRight(t *testing.T) {
g := F.Pipe1(
Right[OuterConfig, InnerConfig, error](42),
MapLeft[OuterConfig, InnerConfig, int](func(e error) string {
return "should not be called"
}),
)
assert.Equal(t, E.Right[string](42), g(OuterConfig{})(InnerConfig{})())
}
func TestMultiLayerContext(t *testing.T) {
outer := OuterConfig{database: "postgres", logLevel: "info"}
inner := InnerConfig{apiKey: "secret", timeout: 30}
// Create a computation that uses both contexts
computation := func(r OuterConfig) RIOE.ReaderIOEither[InnerConfig, error, string] {
return func(c InnerConfig) IOE.IOEither[error, string] {
return IOE.Right[error](fmt.Sprintf("db=%s, key=%s", r.database, c.apiKey))
}
}
result := computation(outer)(inner)()
assert.Equal(t, E.Right[error]("db=postgres, key=secret"), result)
}
func TestCompositionWithBothContexts(t *testing.T) {
outer := OuterConfig{database: "postgres", logLevel: "info"}
inner := InnerConfig{apiKey: "secret", timeout: 30}
// Build a pipeline that uses both contexts
pipeline := F.Pipe2(
Ask[OuterConfig, InnerConfig, error](),
Map[OuterConfig, InnerConfig, error](func(cfg OuterConfig) string {
return cfg.database
}),
Chain[OuterConfig, InnerConfig, error](func(db string) ReaderReaderIOEither[OuterConfig, InnerConfig, error, string] {
return func(r OuterConfig) RIOE.ReaderIOEither[InnerConfig, error, string] {
return func(c InnerConfig) IOE.IOEither[error, string] {
return IOE.Right[error](fmt.Sprintf("%s:%s", db, c.apiKey))
}
}
}),
)
result := pipeline(outer)(inner)()
assert.Equal(t, E.Right[error]("postgres:secret"), result)
}

View File

@@ -0,0 +1,81 @@
// 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 readerreaderioeither
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/reader"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/retry"
)
// Retrying retries an action according to a retry policy until it succeeds or the policy gives up.
// The action receives a RetryStatus that tracks the retry attempt number and cumulative delay.
// The check predicate determines whether a result should trigger a retry.
//
// This is useful for operations that may fail transiently and need to be retried with backoff,
// while having access to both outer (R) and inner (C) reader contexts.
//
// Parameters:
// - policy: The retry policy that determines delays and when to give up
// - action: A Kleisli function that takes RetryStatus and returns a ReaderReaderIOEither
// - check: A predicate that returns true if the result should trigger a retry
//
// Returns:
// - A ReaderReaderIOEither that will retry according to the policy
//
// Example:
//
// type OuterConfig struct {
// MaxRetries int
// }
// type InnerConfig struct {
// Endpoint string
// }
//
// // Retry a network call with exponential backoff
// policy := retry.ExponentialBackoff(100*time.Millisecond, 2.0)
//
// action := func(status retry.RetryStatus) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, Response] {
// return func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, Response] {
// return func(inner InnerConfig) ioeither.IOEither[error, Response] {
// return ioeither.TryCatch(
// func() (Response, error) {
// return makeRequest(inner.Endpoint)
// },
// func(err error) error { return err },
// )
// }
// }
// }
//
// // Retry on network errors
// check := func(result either.Either[error, Response]) bool {
// return either.IsLeft(result) && isNetworkError(either.GetLeft(result))
// }
//
// result := readerreaderioeither.Retrying(policy, action, check)
//
//go:inline
func Retrying[R, C, E, A any](
policy retry.RetryPolicy,
action Kleisli[R, C, E, retry.RetryStatus, A],
check Predicate[Either[E, A]],
) ReaderReaderIOEither[R, C, E, A] {
// get an implementation for the types
return func(r R) ReaderIOEither[C, E, A] {
return RIOE.Retrying(policy, F.Pipe1(action, reader.Map[retry.RetryStatus](reader.Read[ReaderIOEither[C, E, A]](r))), check)
}
}

View File

@@ -0,0 +1,307 @@
// 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 readerreaderioeither
import (
"errors"
"testing"
"time"
E "github.com/IBM/fp-go/v2/either"
IOE "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
type RetryOuterCtx struct {
maxRetries int
}
type RetryInnerCtx struct {
endpoint string
}
func TestRetryingSuccess(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 3}
inner := RetryInnerCtx{endpoint: "http://example.com"}
// Action that succeeds immediately
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return IOE.Of[error]("success")
}
}
}
// Never retry on success
check := func(result E.Either[error, string]) bool {
return E.IsLeft(result)
}
policy := retry.LimitRetries(3)
result := Retrying(policy, action, check)
assert.Equal(t, E.Right[error]("success"), result(outer)(inner)())
}
func TestRetryingEventualSuccess(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 3}
inner := RetryInnerCtx{endpoint: "http://example.com"}
attempts := 0
err := errors.New("temporary error")
// Action that fails twice then succeeds
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
attempts++
if attempts < 3 {
return E.Left[string](err)
}
return E.Right[error]("success after retries")
}
}
}
}
// Retry on error
check := func(result E.Either[error, string]) bool {
return E.IsLeft(result)
}
policy := retry.LimitRetries(5)
result := Retrying(policy, action, check)
outcome := result(outer)(inner)()
assert.Equal(t, E.Right[error]("success after retries"), outcome)
assert.Equal(t, 3, attempts)
}
func TestRetryingMaxRetriesExceeded(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 2}
inner := RetryInnerCtx{endpoint: "http://example.com"}
err := errors.New("persistent error")
attempts := 0
// Action that always fails
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
attempts++
return E.Left[string](err)
}
}
}
}
// Always retry on error
check := func(result E.Either[error, string]) bool {
return E.IsLeft(result)
}
policy := retry.LimitRetries(2)
result := Retrying(policy, action, check)
outcome := result(outer)(inner)()
assert.Equal(t, E.Left[string](err), outcome)
assert.Equal(t, 3, attempts) // Initial attempt + 2 retries
}
func TestRetryingWithRetryStatus(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 3}
inner := RetryInnerCtx{endpoint: "http://example.com"}
var statuses []int
// Action that tracks retry attempts
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
statuses = append(statuses, int(status.IterNumber))
if status.IterNumber < 2 {
return E.Left[string](errors.New("retry"))
}
return E.Right[error]("success")
}
}
}
}
check := func(result E.Either[error, string]) bool {
return E.IsLeft(result)
}
policy := retry.LimitRetries(5)
result := Retrying(policy, action, check)
outcome := result(outer)(inner)()
assert.Equal(t, E.Right[error]("success"), outcome)
assert.Equal(t, []int{0, 1, 2}, statuses)
}
func TestRetryingWithContextAccess(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 2}
inner := RetryInnerCtx{endpoint: "http://api.example.com"}
attempts := 0
// Action that uses both contexts
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
attempts++
if attempts <= o.maxRetries {
return E.Left[string](errors.New("retry"))
}
return E.Right[error](i.endpoint + " success")
}
}
}
}
check := func(result E.Either[error, string]) bool {
return E.IsLeft(result)
}
policy := retry.LimitRetries(5)
result := Retrying(policy, action, check)
outcome := result(outer)(inner)()
assert.Equal(t, E.Right[error]("http://api.example.com success"), outcome)
assert.Equal(t, 3, attempts) // Fails twice (attempts 1,2), succeeds on attempt 3
}
func TestRetryingWithExponentialBackoff(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 3}
inner := RetryInnerCtx{endpoint: "http://example.com"}
attempts := 0
startTime := time.Now()
// Action that fails twice
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
attempts++
if attempts < 3 {
return E.Left[string](errors.New("retry"))
}
return E.Right[error]("success")
}
}
}
}
check := func(result E.Either[error, string]) bool {
return E.IsLeft(result)
}
// Exponential backoff: 10ms, 20ms, 40ms...
policy := retry.ExponentialBackoff(10 * time.Millisecond)
result := Retrying(policy, action, check)
outcome := result(outer)(inner)()
elapsed := time.Since(startTime)
assert.Equal(t, E.Right[error]("success"), outcome)
assert.Equal(t, 3, attempts)
// Should have at least 30ms delay (10ms + 20ms)
assert.True(t, elapsed >= 30*time.Millisecond, "Expected at least 30ms delay, got %v", elapsed)
}
func TestRetryingNoRetryOnSuccess(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 3}
inner := RetryInnerCtx{endpoint: "http://example.com"}
attempts := 0
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
attempts++
return E.Right[error]("immediate success")
}
}
}
}
// Only retry on errors
check := func(result E.Either[error, string]) bool {
return E.IsLeft(result)
}
policy := retry.LimitRetries(5)
result := Retrying(policy, action, check)
outcome := result(outer)(inner)()
assert.Equal(t, E.Right[error]("immediate success"), outcome)
assert.Equal(t, 1, attempts) // Should only run once
}
func TestRetryingCustomCheckPredicate(t *testing.T) {
outer := RetryOuterCtx{maxRetries: 3}
inner := RetryInnerCtx{endpoint: "http://example.com"}
attempts := 0
retryableErr := errors.New("retryable")
fatalErr := errors.New("fatal")
action := func(status retry.RetryStatus) ReaderReaderIOEither[RetryOuterCtx, RetryInnerCtx, error, string] {
return func(o RetryOuterCtx) ReaderIOEither[RetryInnerCtx, error, string] {
return func(i RetryInnerCtx) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
attempts++
if attempts == 1 {
return E.Left[string](retryableErr)
}
if attempts == 2 {
return E.Left[string](fatalErr)
}
return E.Right[error]("success")
}
}
}
}
// Only retry on retryable errors
check := func(result E.Either[error, string]) bool {
return E.Fold(
func(err error) bool {
return err.Error() == "retryable"
},
func(string) bool {
return false
},
)(result)
}
policy := retry.LimitRetries(5)
result := Retrying(policy, action, check)
outcome := result(outer)(inner)()
// Should stop on fatal error without further retries
assert.Equal(t, E.Left[string](fatalErr), outcome)
assert.Equal(t, 2, attempts)
}

View File

@@ -0,0 +1,36 @@
package readerreaderioeither
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
"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/readerioeither"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
Option[A any] = option.Option[A]
Lazy[A any] = lazy.Lazy[A]
Reader[R, A any] = reader.Reader[R, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
ReaderIOEither[R, E, A any] = readerioeither.ReaderIOEither[R, E, A]
Either[E, A any] = either.Either[E, A]
IOEither[E, A any] = ioeither.IOEither[E, A]
IO[A any] = io.IO[A]
ReaderReaderIOEither[R, C, E, A any] = Reader[R, ReaderIOEither[C, E, A]]
Kleisli[R, C, E, A, B any] = Reader[A, ReaderReaderIOEither[R, C, E, B]]
Operator[R, C, E, A, B any] = Kleisli[R, C, E, ReaderReaderIOEither[R, C, E, A], B]
Lens[S, T any] = lens.Lens[S, T]
Trampoline[L, B any] = tailrec.Trampoline[L, B]
Predicate[A any] = predicate.Predicate[A]
)