1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-07 23:03:15 +02:00

Compare commits

...

15 Commits

Author SHA1 Message Date
Dr. Carsten Leue
d8ab6b0ce5 fix: ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-22 10:39:56 +01:00
Dr. Carsten Leue
4e9998b645 fix: benchmarks and better docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 15:39:41 +01:00
Dr. Carsten Leue
2ea9e292e1 fix: idiomatic/readeresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 15:25:59 +01:00
Dr. Carsten Leue
12a20e30d1 fix: implement BindReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 13:01:27 +01:00
Dr. Carsten Leue
4909ad5473 fix: add missing monoid
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 10:22:50 +01:00
Dr. Carsten Leue
d116317cde fix: add readerresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 10:04:28 +01:00
Dr. Carsten Leue
1428241f2c fix: race condition
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 08:36:07 +01:00
Dr. Carsten Leue
ef9216bad7 fix: documentation, tests, some utilities
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-20 08:43:15 +01:00
Dr. Carsten Leue
fe77c770b6 fix: cleanup types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-19 17:36:49 +01:00
Dr. Carsten Leue
1c42b2ac1d fix: implement idiomatic/ioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-19 15:39:02 +01:00
Dr. Carsten Leue
cbd93fdecc fix: add statereaderioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 17:54:04 +01:00
Dr. Carsten Leue
6d94697128 fix: document statereaderioeither
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 16:06:56 +01:00
Dr. Carsten Leue
77dde302ef Merge branch 'main' of github.com:IBM/fp-go 2025-11-18 10:59:57 +01:00
Dr. Carsten Leue
909d626019 fix: serveral performance improvements
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 10:58:24 +01:00
renovate[bot]
b01a8f2aff chore(deps): update actions/checkout action to v4.3.1 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 06:31:59 +00:00
191 changed files with 25638 additions and 463 deletions

View File

@@ -28,7 +28,7 @@ jobs:
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
@@ -66,7 +66,7 @@ jobs:
matrix:
go-version: ['1.24.x', '1.25.x']
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
@@ -126,7 +126,7 @@ jobs:
steps:
# full checkout for semantic-release
- name: Full checkout
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0

View File

@@ -1,12 +1,15 @@
{
"permissions": {
"allow": [
"Bash(ls -la \"c:\\d\\fp-go\\v2\\internal\\monad\"\" && ls -la \"c:dfp-gov2internalapplicative\"\")",
"Bash(ls -la \"c:\\d\\fp-go\\v2\\internal\\chain\"\" && ls -la \"c:dfp-gov2internalfunctor\"\")",
"Bash(go build:*)",
"Bash(go test:*)",
"Bash(go doc:*)",
"Bash(go tool cover:*)",
"Bash(sort:*)",
"Bash(timeout 30 go test:*)",
"Bash(cut:*)",
"Bash(go build:*)"
"Bash(tee:*)",
"Bash(find:*)"
],
"deny": [],
"ask": []

482
v2/BENCHMARK_COMPARISON.md Normal file
View File

@@ -0,0 +1,482 @@
# Benchmark Comparison: Idiomatic vs Standard Either/Result
**Date:** 2025-11-18
**System:** AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics (16 cores)
**Go Version:** go1.23+
This document provides a detailed performance comparison between the optimized `either` package and the `idiomatic/result` package after recent optimizations to the either package.
## Executive Summary
After optimizations to the `either` package, the performance characteristics have changed significantly:
### Key Findings
1. **Constructors & Predicates**: Both packages now perform comparably (~1-2 ns/op) with **zero heap allocations**
2. **Zero-allocation insight**: The `Either` struct (24 bytes) does NOT escape to heap - Go returns it by value on the stack
3. **Core Operations**: Idiomatic package has a **consistent advantage** of 1.2x - 2.3x for most operations
4. **Complex Operations**: Idiomatic package shows **massive advantages**:
- ChainFirst (Right): **32.4x faster** (87.6 ns → 2.7 ns, 72 B → 0 B)
- Pipeline operations: **2-3x faster** with lower allocations
5. **All simple operations**: Both maintain **zero heap allocations** (0 B/op, 0 allocs/op)
### Winner by Category
| Category | Winner | Reason |
|----------|--------|--------|
| Constructors | **TIE** | Both ~1.3-1.8 ns/op |
| Predicates | **TIE** | Both ~1.2-1.5 ns/op |
| Simple Transformations | **Idiomatic** | 1.2-2x faster |
| Monadic Operations | **Idiomatic** | 1.2-2.3x faster |
| Complex Chains | **Idiomatic** | 32x faster, zero allocs |
| Pipelines | **Idiomatic** | 2-2.4x faster, fewer allocs |
| Extraction | **Idiomatic** | 6x faster (GetOrElse) |
## Detailed Benchmark Results
### Constructor Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| Left | 1.76 | **1.35** | **1.3x** ✓ | 0 B/op | 0 B/op |
| Right | 1.38 | 1.43 | 1.0x | 0 B/op | 0 B/op |
| Of | 1.68 | **1.22** | **1.4x** ✓ | 0 B/op | 0 B/op |
**Analysis:** Both packages perform extremely well with **zero heap allocations**. Idiomatic has a slight edge on Left and Of.
**Important Clarification: Neither Package Escapes to Heap**
A common misconception is that struct-based Either escapes to heap while tuples stay on stack. The benchmarks prove this is FALSE:
```go
// Either package - NO heap allocation
type Either[E, A any] struct {
r A // 8 bytes
l E // 8 bytes
isLeft bool // 1 byte + 7 padding
} // Total: 24 bytes
func Of[E, A any](value A) Either[E, A] {
return Right[E](value) // Returns 24-byte struct BY VALUE
}
// Benchmark result: 0 B/op, 0 allocs/op ✓
```
**Why Either doesn't escape:**
1. **Small struct** - At 24 bytes, it's below Go's escape threshold (~64 bytes)
2. **Return by value** - Go returns small structs on the stack
3. **Inlining** - The `//go:inline` directive eliminates function overhead
4. **No pointers** - No pointer escapes in normal usage
**Idiomatic package:**
```go
// Returns native tuple - always stack allocated
func Right[A any](a A) (A, error) {
return a, nil // 16 bytes total (8 + 8)
}
// Benchmark result: 0 B/op, 0 allocs/op ✓
```
**Both achieve zero allocations** - the performance difference comes from other factors like function composition overhead, not from constructor allocations.
### Predicate Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| IsLeft | 1.45 | **1.35** | **1.1x** ✓ | 0 B/op | 0 B/op |
| IsRight | 1.47 | 1.51 | 1.0x | 0 B/op | 0 B/op |
**Analysis:** Virtually identical performance. The optimizations brought them to parity.
### Fold Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| MonadFold (Right) | 2.71 | - | - | 0 B/op | - |
| MonadFold (Left) | 2.26 | - | - | 0 B/op | - |
| Fold (Right) | 4.03 | **2.75** | **1.5x** ✓ | 0 B/op | 0 B/op |
| Fold (Left) | 3.69 | **2.40** | **1.5x** ✓ | 0 B/op | 0 B/op |
**Analysis:** Idiomatic package is 1.5x faster for curried Fold operations.
### Unwrap Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|-----------|----------------|-------------------|------|
| Unwrap (Right) | 1.27 | N/A | Either-specific |
| Unwrap (Left) | 1.24 | N/A | Either-specific |
| UnwrapError (Right) | 1.27 | N/A | Either-specific |
| UnwrapError (Left) | 1.27 | N/A | Either-specific |
| ToError (Right) | N/A | 1.40 | Idiomatic-specific |
| ToError (Left) | N/A | 1.84 | Idiomatic-specific |
**Analysis:** Both provide fast unwrapping. Idiomatic's tuple return is naturally unwrapped.
### Map Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| MonadMap (Right) | 2.96 | - | - | 0 B/op | - |
| MonadMap (Left) | 1.99 | - | - | 0 B/op | - |
| Map (Right) | 5.13 | **4.34** | **1.2x** ✓ | 0 B/op | 0 B/op |
| Map (Left) | 4.19 | **2.48** | **1.7x** ✓ | 0 B/op | 0 B/op |
| MapLeft (Right) | 3.93 | **2.22** | **1.8x** ✓ | 0 B/op | 0 B/op |
| MapLeft (Left) | 7.22 | **3.51** | **2.1x** ✓ | 0 B/op | 0 B/op |
**Analysis:** Idiomatic is consistently faster across all Map variants, especially for error path (Left).
### BiMap Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| BiMap (Right) | 16.79 | **3.82** | **4.4x** ✓ | 0 B/op | 0 B/op |
| BiMap (Left) | 11.47 | **3.47** | **3.3x** ✓ | 0 B/op | 0 B/op |
**Analysis:** Idiomatic package shows significant advantage for BiMap operations (3-4x faster).
### Chain (Monadic Bind) Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| MonadChain (Right) | 2.89 | - | - | 0 B/op | - |
| MonadChain (Left) | 2.03 | - | - | 0 B/op | - |
| Chain (Right) | 5.44 | **2.34** | **2.3x** ✓ | 0 B/op | 0 B/op |
| Chain (Left) | 4.44 | **2.53** | **1.8x** ✓ | 0 B/op | 0 B/op |
| ChainFirst (Right) | 87.62 | **2.71** | **32.4x** ✓✓✓ | 72 B, 3 allocs | 0 B, 0 allocs |
| ChainFirst (Left) | 3.94 | **2.48** | **1.6x** ✓ | 0 B/op | 0 B/op |
**Analysis:**
- Idiomatic is 2x faster for standard Chain operations
- **ChainFirst shows the most dramatic difference**: 32.4x faster with zero allocations vs 72 bytes!
### Flatten Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|-----------|----------------|-------------------|------|
| Flatten (Right) | 8.73 | N/A | Either-specific nested structure |
| Flatten (Left) | 8.86 | N/A | Either-specific nested structure |
**Analysis:** Flatten is specific to Either's nested structure handling.
### Applicative Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| MonadAp (RR) | 3.81 | - | - | 0 B/op | - |
| MonadAp (RL) | 3.07 | - | - | 0 B/op | - |
| MonadAp (LR) | 3.08 | - | - | 0 B/op | - |
| Ap (RR) | 6.99 | - | - | 0 B/op | - |
**Analysis:** MonadAp is fast in Either. Idiomatic package doesn't expose direct Ap benchmarks.
### Alternative Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| Alt (RR) | 5.72 | **2.40** | **2.4x** ✓ | 0 B/op | 0 B/op |
| Alt (LR) | 4.89 | **2.39** | **2.0x** ✓ | 0 B/op | 0 B/op |
| OrElse (Right) | 5.28 | **2.40** | **2.2x** ✓ | 0 B/op | 0 B/op |
| OrElse (Left) | 3.99 | **2.42** | **1.6x** ✓ | 0 B/op | 0 B/op |
**Analysis:** Idiomatic package is consistently 2x faster for alternative operations.
### GetOrElse Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| GetOrElse (Right) | 9.01 | **1.49** | **6.1x** ✓✓ | 0 B/op | 0 B/op |
| GetOrElse (Left) | 6.35 | **2.08** | **3.1x** ✓✓ | 0 B/op | 0 B/op |
**Analysis:** Idiomatic package shows dramatic advantage for value extraction (3-6x faster).
### TryCatch Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|-----------|----------------|-------------------|------|
| TryCatch (Success) | 2.39 | N/A | Either-specific |
| TryCatch (Error) | 3.40 | N/A | Either-specific |
| TryCatchError (Success) | 3.32 | N/A | Either-specific |
| TryCatchError (Error) | 6.44 | N/A | Either-specific |
**Analysis:** TryCatch/TryCatchError are Either-specific for wrapping (value, error) tuples.
### Other Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| Swap (Right) | 2.30 | - | - | 0 B/op | - |
| Swap (Left) | 3.05 | - | - | 0 B/op | - |
| MapTo (Right) | - | 1.60 | - | - | 0 B/op |
| MapTo (Left) | - | 1.73 | - | - | 0 B/op |
| ChainTo (Right) | - | 2.66 | - | - | 0 B/op |
| ChainTo (Left) | - | 2.85 | - | - | 0 B/op |
| Reduce (Right) | - | 2.34 | - | - | 0 B/op |
| Reduce (Left) | - | 1.40 | - | - | 0 B/op |
| Flap (Right) | - | 3.86 | - | - | 0 B/op |
| Flap (Left) | - | 2.58 | - | - | 0 B/op |
### FromPredicate Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| FromPredicate (Pass) | - | 3.38 | - | - | 0 B/op |
| FromPredicate (Fail) | - | 5.03 | - | - | 0 B/op |
**Analysis:** FromPredicate in idiomatic shows good performance for validation patterns.
### Option Conversion
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| ToOption (Right) | - | 1.17 | - | - | 0 B/op |
| ToOption (Left) | - | 1.21 | - | - | 0 B/op |
| FromOption (Some) | - | 2.68 | - | - | 0 B/op |
| FromOption (None) | - | 3.72 | - | - | 0 B/op |
**Analysis:** Very fast conversion between Result and Option in idiomatic package.
## Pipeline Benchmarks
These benchmarks measure realistic composition scenarios using F.Pipe.
### Simple Map Pipeline
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| Pipeline Map (Right) | 112.7 | **46.5** | **2.4x** ✓ | 72 B, 3 allocs | 48 B, 2 allocs |
| Pipeline Map (Left) | 116.8 | **47.2** | **2.5x** ✓ | 72 B, 3 allocs | 48 B, 2 allocs |
### Chain Pipeline
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| Pipeline Chain (Right) | 74.4 | **26.1** | **2.9x** ✓ | 48 B, 2 allocs | 24 B, 1 allocs |
| Pipeline Chain (Left) | 86.4 | **25.7** | **3.4x** ✓ | 48 B, 2 allocs | 24 B, 1 allocs |
### Complex Pipeline (Map → Chain → Map)
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| Complex (Right) | 279.8 | **116.3** | **2.4x** ✓ | 192 B, 8 allocs | 120 B, 5 allocs |
| Complex (Left) | 288.1 | **115.8** | **2.5x** ✓ | 192 B, 8 allocs | 120 B, 5 allocs |
**Analysis:**
- Idiomatic package shows **2-3.4x speedup** for realistic pipelines
- Significantly fewer allocations in all pipeline scenarios
- The gap widens as pipelines become more complex
## Array/Collection Operations
### TraverseArray
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|-----------|----------------|-------------------|------|
| TraverseArray (Success) | - | 32.3 | 48 B, 1 alloc |
| TraverseArray (Error) | - | 28.3 | 48 B, 1 alloc |
**Analysis:** Idiomatic package provides efficient array traversal with minimal allocations.
## Validation (ApV)
### ApV Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| ApV (BothRight) | - | 1.17 | - | - | 0 B/op |
| ApV (BothLeft) | - | 141.5 | - | - | 48 B, 2 allocs |
**Analysis:** Idiomatic's validation applicative shows fast success path, with allocations only when accumulating errors.
## String Formatting
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| String/ToString (Right) | 139.9 | **81.8** | **1.7x** ✓ | 16 B, 1 alloc | 16 B, 1 alloc |
| String/ToString (Left) | 161.6 | **72.7** | **2.2x** ✓ | 48 B, 1 alloc | 24 B, 1 alloc |
**Analysis:** Idiomatic package formats strings faster with fewer allocations for Left values.
## Do-Notation
| Operation | Either (ns/op) | Idiomatic (ns/op) | Note |
|-----------|----------------|-------------------|------|
| Do | 2.03 | - | Either-specific |
| Bind | 153.4 | - | 96 B, 4 allocs |
| Let | 33.5 | - | 16 B, 1 alloc |
**Analysis:** Do-notation is specific to Either package for monadic composition patterns.
## Summary Statistics
### Simple Operations (< 10 ns/op)
**Either Package:**
- Count: 24 operations
- Average: 3.2 ns/op
- Range: 1.24 - 9.01 ns/op
**Idiomatic Package:**
- Count: 36 operations
- Average: 2.1 ns/op
- Range: 1.17 - 5.03 ns/op
**Winner:** Idiomatic (1.5x faster average)
### Complex Operations (Pipelines, allocations)
**Either Package:**
- Pipeline Map: 112.7 ns/op (72 B, 3 allocs)
- Pipeline Chain: 74.4 ns/op (48 B, 2 allocs)
- Complex: 279.8 ns/op (192 B, 8 allocs)
- ChainFirst: 87.6 ns/op (72 B, 3 allocs)
**Idiomatic Package:**
- Pipeline Map: 46.5 ns/op (48 B, 2 allocs)
- Pipeline Chain: 26.1 ns/op (24 B, 1 allocs)
- Complex: 116.3 ns/op (120 B, 5 allocs)
- ChainFirst: 2.71 ns/op (0 B, 0 allocs)
**Winner:** Idiomatic (2-32x faster, significantly fewer allocations)
### Allocation Analysis
**Either Package:**
- Zero-allocation operations: Most simple operations
- Operations with allocations: Pipelines, Bind, Do-notation, ChainFirst
**Idiomatic Package:**
- Zero-allocation operations: Almost all operations except pipelines and validation
- Significantly fewer allocations in pipeline scenarios
- ChainFirst: **Zero allocations** (vs 72 B in Either)
## Performance Characteristics
### Where Either Package Excels
1. **Comparable to Idiomatic**: After optimizations, Either matches Idiomatic for constructors and predicates
2. **Feature Richness**: More operations (Do-notation, Bind, Let, Flatten, Swap)
3. **Type Flexibility**: Full Either[E, A] with custom error types
### Where Idiomatic Package Excels
1. **Core Operations**: 1.2-2.3x faster for Map, Chain, Fold
2. **Complex Operations**: 32x faster for ChainFirst
3. **Pipelines**: 2-3.4x faster with fewer allocations
4. **Extraction**: 3-6x faster for GetOrElse
5. **Alternative**: 2x faster for Alt/OrElse
6. **BiMap**: 3-4x faster
7. **Consistency**: More predictable performance profile
## Real-World Performance Impact
### Hot Path Example (1 million operations)
```go
// Map operation (very common)
// Either: 5.13 ns/op × 1M = 5.13 ms
// Idiomatic: 4.34 ns/op × 1M = 4.34 ms
// Savings: 0.79 ms per million operations
// Chain operation (common in pipelines)
// Either: 5.44 ns/op × 1M = 5.44 ms
// Idiomatic: 2.34 ns/op × 1M = 2.34 ms
// Savings: 3.10 ms per million operations
// Pipeline Complex (realistic composition)
// Either: 279.8 ns/op × 1M = 279.8 ms
// Idiomatic: 116.3 ns/op × 1M = 116.3 ms
// Savings: 163.5 ms per million operations
```
### Memory Impact
For 1 million ChainFirst operations:
- Either: 72 MB allocated
- Idiomatic: 0 MB allocated
- **Savings: 72 MB + reduced GC pressure**
## Recommendations
### Use Idiomatic Package When:
1. **Performance is Critical**
- Hot paths in your application
- High-throughput services (>10k req/s)
- Complex operation chains
- Memory-constrained environments
2. **Natural Go Integration**
- Working with stdlib (value, error) patterns
- Team familiar with Go idioms
- Simple migration from existing code
- Want zero-cost abstractions
3. **Pipeline-Heavy Code**
- 2-3.4x faster pipelines
- Significantly fewer allocations
- Better CPU cache utilization
### Use Either Package When:
1. **Feature Requirements**
- Need custom error types (Either[E, A])
- Using Do-notation for complex compositions
- Need Flatten, Swap, or other Either-specific operations
- Porting from FP languages (Scala, Haskell)
2. **Type Safety Over Performance**
- Explicit Either semantics
- Algebraic data type guarantees
- Teaching/learning FP concepts
3. **Moderate Performance Needs**
- After optimizations, Either is quite fast
- Difference matters only at high scale
- Code clarity > micro-optimizations
### Hybrid Approach
```go
// Use Either for complex type safety
import "github.com/IBM/fp-go/v2/either"
type ValidationError struct { Field, Message string }
validated := either.Either[ValidationError, Input]{...}
// Convert to Idiomatic for hot path
import "github.com/IBM/fp-go/v2/idiomatic/result"
value, err := either.UnwrapError(either.MapLeft(toError)(validated))
processed, err := result.Chain(hotPathProcessing)(value, err)
```
## Conclusion
After optimizations to the Either package:
1. **Both packages achieve zero heap allocations for constructors** - The Either struct (24 bytes) does NOT escape to heap
2. **Simple operations** are now **comparable** between both packages (~1-2 ns/op, 0 B/op)
3. **Core transformations** favor Idiomatic by **1.2-2.3x**
4. **Complex operations** heavily favor Idiomatic by **2-32x**
5. **Memory efficiency** strongly favors Idiomatic (especially ChainFirst: 72 B → 0 B)
6. **Real-world pipelines** show **2-3.4x speedup** with Idiomatic
### Key Insight: No Heap Escape Myth
A critical finding: **Both packages avoid heap allocations for simple operations.** The Either struct is small enough (24 bytes) that Go returns it by value on the stack, not the heap. The `0 B/op, 0 allocs/op` benchmarks confirm this.
The performance differences come from:
- **Function composition overhead** in complex operations
- **Currying and closure creation** in pipelines
- **Tuple simplicity** vs struct field access
Not from constructor allocations—both are equally efficient there.
### Final Verdict
The idiomatic package provides a compelling performance advantage for production workloads while maintaining zero-cost functional programming abstractions. The Either package remains excellent for type safety, feature richness, and scenarios where explicit Either[E, A] semantics are valuable.
**Bottom Line:**
- For **high-performance Go services**: idiomatic package is the clear winner (1.2-32x faster)
- For **type-safe, feature-rich FP**: Either package is excellent (comparable simple ops, more features)
- **Both avoid heap allocations** for constructors—choose based on your performance vs features trade-off

816
v2/IDIOMATIC_COMPARISON.md Normal file
View File

@@ -0,0 +1,816 @@
# Idiomatic vs Standard Package Comparison
> **Latest Update:** 2025-11-18 - Updated with fresh benchmarks after `either` package optimizations
This document provides a comprehensive comparison between the `idiomatic` packages and the standard fp-go packages (`result` and `option`).
**See also:** [BENCHMARK_COMPARISON.md](./BENCHMARK_COMPARISON.md) for detailed performance analysis.
## Table of Contents
1. [Overview](#overview)
2. [Design Differences](#design-differences)
3. [Performance Comparison](#performance-comparison)
4. [API Comparison](#api-comparison)
5. [When to Use Each](#when-to-use-each)
## Overview
The fp-go library provides two approaches to functional programming patterns in Go:
- **Standard Packages** (`result`, `either`, `option`): Use struct wrappers for algebraic data types
- **Idiomatic Packages** (`idiomatic/result`, `idiomatic/option`): Use native Go tuples for the same patterns
### Key Insight
After recent optimizations to the `either` package, both approaches now offer excellent performance:
- **Simple operations** (~1-5 ns/op): Both packages perform comparably
- **Core transformations**: Idiomatic is **1.2-2.3x faster**
- **Complex operations**: Idiomatic is **2-32x faster** with significantly fewer allocations
- **Real-world pipelines**: Idiomatic shows **2-3.4x speedup**
The idiomatic packages provide:
- Consistently better performance across most operations
- Zero allocations for complex operations (ChainFirst: 72 B → 0 B)
- More familiar Go idioms
- Seamless integration with existing Go code
## Design Differences
### Data Representation
#### Standard Result Package
```go
// Uses Either[error, A] which is a struct wrapper
type Result[A any] = Either[error, A]
type Either[E, A any] struct {
r A
l E
isLeft bool
}
// Creating values - ZERO heap allocations (struct returned by value)
success := result.Right[error](42) // Returns Either struct by value (0 B/op)
failure := result.Left[int](err) // Returns Either struct by value (0 B/op)
// Benchmarks confirm:
// BenchmarkRight-16 871258489 1.384 ns/op 0 B/op 0 allocs/op
// BenchmarkLeft-16 683089270 1.761 ns/op 0 B/op 0 allocs/op
```
#### Idiomatic Result Package
```go
// Uses native Go tuples (value, error)
type Kleisli[A, B any] = func(A) (B, error)
type Operator[A, B any] = func(A, error) (B, error)
// Creating values - ZERO allocations (tuples on stack)
success := result.Right(42) // Returns (42, nil) - 0 B/op
failure := result.Left[int](err) // Returns (0, err) - 0 B/op
// Benchmarks confirm:
// BenchmarkRight-16 789879016 1.427 ns/op 0 B/op 0 allocs/op
// BenchmarkLeft-16 895412131 1.349 ns/op 0 B/op 0 allocs/op
```
### Type Signatures
#### Standard Result
```go
// Functions take and return Result[T] structs
func Map[A, B any](f func(A) B) func(Result[A]) Result[B]
func Chain[A, B any](f Kleisli[A, B]) func(Result[A]) Result[B]
func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(Result[A]) B
// Usage requires wrapping/unwrapping
result := result.Right[error](42)
mapped := result.Map(double)(result)
value, err := result.UnwrapError(mapped)
```
#### Idiomatic Result
```go
// Functions work directly with tuples
func Map[A, B any](f func(A) B) func(A, error) (B, error)
func Chain[A, B any](f Kleisli[A, B]) func(A, error) (B, error)
func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(A, error) B
// Usage works naturally with Go's error handling
value, err := result.Right(42)
value, err = result.Map(double)(value, err)
// Can use directly: if err != nil { ... }
```
### Memory Layout
#### Standard Result (struct-based)
```
Either[error, int] struct (returned by value):
┌─────────────────────┐
│ r: int (8B) │ Stack allocation: 24 bytes
│ l: error (8B) │ NO heap allocation when returned by value
│ isLeft: bool (1B) │ Benchmarks show 0 B/op, 0 allocs/op
│ padding (7B) │
└─────────────────────┘
Key insight: Go returns small structs (<= ~64 bytes) by value on the stack.
The Either struct (24 bytes) does NOT escape to heap in normal usage.
```
#### Idiomatic Result (tuple-based)
```
(int, error) tuple:
┌─────────────────────┐
│ int: 8 bytes │ Stack allocation: 16 bytes
│ error: 8 bytes │ NO heap allocation
└─────────────────────┘
Both approaches achieve zero heap allocations for constructor operations!
```
### Why Both Have Zero Allocations
Both packages avoid heap allocations for simple operations:
**Standard Either/Result:**
- `Either` struct is small (24 bytes)
- Go returns by value on the stack
- Inlining eliminates function call overhead
- Result: `0 B/op, 0 allocs/op`
**Idiomatic Result:**
- Tuples are native Go multi-value returns
- Always on stack, never heap
- Even simpler than structs
- Result: `0 B/op, 0 allocs/op`
**When Either WOULD escape to heap:**
```go
// Taking address of local Either
func bad1() *Either[error, int] {
e := Right[error](42)
return &e // ESCAPES: pointer to local
}
// Storing in interface
func bad2() interface{} {
return Right[error](42) // ESCAPES: interface boxing
}
// Closure capture with pointer receiver
func bad3() func() Either[error, int] {
e := Right[error](42)
return func() Either[error, int] {
return e // May escape depending on usage
}
}
```
In normal functional composition (Map, Chain, Fold), neither package causes heap allocations for simple operations.
## Performance Comparison
> **Latest benchmarks:** 2025-11-18 after `either` package optimizations
>
> For detailed analysis, see [BENCHMARK_COMPARISON.md](./BENCHMARK_COMPARISON.md)
### Quick Summary (Either vs Idiomatic)
Both packages now show **excellent performance** after optimizations:
| Category | Either | Idiomatic | Winner | Speedup |
|----------|--------|-----------|--------|---------|
| **Constructors** | 1.4-1.8 ns/op | 1.2-1.4 ns/op | **TIE** | ~1.0-1.3x |
| **Predicates** | 1.5 ns/op | 1.3-1.5 ns/op | **TIE** | ~1.0x |
| **Map Operations** | 4.2-7.2 ns/op | 2.5-4.3 ns/op | **Idiomatic** | 1.2-2.1x |
| **Chain Operations** | 4.4-5.4 ns/op | 2.3-2.5 ns/op | **Idiomatic** | 1.8-2.3x |
| **ChainFirst** | **87.6 ns/op** (72 B) | **2.7 ns/op** (0 B) | **Idiomatic** | **32.4x** ✓✓✓ |
| **BiMap** | 11.5-16.8 ns/op | 3.5-3.8 ns/op | **Idiomatic** | 3.3-4.4x |
| **Alt/OrElse** | 4.0-5.7 ns/op | 2.4 ns/op | **Idiomatic** | 1.6-2.4x |
| **GetOrElse** | 6.3-9.0 ns/op | 1.5-2.1 ns/op | **Idiomatic** | 3.1-6.1x |
| **Pipelines** | 75-280 ns/op | 26-116 ns/op | **Idiomatic** | 2.4-3.4x |
### Constructor Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Winner |
|-----------|----------------|-------------------|---------|--------|
| Left | 1.76 | **1.35** | 1.3x | Idiomatic ✓ |
| Right | 1.38 | 1.43 | ~1.0x | Tie |
| Of | 1.68 | **1.22** | 1.4x | Idiomatic ✓ |
**Analysis:** After optimizations, both packages have comparable constructor performance.
### Core Transformation Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Winner |
|------------------|----------------|-------------------|---------|--------|
| Map (Right) | 5.13 | **4.34** | 1.2x | Idiomatic ✓ |
| Map (Left) | 4.19 | **2.48** | 1.7x | Idiomatic ✓ |
| MapLeft (Right) | 3.93 | **2.22** | 1.8x | Idiomatic ✓ |
| MapLeft (Left) | 7.22 | **3.51** | 2.1x | Idiomatic ✓ |
| Chain (Right) | 5.44 | **2.34** | 2.3x | Idiomatic ✓ |
| Chain (Left) | 4.44 | **2.53** | 1.8x | Idiomatic ✓ |
### Complex Operations - The Big Difference
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------------------|----------------|-------------------|---------|---------------|-------------|
| **ChainFirst (Right)** | **87.62** | **2.71** | **32.4x** ✓✓✓ | 72 B, 3 allocs | **0 B, 0 allocs** |
| ChainFirst (Left) | 3.94 | 2.48 | 1.6x | 0 B | 0 B |
| BiMap (Right) | 16.79 | **3.82** | 4.4x | 0 B | 0 B |
| BiMap (Left) | 11.47 | **3.47** | 3.3x | 0 B | 0 B |
**Critical Insight:** ChainFirst shows the most dramatic difference - **32x faster** with **zero allocations** in idiomatic.
### Pipeline Benchmarks (Real-World Scenarios)
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Either Allocs | Idio Allocs |
|-----------|----------------|-------------------|---------|---------------|-------------|
| Pipeline Map (Right) | 112.7 | **46.5** | **2.4x** ✓ | 72 B, 3 allocs | 48 B, 2 allocs |
| Pipeline Chain (Right) | 74.4 | **26.1** | **2.9x** ✓ | 48 B, 2 allocs | 24 B, 1 alloc |
| Pipeline Complex (Right)| 279.8 | **116.3** | **2.4x** ✓ | 192 B, 8 allocs | 120 B, 5 allocs |
**Analysis:** In realistic composition scenarios, idiomatic is consistently 2-3x faster with fewer allocations.
### Extraction Operations
| Operation | Either (ns/op) | Idiomatic (ns/op) | Speedup | Winner |
|-----------|----------------|-------------------|---------|--------|
| GetOrElse (Right) | 9.01 | **1.49** | **6.1x** ✓✓ | Idiomatic |
| GetOrElse (Left) | 6.35 | **2.08** | **3.1x** ✓✓ | Idiomatic |
| Alt (Right) | 5.72 | **2.40** | **2.4x** ✓ | Idiomatic |
| Alt (Left) | 4.89 | **2.39** | **2.0x** ✓ | Idiomatic |
| Fold (Right) | 4.03 | **2.75** | **1.5x** ✓ | Idiomatic |
| Fold (Left) | 3.69 | **2.40** | **1.5x** ✓ | Idiomatic |
**Analysis:** Idiomatic shows significant advantages (1.5-6x) for value extraction operations.
### Key Findings After Optimizations
1. **Both packages are now fast** - Simple operations are in the 1-5 ns/op range for both
2. **Idiomatic leads in most operations** - 1.2-2.3x faster for common transformations
3. **ChainFirst is the standout** - 32x faster with zero allocations in idiomatic
4. **Pipelines favor idiomatic** - 2-3.4x faster in realistic composition scenarios
5. **Memory efficiency** - Idiomatic consistently uses fewer allocations
### Performance Summary
**Idiomatic Advantages:**
- **Core operations**: 1.2-2.3x faster for Map, Chain, Fold
- **Complex operations**: 3-32x faster with zero allocations
- **Pipelines**: 2-3.4x faster with significantly fewer allocations
- **Extraction**: 1.5-6x faster for GetOrElse, Alt, Fold
- **Consistency**: Predictable, fast performance across all operations
**Either Advantages:**
- **Comparable performance**: After optimizations, matches idiomatic for simple operations
- **Feature richness**: More operations (Do-notation, Bind, Let, Flatten, Swap)
- **Type flexibility**: Full Either[E, A] with custom error types
- **Zero allocations**: Most simple operations have zero allocations
## API Comparison
### Creating Values
#### Standard Result
```go
import "github.com/IBM/fp-go/v2/result"
// Create success/failure
success := result.Right[error](42)
failure := result.Left[int](errors.New("oops"))
// Type annotation required
var r result.Result[int] = result.Right[error](42)
```
#### Idiomatic Result
```go
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Create success/failure (more concise)
success := result.Right(42) // (42, nil)
failure := result.Left[int](errors.New("oops")) // (0, error)
// Native Go pattern
value, err := result.Right(42)
if err != nil {
// handle error
}
```
### Transforming Values
#### Standard Result
```go
// Map transforms the success value
double := result.Map(func(x int) int { return x * 2 })
result := double(result.Right[error](21)) // Right(42)
// Chain sequences operations
validate := result.Chain(func(x int) result.Result[int] {
if x > 0 {
return result.Right[error](x * 2)
}
return result.Left[int](errors.New("negative"))
})
```
#### Idiomatic Result
```go
// Map transforms the success value
double := result.Map(func(x int) int { return x * 2 })
value, err := double(21, nil) // (42, nil)
// Chain sequences operations
validate := result.Chain(func(x int) (int, error) {
if x > 0 {
return x * 2, nil
}
return 0, errors.New("negative")
})
```
### Pattern Matching
#### Standard Result
```go
// Fold extracts the value
output := result.Fold(
func(err error) string { return "Error: " + err.Error() },
func(n int) string { return fmt.Sprintf("Value: %d", n) },
)(myResult)
// GetOrElse with default
value := result.GetOrElse(func(err error) int { return 0 })(myResult)
```
#### Idiomatic Result
```go
// Fold extracts the value (same API, different input)
output := result.Fold(
func(err error) string { return "Error: " + err.Error() },
func(n int) string { return fmt.Sprintf("Value: %d", n) },
)(value, err)
// GetOrElse with default
value := result.GetOrElse(func(err error) int { return 0 })(value, err)
// Or use native Go pattern
if err != nil {
value = 0
}
```
### Integration with Existing Code
#### Standard Result
```go
// Converting from (value, error) to Result
func doSomething() (int, error) {
return 42, nil
}
result := result.TryCatchError(doSomething())
// Converting back to (value, error)
value, err := result.UnwrapError(result)
```
#### Idiomatic Result
```go
// Direct compatibility with (value, error)
func doSomething() (int, error) {
return 42, nil
}
// No conversion needed!
value, err := doSomething()
value, err = result.Map(double)(value, err)
```
### Pipeline Composition
#### Standard Result
```go
import F "github.com/IBM/fp-go/v2/function"
output := F.Pipe3(
result.Right[error](10),
result.Map(double),
result.Chain(validate),
result.Map(format),
)
// Need to unwrap at the end
value, err := result.UnwrapError(output)
```
#### Idiomatic Result
```go
import F "github.com/IBM/fp-go/v2/function"
value, err := F.Pipe3(
result.Right(10),
result.Map(double),
result.Chain(validate),
result.Map(format),
)
// Already in (value, error) form
if err != nil {
// handle error
}
```
## Detailed Design Comparison
### Type System
#### Standard Result
**Strengths:**
- Full algebraic data type semantics
- Explicit Either[E, A] allows custom error types
- Type-safe by construction
- Clear separation of error and success channels
**Weaknesses:**
- Requires wrapper structs (memory overhead)
- Less familiar to Go developers
- Needs conversion functions for Go's standard library
- More verbose type annotations
#### Idiomatic Result
**Strengths:**
- Native Go idioms (value, error) pattern
- Zero wrapper overhead
- Seamless stdlib integration
- Familiar to all Go developers
- Terser syntax
**Weaknesses:**
- Error type fixed to `error`
- Less explicit about Either semantics
- Cannot use custom error types without conversion
- Slightly less type-safe (can accidentally ignore bool/error)
### Monad Laws
Both packages satisfy the monad laws, but enforce them differently:
#### Standard Result
```go
// Left identity: return a >>= f ≡ f a
assert.Equal(
result.Chain(f)(result.Of(a)),
f(a),
)
// Right identity: m >>= return ≡ m
assert.Equal(
result.Chain(result.Of[int])(m),
m,
)
// Associativity: (m >>= f) >>= g ≡ m >>= (\x -> f x >>= g)
assert.Equal(
result.Chain(g)(result.Chain(f)(m)),
result.Chain(func(x int) result.Result[int] {
return result.Chain(g)(f(x))
})(m),
)
```
#### Idiomatic Result
```go
// Same laws, different syntax
// Left identity
a, aerr := result.Of(val)
b, berr := result.Chain(f)(a, aerr)
c, cerr := f(val)
assert.Equal((b, berr), (c, cerr))
// Right identity
value, err := m()
identity := result.Chain(result.Of[int])
assert.Equal(identity(value, err), (value, err))
// Associativity (same structure, tuple-based)
```
### Error Handling Philosophy
#### Standard Result
```go
// Explicit error handling through types
func processUser(id int) result.Result[User] {
user := fetchUser(id) // Returns Result[User]
return F.Pipe2(
user,
result.Chain(validateUser),
result.Chain(enrichUser),
)
}
// Must explicitly unwrap
user, err := result.UnwrapError(processUser(42))
if err != nil {
log.Error(err)
}
```
#### Idiomatic Result
```go
// Natural Go error handling
func processUser(id int) (User, error) {
user, err := fetchUser(id) // Returns (User, error)
return F.Pipe2(
(user, err),
result.Chain(validateUser),
result.Chain(enrichUser),
)
}
// Already in Go form
user, err := processUser(42)
if err != nil {
log.Error(err)
}
```
### Composition Patterns
#### Standard Result
```go
// Applicative composition
import A "github.com/IBM/fp-go/v2/apply"
type Config struct {
Host string
Port int
DB string
}
config := A.SequenceT3(
result.FromPredicate(validHost, hostError)(host),
result.FromPredicate(validPort, portError)(port),
result.FromPredicate(validDB, dbError)(db),
)(func(h string, p int, d string) Config {
return Config{h, p, d}
})
```
#### Idiomatic Result
```go
// Direct tuple composition
config, err := func() (Config, error) {
host, err := result.FromPredicate(validHost, hostError)(host)
if err != nil {
return Config{}, err
}
port, err := result.FromPredicate(validPort, portError)(port)
if err != nil {
return Config{}, err
}
db, err := result.FromPredicate(validDB, dbError)(db)
if err != nil {
return Config{}, err
}
return Config{host, port, db}, nil
}()
```
## When to Use Each
### Use Idiomatic Result When (Recommended for Most Cases):
1. **Performance Matters**
- Any production service (web servers, APIs, microservices)
- Hot paths and high-throughput scenarios (>1000 req/s)
- Complex operation chains (**32x faster** ChainFirst)
- Real-world pipelines (**2-3x faster**)
- Memory-constrained environments (zero allocations)
- Want **1.2-6x speedup** across most operations
2. **Go Integration** ⭐⭐
- Working with existing Go codebases
- Interfacing with standard library (native (value, error))
- Team familiar with Go, new to FP
- Want zero-cost functional abstractions
- Seamless error handling patterns
3. **Pragmatic Functional Programming**
- Value performance AND functional patterns
- Prefer Go idioms over FP terminology
- Simpler function signatures
- Lower cognitive overhead
- Production-ready patterns
4. **Real-World Applications**
- Web servers, REST APIs, gRPC services
- CLI tools and command-line applications
- Data processing pipelines
- Any latency-sensitive application
- Systems with tight performance budgets
**Performance Gains:** Use idiomatic for 1.2-32x speedup depending on operation, with consistently lower allocations.
### Use Standard Either/Result When:
1. **Type Safety & Flexibility**
- Need explicit Either[E, A] with **custom error types**
- Building domain-specific error hierarchies
- Want to distinguish different error categories at type level
- Type system enforcement is critical
2. **Advanced FP Features**
- Using Do-notation for complex monadic compositions
- Need operations like Flatten, Swap, Bind, Let
- Leveraging advanced type classes (Semigroup, Monoid)
- Want the complete FP toolkit
3. **FP Expertise & Education**
- Porting code from other FP languages (Scala, Haskell)
- Teaching functional programming concepts
- Team has strong FP background
- Explicit algebraic data types preferred
- Code review benefits from FP terminology
4. **Performance is Acceptable**
- After optimizations, Either is **quite fast** (1-5 ns/op for simple operations)
- Difference matters mainly at high scale (millions of operations)
- Code clarity > micro-optimizations
- Simple operations dominate your workload
**Note:** Either package is now performant enough for most use cases. Choose it for features, not performance concerns.
### Hybrid Approach
You can use both packages together:
```go
import (
stdResult "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
// Use standard for complex types
type ValidationError struct {
Field string
Error string
}
func validateInput(input string) stdResult.Either[ValidationError, Input] {
// ... validation logic
}
// Convert to idiomatic for performance
func processInput(input string) (Output, error) {
validated := validateInput(input)
value, err := stdResult.UnwrapError(
stdResult.MapLeft(toError)(validated),
)
// Use idiomatic for hot path
return result.Chain(heavyProcessing)(value, err)
}
```
## Migration Guide
### From Standard to Idiomatic
```go
// Before (standard)
import "github.com/IBM/fp-go/v2/result"
func process(x int) result.Result[int] {
return F.Pipe2(
result.Right[error](x),
result.Map(double),
result.Chain(validate),
)
}
// After (idiomatic)
import "github.com/IBM/fp-go/v2/idiomatic/result"
func process(x int) (int, error) {
return F.Pipe2(
result.Right(x),
result.Map(double),
result.Chain(validate),
)
}
```
### Key Changes
1. **Type signatures**: `Result[T]``(T, error)`
2. **Kleisli**: `func(A) Result[B]``func(A) (B, error)`
3. **Operator**: `func(Result[A]) Result[B]``func(A, error) (B, error)`
4. **Return values**: Function calls return tuples, not wrapped values
5. **Pattern matching**: Same Fold/GetOrElse API, different inputs
## Conclusion
### Performance Summary (After Either Optimizations)
The latest benchmark results show a clear pattern:
**Both packages are now fast**, but idiomatic consistently leads:
- **Constructors & Predicates**: Both ~1-2 ns/op (essentially tied)
- **Core transformations**: Idiomatic **1.2-2.3x faster** (Map, Chain, Fold)
- **Complex operations**: Idiomatic **3-32x faster** (BiMap, ChainFirst)
- **Pipelines**: Idiomatic **2-3.4x faster** with fewer allocations
- **Extraction**: Idiomatic **1.5-6x faster** (GetOrElse, Alt)
**Key Insight:** The idiomatic package delivers **consistently better performance** across the board while maintaining zero-cost abstractions. The Either package is now fast enough for most use cases, but idiomatic is the performance winner.
### Updated Recommendation Matrix
| Scenario | Recommendation | Reason |
|----------|---------------|--------|
| **New Go project** | **Idiomatic** ⭐ | Natural Go patterns, 1.2-6x faster, better integration |
| **Production services** | **Idiomatic** ⭐⭐ | 2-3x faster pipelines, zero allocations, proven performance |
| **Performance critical** | **Idiomatic** ⭐⭐⭐ | 32x faster complex ops, minimal allocations |
| **Microservices/APIs** | **Idiomatic** ⭐⭐ | High throughput, familiar patterns, better performance |
| **CLI Tools** | **Idiomatic** ⭐ | Low overhead, Go idioms, fast |
| Custom error types | Standard/Either | Need Either[E, A] with domain types |
| Learning FP | Standard/Either | Clearer ADT semantics, educational |
| FP-heavy codebase | Standard/Either | Consistency, Do-notation, full FP toolkit |
| Library/Framework | Either way | Both are good; choose based on API style |
### Real-World Impact
For a service handling 10,000 requests/second with typical pipeline operations:
```
Either package: 280 ns/op × 10M req/day = 2,800 seconds = 46.7 minutes
Idiomatic package: 116 ns/op × 10M req/day = 1,160 seconds = 19.3 minutes
Time saved: 27.4 minutes of CPU time per day
```
At scale, this translates to:
- Lower latency (2-3x faster response times for FP operations)
- Reduced CPU usage (fewer cores needed)
- Lower memory pressure (significantly fewer allocations)
- Better resource utilization
### Final Recommendation
**For most Go projects:** Use **idiomatic packages**
- 1.2-32x faster across operations
- Native Go idioms
- Zero-cost abstractions
- Production-proven performance
- Easier integration
**For specialized needs:** Use **standard Either/Result**
- Need custom error types Either[E, A]
- Want Do-notation and advanced FP features
- Porting from FP languages
- Educational/learning context
- FP-heavy existing codebase
### Bottom Line
After optimizations, both packages are excellent:
- **Either/Result**: Fast enough for most use cases, feature-rich, type-safe
- **Idiomatic**: **Faster in practice** (1.2-32x), native Go, zero-cost FP
The idiomatic packages now represent the **best of both worlds**: full functional programming capabilities with Go's native performance and idioms. Unless you specifically need Either[E, A]'s custom error types or advanced FP features, **idiomatic is the recommended choice** for production Go services.
Both maintain the core benefits of functional programming—choose based on whether you prioritize performance & Go integration (idiomatic) or type flexibility & FP features (either).

View File

@@ -658,7 +658,7 @@ func Defer[A any](gen Lazy[ReaderIOResult[A]]) ReaderIOResult[A] {
//
//go:inline
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOResult[A] {
return RIOR.TryCatch(f, errors.IdentityError)
return RIOR.TryCatch(f, errors.Identity)
}
// MonadAlt provides an alternative [ReaderIOResult] if the first one fails.

View File

@@ -0,0 +1,209 @@
// Copyright (c) 2024 - 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 statereaderioresult
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Do starts a do-notation chain for building computations in a fluent style.
// This is typically used with Bind, Let, and other combinators to compose
// stateful, context-dependent computations that can fail.
//
// Example:
//
// type State struct {
// name string
// age int
// }
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(...),
// statereaderioresult.Let(...),
// )
//
//go:inline
func Do[ST, A any](
empty A,
) StateReaderIOResult[ST, A] {
return Of[ST](empty)
}
// Bind executes a computation and binds its result to a field in the accumulator state.
// This is used in do-notation to sequence dependent computations.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(
// func(name string) func(State) State {
// return func(s State) State { return State{name: name, age: s.age} }
// },
// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState]("John")
// },
// ),
// )
//
//go:inline
func Bind[ST, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[ST, S1, T],
) Operator[ST, S1, S2] {
return C.Bind(
Chain[ST, S1, S2],
Map[ST, T, S2],
setter,
f,
)
}
// Let computes a derived value and binds it to a field in the accumulator state.
// Unlike Bind, this does not execute a monadic computation, just a pure function.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{age: 25}),
// statereaderioresult.Let(
// func(isAdult bool) func(State) State {
// return func(s State) State { return State{age: s.age, isAdult: isAdult} }
// },
// func(s State) bool { return s.age >= 18 },
// ),
// )
//
//go:inline
func Let[ST, S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
) Operator[ST, S1, S2] {
return F.Let(
Map[ST, S1, S2],
key,
f,
)
}
// LetTo binds a constant value to a field in the accumulator state.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.LetTo(
// func(status string) func(State) State {
// return func(s State) State { return State{...s, status: status} }
// },
// "active",
// ),
// )
//
//go:inline
func LetTo[ST, S1, S2, T any](
key func(T) func(S1) S2,
b T,
) Operator[ST, S1, S2] {
return F.LetTo(
Map[ST, S1, S2],
key,
b,
)
}
// BindTo wraps a value in a simple constructor, typically used to start a do-notation chain
// after getting an initial value.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Of[AppState](42),
// statereaderioresult.BindTo[AppState](func(x int) State { return State{value: x} }),
// )
//
//go:inline
func BindTo[ST, S1, T any](
setter func(T) S1,
) Operator[ST, T, S1] {
return C.BindTo(
Map[ST, T, S1],
setter,
)
}
// ApS applies a computation in sequence and binds the result to a field.
// This is the applicative version of Bind.
//
//go:inline
func ApS[ST, S1, S2, T any](
setter func(T) func(S1) S2,
fa StateReaderIOResult[ST, T],
) Operator[ST, S1, S2] {
return A.ApS(
Ap[S2, ST, T],
Map[ST, S1, func(T) S2],
setter,
fa,
)
}
// ApSL is a lens-based variant of ApS for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func ApSL[ST, S, T any](
lens Lens[S, T],
fa StateReaderIOResult[ST, T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return ApS(lens.Set, fa)
}
// BindL is a lens-based variant of Bind for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func BindL[ST, S, T any](
lens Lens[S, T],
f Kleisli[ST, T, T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL is a lens-based variant of Let for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func LetL[ST, S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return Let[ST](lens.Set, function.Flow2(lens.Get, f))
}
// LetToL is a lens-based variant of LetTo for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func LetToL[ST, S, T any](
lens Lens[S, T],
b T,
) Endomorphism[StateReaderIOResult[ST, S]] {
return LetTo[ST](lens.Set, b)
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) 2024 - 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 statereaderioresult provides a functional programming abstraction that combines
// four powerful concepts: State, Reader, IO, and Result monads, specialized for Go's context.Context.
//
// # StateReaderIOResult
//
// StateReaderIOResult[S, A] represents a computation that:
// - Manages state of type S (State)
// - Depends on a [context.Context] (Reader)
// - Performs side effects (IO)
// - Can fail with an [error] or succeed with a value of type A (Result)
//
// This is a specialization of StateReaderIOEither with:
// - Context type fixed to [context.Context]
// - Error type fixed to [error]
//
// This is particularly useful for:
// - Stateful computations with dependency injection using Go contexts
// - Error handling in effectful computations with state
// - Composing operations that need access to context, manage state, and can fail
// - Working with Go's standard context patterns (cancellation, deadlines, values)
//
// # Core Operations
//
// Construction:
// - Of/Right: Create a successful computation with a value
// - Left: Create a failed computation with an error
// - FromState: Lift a State into StateReaderIOResult
// - FromIO: Lift an IO into StateReaderIOResult
// - FromResult: Lift a Result into StateReaderIOResult
// - FromIOResult: Lift an IOResult into StateReaderIOResult
// - FromReaderIOResult: Lift a ReaderIOResult into StateReaderIOResult
//
// Transformation:
// - Map: Transform the success value
// - Chain: Sequence dependent computations (monadic bind)
// - Flatten: Flatten nested StateReaderIOResult
//
// Combination:
// - Ap: Apply a function in a context to a value in a context
//
// Context Access:
// - Asks: Get a value derived from the context
// - Local: Run a computation with a modified context
//
// Kleisli Arrows:
// - FromResultK: Lift a Result-returning function to a Kleisli arrow
// - FromIOK: Lift an IO-returning function to a Kleisli arrow
// - FromIOResultK: Lift an IOResult-returning function to a Kleisli arrow
// - FromReaderIOResultK: Lift a ReaderIOResult-returning function to a Kleisli arrow
// - ChainResultK: Chain with a Result-returning function
// - ChainIOResultK: Chain with an IOResult-returning function
// - ChainReaderIOResultK: Chain with a ReaderIOResult-returning function
//
// Do Notation (Monadic Composition):
// - Do: Start a do-notation chain
// - Bind: Bind a value from a computation
// - BindTo: Bind a value to a simple constructor
// - Let: Compute a derived value
// - LetTo: Set a constant value
// - ApS: Apply in sequence (for applicative composition)
// - BindL/ApSL/LetL/LetToL: Lens-based variants for working with nested structures
//
// # Example Usage
//
// type AppState struct {
// RequestCount int
// LastError error
// }
//
// // A computation that manages state, depends on context, performs IO, and can fail
// func processRequest(data string) statereaderioresult.StateReaderIOResult[AppState, string] {
// return func(state AppState) readerioresult.ReaderIOResult[pair.Pair[AppState, string]] {
// return func(ctx context.Context) ioresult.IOResult[pair.Pair[AppState, string]] {
// return func() result.Result[pair.Pair[AppState, string]] {
// // Check context for cancellation
// if ctx.Err() != nil {
// return result.Error[pair.Pair[AppState, string]](ctx.Err())
// }
// // Update state
// newState := AppState{RequestCount: state.RequestCount + 1}
// // Perform IO operations
// return result.Of(pair.MakePair(newState, "processed: " + data))
// }
// }
// }
// }
//
// // Compose operations using do-notation
// result := function.Pipe3(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(
// func(result string) func(State) State { return func(s State) State { return State{result: result} } },
// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] {
// return processRequest(s.input)
// },
// ),
// statereaderioresult.Map[AppState](func(s State) string { return s.result }),
// )
//
// // Execute with initial state and context
// initialState := AppState{RequestCount: 0}
// ctx := context.Background()
// outcome := result(initialState)(ctx)() // Returns result.Result[pair.Pair[AppState, string]]
//
// # Context Integration
//
// This package is designed to work seamlessly with Go's context.Context:
//
// // Using context values
// getUserID := statereaderioresult.Asks[AppState, string](func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] {
// userID, ok := ctx.Value("userID").(string)
// if !ok {
// return statereaderioresult.Left[AppState, string](errors.New("missing userID"))
// }
// return statereaderioresult.Of[AppState](userID)
// })
//
// // Using context cancellation
// withTimeout := statereaderioresult.Local[AppState, string](func(ctx context.Context) context.Context {
// ctx, _ = context.WithTimeout(ctx, 5*time.Second)
// return ctx
// })
//
// # Monad Laws
//
// StateReaderIOResult satisfies the monad laws:
// - Left Identity: Of(a) >>= f ≡ f(a)
// - Right Identity: m >>= Of ≡ m
// - Associativity: (m >>= f) >>= g ≡ m >>= (x => f(x) >>= g)
//
// These laws are verified in the testing subpackage.
package statereaderioresult

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 - 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 statereaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// Eq implements the equals predicate for values contained in the [StateReaderIOResult] monad
func Eq[S, A any](eqr eq.Eq[ReaderIOResult[Pair[S, A]]]) func(S) eq.Eq[StateReaderIOResult[S, A]] {
return func(s S) eq.Eq[StateReaderIOResult[S, A]] {
return eq.FromEquals(func(l, r StateReaderIOResult[S, A]) bool {
return eqr.Equals(l(s), r(s))
})
}
}
// FromStrictEquals constructs an [eq.Eq] from the canonical comparison function
func FromStrictEquals[S comparable, A comparable]() func(context.Context) func(S) eq.Eq[StateReaderIOResult[S, A]] {
return function.Flow2(
RIOR.FromStrictEquals[context.Context, Pair[S, A]](),
Eq[S, A],
)
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2024 - 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 statereaderioresult
import (
"github.com/IBM/fp-go/v2/internal/applicative"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/monad"
"github.com/IBM/fp-go/v2/internal/pointed"
)
type stateReaderIOResultPointed[
S, A any,
] struct{}
type stateReaderIOResultFunctor[
S, A, B any,
] struct{}
type stateReaderIOResultApplicative[
S, A, B any,
] struct{}
type stateReaderIOResultMonad[
S, A, B any,
] struct{}
func (o *stateReaderIOResultPointed[S, A]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultMonad[S, A, B]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultMonad[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultFunctor[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultMonad[S, A, B]) Chain(f Kleisli[S, A, B]) Operator[S, A, B] {
return Chain(f)
}
func (o *stateReaderIOResultMonad[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return Ap[B](fa)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return Ap[B](fa)
}
// Pointed implements the [pointed.Pointed] operations for [StateReaderIOResult]
func Pointed[
S, A any,
]() pointed.Pointed[A, StateReaderIOResult[S, A]] {
return &stateReaderIOResultPointed[S, A]{}
}
// Functor implements the [functor.Functor] operations for [StateReaderIOResult]
func Functor[
S, A, B any,
]() functor.Functor[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B]] {
return &stateReaderIOResultFunctor[S, A, B]{}
}
// Applicative implements the [applicative.Applicative] operations for [StateReaderIOResult]
func Applicative[
S, A, B any,
]() applicative.Applicative[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] {
return &stateReaderIOResultApplicative[S, A, B]{}
}
// Monad implements the [monad.Monad] operations for [StateReaderIOResult]
func Monad[
S, A, B any,
]() monad.Monad[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] {
return &stateReaderIOResultMonad[S, A, B]{}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) 2024 - 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 statereaderioresult
import "github.com/IBM/fp-go/v2/statereaderioeither"
// WithResource constructs a function that creates a resource with state management, operates on it,
// and then releases the resource. This ensures proper resource cleanup even in the presence of errors,
// following the Resource Acquisition Is Initialization (RAII) pattern.
//
// The state is threaded through all operations: resource creation, usage, and release.
//
// The resource lifecycle with state management is:
// 1. onCreate: Acquires the resource (may modify state)
// 2. use: Operates on the resource with current state (provided as argument to the returned function)
// 3. onRelease: Releases the resource with current state (called regardless of success or failure)
//
// Type parameters:
// - A: The type of the result produced by using the resource
// - S: The state type that is threaded through all operations
// - RES: The resource type
// - ANY: The type returned by the release function (typically ignored)
//
// Parameters:
// - onCreate: A stateful computation that acquires the resource
// - onRelease: A stateful function that releases the resource, called with the resource and current state,
// executed regardless of errors
//
// Returns:
//
// A function that takes a resource-using function and returns a StateReaderIOResult that manages
// the resource lifecycle with state
//
// Example:
//
// type AppState struct {
// openFiles int
// }
//
// // Resource creation that updates state
// openFile := func(filename string) StateReaderIOResult[AppState, *File] {
// return func(state AppState) ReaderIOResult[Pair[AppState, *File]] {
// return func(ctx context.Context) IOResult[Pair[AppState, *File]] {
// return func() Result[Pair[AppState, *File]] {
// file, err := os.Open(filename)
// if err != nil {
// return result.Error[Pair[AppState, *File]](err)
// }
// newState := AppState{openFiles: state.openFiles + 1}
// return result.Of(pair.MakePair(newState, file))
// }
// }
// }
// }
//
// // Resource release that updates state
// closeFile := func(f *File) StateReaderIOResult[AppState, int] {
// return func(state AppState) ReaderIOResult[Pair[AppState, int]] {
// return func(ctx context.Context) IOResult[Pair[AppState, int]] {
// return func() Result[Pair[AppState, int]] {
// f.Close()
// newState := AppState{openFiles: state.openFiles - 1}
// return result.Of(pair.MakePair(newState, 0))
// }
// }
// }
// }
//
// // Use the resource with automatic cleanup
// withFile := WithResource(
// openFile("data.txt"),
// closeFile,
// )
//
// result := withFile(func(f *File) StateReaderIOResult[AppState, string] {
// return readContent(f) // File will be closed automatically
// })
//
// // Execute the computation
// initialState := AppState{openFiles: 0}
// ctx := context.Background()
// outcome := result(initialState)(ctx)()
func WithResource[A, S, RES, ANY any](
onCreate StateReaderIOResult[S, RES],
onRelease Kleisli[S, RES, ANY],
) Kleisli[S, Kleisli[S, RES, A], A] {
return statereaderioeither.WithResource[A](onCreate, onRelease)
}

View File

@@ -0,0 +1,415 @@
// Copyright (c) 2024 - 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 statereaderioresult
import (
"context"
"errors"
"testing"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// resourceState tracks the lifecycle of resources for testing
type resourceState struct {
resourcesCreated int
resourcesReleased int
lastError error
}
// mockResource represents a test resource
type mockResource struct {
id int
isValid bool
}
// TestWithResourceSuccess tests successful resource creation, usage, and release
func TestWithResourceSuccess(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
// Create a resource
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
// Release a resource
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Use the resource
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, string]] {
return func(ctx context.Context) IOResult[Pair[resourceState, string]] {
return func() Result[Pair[resourceState, string]] {
result := "used resource " + string(rune(res.id+'0'))
return RES.Of(P.MakePair(s, result))
}
}
}
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, string]) Pair[resourceState, string] {
state := P.Head(p)
value := P.Tail(p)
// Verify state updates
// Note: Final state comes from the use function, not the release function
// onCreate: 0->1, use: sees 1 (doesn't modify), release: sees 1 and increments released
// The final state is from use function which saw state=1 with resourcesReleased=0
assert.Equal(t, 1, state.resourcesCreated, "Resource should be created once")
assert.Equal(t, 0, state.resourcesReleased, "Final state is from use function, before release")
// Verify result
assert.Equal(t, "used resource 1", value)
return p
})(outcome)
}
// TestWithResourceErrorInCreate tests error handling when resource creation fails
func TestWithResourceErrorInCreate(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
createError := errors.New("failed to create resource")
// onCreate that fails
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
return RES.Left[Pair[resourceState, mockResource]](createError)
}
}
}
// Release should not be called if onCreate fails
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
t.Error("onRelease should not be called when onCreate fails")
return RES.Of(P.MakePair(s, 0))
}
}
}
}
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Of[resourceState]("should not reach here")
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
RES.Fold(
func(err error) bool {
assert.Equal(t, createError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}
// TestWithResourceErrorInUse tests that resources are released even when usage fails
func TestWithResourceErrorInUse(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
useError := errors.New("failed to use resource")
// Create a resource
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: 1, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
releaseWasCalled := false
// Release should still be called even if use fails
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
releaseWasCalled = true
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Use that fails
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Left[resourceState, string](useError)
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
assert.True(t, releaseWasCalled, "onRelease should be called even when use fails")
RES.Fold(
func(err error) bool {
assert.Equal(t, useError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}
// TestWithResourceStateThreading tests that state is properly threaded through all operations
func TestWithResourceStateThreading(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
// Create increments counter
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
// Use observes the state after creation
useResource := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
// Verify state was updated by onCreate
assert.Equal(t, 1, s.resourcesCreated)
assert.Equal(t, 0, s.resourcesReleased)
return RES.Of(P.MakePair(s, s.resourcesCreated))
}
}
}
}
// Release increments released counter
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
// Verify state was updated by onCreate and use
assert.Equal(t, 1, s.resourcesCreated)
assert.Equal(t, 0, s.resourcesReleased)
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
withResource := WithResource[int](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] {
finalState := P.Head(p)
value := P.Tail(p)
// Verify final state
// Note: Final state is from the use function, which preserves the state it received
// onCreate: 0->1, use: sees 1, release: sees 1 and increments released to 1
// But final state is from use function where resourcesReleased=0
assert.Equal(t, 1, finalState.resourcesCreated)
assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before release")
assert.Equal(t, 1, value)
return p
})(outcome)
}
// TestWithResourceMultipleResources tests using WithResource multiple times (nesting)
func TestWithResourceMultipleResources(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
createResource := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
releaseResource := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Create two nested resources
withResource1 := WithResource[int](createResource, releaseResource)
withResource2 := WithResource[int](createResource, releaseResource)
result := withResource1(func(res1 mockResource) StateReaderIOResult[resourceState, int] {
return withResource2(func(res2 mockResource) StateReaderIOResult[resourceState, int] {
// Both resources should be available
return Of[resourceState](res1.id + res2.id)
})
})
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] {
finalState := P.Head(p)
value := P.Tail(p)
// Both resources created, but final state is from innermost use function
// onCreate1: 0->1, onCreate2: 1->2, use (Of): sees 2
// Release functions execute but their state changes aren't in the final result
assert.Equal(t, 2, finalState.resourcesCreated)
assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before releases")
// res1.id = 1, res2.id = 2, sum = 3
assert.Equal(t, 3, value)
return p
})(outcome)
}
// TestWithResourceContextCancellation tests behavior with context cancellation
func TestWithResourceContextCancellation(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
cancelError := errors.New("context cancelled")
// Create should respect context cancellation
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
if ctx.Err() != nil {
return RES.Left[Pair[resourceState, mockResource]](cancelError)
}
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: 1, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Of[resourceState]("should not reach here")
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
RES.Fold(
func(err error) bool {
assert.Equal(t, cancelError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}

View File

@@ -0,0 +1,309 @@
// Copyright (c) 2024 - 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 statereaderioresult
import (
"context"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/statet"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// Left creates a StateReaderIOResult that represents a failed computation with the given error.
// The error value is immediately available and does not depend on state or context.
//
// Example:
//
// result := statereaderioresult.Left[AppState, string](errors.New("validation failed"))
// // Returns a failed computation that ignores state and context
func Left[S, A any](e error) StateReaderIOResult[S, A] {
return function.Constant1[S](RIORES.Left[Pair[S, A]](e))
}
// Right creates a StateReaderIOResult that represents a successful computation with the given value.
// The value is wrapped and the state is passed through unchanged.
//
// Example:
//
// result := statereaderioresult.Right[AppState](42)
// // Returns a successful computation containing 42
func Right[S, A any](a A) StateReaderIOResult[S, A] {
return statet.Of[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], a)
}
// Of creates a StateReaderIOResult that represents a successful computation with the given value.
// This is the monadic return/pure operation for StateReaderIOResult.
// Equivalent to [Right].
//
// Example:
//
// result := statereaderioresult.Of[AppState](42)
// // Returns a successful computation containing 42
func Of[S, A any](a A) StateReaderIOResult[S, A] {
return Right[S](a)
}
// MonadMap transforms the success value of a StateReaderIOResult using the provided function.
// If the computation fails, the error is propagated unchanged.
// The state is threaded through the computation.
// This is the functor map operation.
//
// Example:
//
// result := statereaderioresult.MonadMap(
// statereaderioresult.Of[AppState](21),
// func(x int) int { return x * 2 },
// ) // Result contains 42
func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReaderIOResult[S, B] {
return statet.MonadMap[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
RIORES.MonadMap[Pair[S, A], Pair[S, B]],
fa,
f,
)
}
// Map is the curried version of [MonadMap].
// Returns a function that transforms a StateReaderIOResult.
//
// Example:
//
// double := statereaderioresult.Map[AppState](func(x int) int { return x * 2 })
// result := function.Pipe1(statereaderioresult.Of[AppState](21), double)
func Map[S, A, B any](f func(A) B) Operator[S, A, B] {
return statet.Map[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
RIORES.Map[Pair[S, A], Pair[S, B]],
f,
)
}
// MonadChain sequences two computations, passing the result of the first to a function
// that produces the second computation. This is the monadic bind operation.
// The state is threaded through both computations.
//
// Example:
//
// result := statereaderioresult.MonadChain(
// statereaderioresult.Of[AppState](5),
// func(x int) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](fmt.Sprintf("value: %d", x))
// },
// )
func MonadChain[S, A, B any](fa StateReaderIOResult[S, A], f Kleisli[S, A, B]) StateReaderIOResult[S, B] {
return statet.MonadChain(
RIORES.MonadChain[Pair[S, A], Pair[S, B]],
fa,
f,
)
}
// Chain is the curried version of [MonadChain].
// Returns a function that sequences computations.
//
// Example:
//
// stringify := statereaderioresult.Chain[AppState](func(x int) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](fmt.Sprintf("%d", x))
// })
// result := function.Pipe1(statereaderioresult.Of[AppState](42), stringify)
func Chain[S, A, B any](f Kleisli[S, A, B]) Operator[S, A, B] {
return statet.Chain[StateReaderIOResult[S, A]](
RIORES.Chain[Pair[S, A], Pair[S, B]],
f,
)
}
// MonadAp applies a function wrapped in a StateReaderIOResult to a value wrapped in a StateReaderIOResult.
// If either the function or the value fails, the error is propagated.
// The state is threaded through both computations sequentially.
// This is the applicative apply operation.
//
// Example:
//
// fab := statereaderioresult.Of[AppState](func(x int) int { return x * 2 })
// fa := statereaderioresult.Of[AppState](21)
// result := statereaderioresult.MonadAp(fab, fa) // Result contains 42
func MonadAp[B, S, A any](fab StateReaderIOResult[S, func(A) B], fa StateReaderIOResult[S, A]) StateReaderIOResult[S, B] {
return statet.MonadAp[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
RIORES.MonadMap[Pair[S, A], Pair[S, B]],
RIORES.MonadChain[Pair[S, func(A) B], Pair[S, B]],
fab,
fa,
)
}
// Ap is the curried version of [MonadAp].
// Returns a function that applies a wrapped function to the given wrapped value.
func Ap[B, S, A any](fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return statet.Ap[StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]](
RIORES.Map[Pair[S, A], Pair[S, B]],
RIORES.Chain[Pair[S, func(A) B], Pair[S, B]],
fa,
)
}
// FromReaderIOResult lifts a ReaderIOResult into a StateReaderIOResult.
// The state is passed through unchanged.
//
// Example:
//
// riores := readerioresult.Of(42)
// result := statereaderioresult.FromReaderIOResult[AppState](riores)
func FromReaderIOResult[S, A any](fa ReaderIOResult[A]) StateReaderIOResult[S, A] {
return statet.FromF[StateReaderIOResult[S, A]](
RIORES.MonadMap[A],
fa,
)
}
// FromIOResult lifts an IOResult into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
func FromIOResult[S, A any](fa IOResult[A]) StateReaderIOResult[S, A] {
return FromReaderIOResult[S](RIORES.FromIOResult(fa))
}
// FromState lifts a State computation into a StateReaderIOResult.
// The computation cannot fail (uses the error type).
func FromState[S, A any](sa State[S, A]) StateReaderIOResult[S, A] {
return statet.FromState[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], sa)
}
// FromIO lifts an IO computation into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
func FromIO[S, A any](fa IO[A]) StateReaderIOResult[S, A] {
return FromReaderIOResult[S](RIORES.FromIO(fa))
}
// FromResult lifts a Result into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
//
// Example:
//
// result := statereaderioresult.FromResult[AppState](result.Of(42))
func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
return result.Fold(Left[S, A], Right[S, A])(ma)
}
// Combinators
// Local runs a computation with a modified context.
// The function f transforms the context before passing it to the computation.
//
// Example:
//
// // Modify context before running computation
// withTimeout := statereaderioresult.Local[AppState](
// func(ctx context.Context) context.Context {
// ctx, _ = context.WithTimeout(ctx, 60*time.Second)
// return ctx
// }
// )
// result := withTimeout(computation)
func Local[S, A any](f func(context.Context) context.Context) func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return func(ma StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return function.Flow2(ma, RIOR.Local[Pair[S, A]](f))
}
}
// Asks creates a computation that derives a value from the context.
// The function receives the context and returns a StateReaderIOResult.
//
// Example:
//
// getValue := statereaderioresult.Asks[AppState, string](
// func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](ctx.Value("key").(string))
// },
// )
func Asks[S, A any](f func(context.Context) StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return func(s S) ReaderIOResult[Pair[S, A]] {
return func(ctx context.Context) IOResult[Pair[S, A]] {
return f(ctx)(s)(ctx)
}
}
}
// FromResultK lifts a Result-returning function into a Kleisli arrow for StateReaderIOResult.
//
// Example:
//
// validate := func(x int) result.Result[int] {
// if x > 0 { return result.Of(x) }
// return result.Error[int](errors.New("negative"))
// }
// kleisli := statereaderioresult.FromResultK[AppState](validate)
func FromResultK[S, A, B any](f func(A) Result[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromResult[S, B],
)
}
// FromIOK lifts an IO-returning function into a Kleisli arrow for StateReaderIOResult.
func FromIOK[S, A, B any](f func(A) IO[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromIO[S, B],
)
}
// FromIOResultK lifts an IOResult-returning function into a Kleisli arrow for StateReaderIOResult.
func FromIOResultK[S, A, B any](f func(A) IOResult[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromIOResult[S, B],
)
}
// FromReaderIOResultK lifts a ReaderIOResult-returning function into a Kleisli arrow for StateReaderIOResult.
func FromReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromReaderIOResult[S, B],
)
}
// MonadChainReaderIOResultK chains a StateReaderIOResult with a ReaderIOResult-returning function.
func MonadChainReaderIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) ReaderIOResult[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromReaderIOResultK[S](f))
}
// ChainReaderIOResultK is the curried version of [MonadChainReaderIOResultK].
func ChainReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Operator[S, A, B] {
return Chain(FromReaderIOResultK[S](f))
}
// MonadChainIOResultK chains a StateReaderIOResult with an IOResult-returning function.
func MonadChainIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) IOResult[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromIOResultK[S](f))
}
// ChainIOResultK is the curried version of [MonadChainIOResultK].
func ChainIOResultK[S, A, B any](f func(A) IOResult[B]) Operator[S, A, B] {
return Chain(FromIOResultK[S](f))
}
// MonadChainResultK chains a StateReaderIOResult with a Result-returning function.
func MonadChainResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) Result[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromResultK[S](f))
}
// ChainResultK is the curried version of [MonadChainResultK].
func ChainResultK[S, A, B any](f func(A) Result[B]) Operator[S, A, B] {
return Chain(FromResultK[S](f))
}

View File

@@ -0,0 +1,567 @@
// Copyright (c) 2024 - 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 statereaderioresult
import (
"context"
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
IOR "github.com/IBM/fp-go/v2/ioresult"
N "github.com/IBM/fp-go/v2/number"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type testState struct {
counter int
}
func TestOf(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := Of[testState](42)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Fold(
func(err error) bool {
t.Fatalf("Expected Success but got Error: %v", err)
return false
},
func(p P.Pair[testState, int]) bool {
assert.Equal(t, 42, P.Tail(p))
assert.Equal(t, 0, P.Head(p).counter) // State unchanged
return true
},
)(res)
}
func TestRight(t *testing.T) {
state := testState{counter: 5}
ctx := context.Background()
result := Right[testState](100)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 100, P.Tail(p))
assert.Equal(t, 5, P.Head(p).counter)
return p
})(res)
}
func TestLeft(t *testing.T) {
state := testState{counter: 10}
ctx := context.Background()
testErr := errors.New("test error")
result := Left[testState, int](testErr)
res := result(state)(ctx)()
assert.True(t, RES.IsLeft(res))
}
func TestMonadMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := MonadMap(
Of[testState](21),
N.Mul(2),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := F.Pipe1(
Of[testState](21),
Map[testState](N.Mul(2)),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestMonadChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := MonadChain(
Of[testState](5),
func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("value: %d", x))
},
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value: 5", P.Tail(p))
return p
})(res)
}
func TestChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := F.Pipe1(
Of[testState](5),
Chain(func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("value: %d", x))
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value: 5", P.Tail(p))
return p
})(res)
}
func TestMonadAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
fab := Of[testState](N.Mul(2))
fa := Of[testState](21)
result := MonadAp(fab, fa)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
fa := Of[testState](21)
result := F.Pipe1(
Of[testState](N.Mul(2)),
Ap[int](fa),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestFromIOResult(t *testing.T) {
state := testState{counter: 3}
ctx := context.Background()
ior := IOR.Of(55)
result := FromIOResult[testState](ior)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 55, P.Tail(p))
assert.Equal(t, 3, P.Head(p).counter)
return p
})(res)
}
func TestFromState(t *testing.T) {
initialState := testState{counter: 10}
ctx := context.Background()
// State computation that increments counter and returns it
stateComp := func(s testState) P.Pair[testState, int] {
newState := testState{counter: s.counter + 1}
return P.MakePair(newState, newState.counter)
}
result := FromState(stateComp)
res := result(initialState)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 11, P.Tail(p)) // Incremented value
assert.Equal(t, 11, P.Head(p).counter) // State updated
return p
})(res)
}
func TestFromIO(t *testing.T) {
state := testState{counter: 8}
ctx := context.Background()
ioVal := func() int { return 99 }
result := FromIO[testState](ioVal)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 99, P.Tail(p))
assert.Equal(t, 8, P.Head(p).counter)
return p
})(res)
}
func TestFromResult(t *testing.T) {
state := testState{counter: 12}
ctx := context.Background()
// Test Success case
resultSuccess := FromResult[testState](RES.Of(42))
resSuccess := resultSuccess(state)(ctx)()
assert.True(t, RES.IsRight(resSuccess))
// Test Error case
resultError := FromResult[testState](RES.Left[int](errors.New("error")))
resError := resultError(state)(ctx)()
assert.True(t, RES.IsLeft(resError))
}
func TestLocal(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "key", "value1")
// Create a computation that uses the context
comp := Asks(func(c context.Context) StateReaderIOResult[testState, string] {
val := c.Value("key").(string)
return Of[testState](val)
})
// Modify context before running computation
result := Local[testState, string](
func(c context.Context) context.Context {
return context.WithValue(c, "key", "value2")
},
)(comp)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value2", P.Tail(p))
return p
})(res)
}
func TestAsks(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "multiplier", 7)
result := Asks(func(c context.Context) StateReaderIOResult[testState, int] {
mult := c.Value("multiplier").(int)
return Of[testState](mult * 5)
})
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 35, P.Tail(p))
return p
})(res)
}
func TestFromResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
validate := func(x int) RES.Result[int] {
if x > 0 {
return RES.Of(x * 2)
}
return RES.Left[int](errors.New("negative"))
}
kleisli := FromResultK[testState](validate)
// Test with valid input
resultValid := kleisli(5)
resValid := resultValid(state)(ctx)()
assert.True(t, RES.IsRight(resValid))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 10, P.Tail(p))
return p
})(resValid)
// Test with invalid input
resultInvalid := kleisli(-5)
resInvalid := resultInvalid(state)(ctx)()
assert.True(t, RES.IsLeft(resInvalid))
}
func TestFromIOK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ioFunc := func(x int) io.IO[int] {
return func() int { return x * 3 }
}
kleisli := FromIOK[testState](ioFunc)
result := kleisli(7)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 21, P.Tail(p))
return p
})(res)
}
func TestFromIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
iorFunc := func(x int) IOR.IOResult[int] {
if x > 0 {
return IOR.Of(x * 4)
}
return IOR.Left[int](errors.New("invalid"))
}
kleisli := FromIOResultK[testState](iorFunc)
result := kleisli(3)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 12, P.Tail(p))
return p
})(res)
}
func TestChainResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
validate := func(x int) RES.Result[string] {
if x > 0 {
return RES.Of(fmt.Sprintf("valid: %d", x))
}
return RES.Left[string](errors.New("invalid"))
}
result := F.Pipe1(
Of[testState](42),
ChainResultK[testState](validate),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "valid: 42", P.Tail(p))
return p
})(res)
}
func TestChainIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
iorFunc := func(x int) IOR.IOResult[string] {
return IOR.Of(fmt.Sprintf("result: %d", x))
}
result := F.Pipe1(
Of[testState](100),
ChainIOResultK[testState](iorFunc),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "result: 100", P.Tail(p))
return p
})(res)
}
func TestDo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
type Result struct {
value int
}
result := Do[testState](Result{})
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] {
assert.Equal(t, 0, P.Tail(p).value)
return p
})(res)
}
func TestBindTo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
type Result struct {
value int
}
result := F.Pipe1(
Of[testState](42),
BindTo[testState](func(v int) Result {
return Result{value: v}
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] {
assert.Equal(t, 42, P.Tail(p).value)
return p
})(res)
}
func TestStatefulComputation(t *testing.T) {
initialState := testState{counter: 0}
ctx := context.Background()
// Create a computation that modifies state
incrementAndGet := func(s testState) P.Pair[testState, int] {
newState := testState{counter: s.counter + 1}
return P.MakePair(newState, newState.counter)
}
// Chain multiple stateful operations
result := F.Pipe2(
FromState(incrementAndGet),
Chain(func(v1 int) StateReaderIOResult[testState, int] {
return FromState(incrementAndGet)
}),
Chain(func(v2 int) StateReaderIOResult[testState, int] {
return FromState(incrementAndGet)
}),
)
res := result(initialState)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
assert.Equal(t, 3, P.Head(p).counter) // State updated three times
return p
})(res)
}
func TestErrorPropagation(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
testErr := errors.New("test error")
// Chain operations where the second one fails
result := F.Pipe1(
Of[testState](42),
Chain(func(x int) StateReaderIOResult[testState, int] {
return Left[testState, int](testErr)
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsLeft(res))
}
func TestPointed(t *testing.T) {
p := Pointed[testState, int]()
assert.NotNil(t, p)
result := p.Of(42)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
}
func TestFunctor(t *testing.T) {
f := Functor[testState, int, string]()
assert.NotNil(t, f)
mapper := f.Map(func(x int) string { return fmt.Sprintf("%d", x) })
result := mapper(Of[testState](42))
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}
func TestApplicative(t *testing.T) {
a := Applicative[testState, int, string]()
assert.NotNil(t, a)
fab := Of[testState](func(x int) string { return fmt.Sprintf("%d", x) })
fa := Of[testState](42)
result := a.Ap(fa)(fab)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}
func TestMonad(t *testing.T) {
m := Monad[testState, int, string]()
assert.NotNil(t, m)
fa := m.Of(42)
result := m.Chain(func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("%d", x))
})(fa)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2024 - 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 testing
import (
"context"
"testing"
EQ "github.com/IBM/fp-go/v2/eq"
L "github.com/IBM/fp-go/v2/internal/monad/testing"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
)
// AssertLaws asserts the monad laws for the StateReaderIOResult monad
func AssertLaws[S, A, B, C any](t *testing.T,
eqs EQ.Eq[S],
eqa EQ.Eq[A],
eqb EQ.Eq[B],
eqc EQ.Eq[C],
ab func(A) B,
bc func(B) C,
s S,
ctx context.Context,
) func(a A) bool {
eqra := RIORES.Eq(RES.Eq(P.Eq(eqs, eqa)))(ctx)
eqrb := RIORES.Eq(RES.Eq(P.Eq(eqs, eqb)))(ctx)
eqrc := RIORES.Eq(RES.Eq(P.Eq(eqs, eqc)))(ctx)
fofc := ST.Pointed[S, C]()
fofaa := ST.Pointed[S, func(A) A]()
fofbc := ST.Pointed[S, func(B) C]()
fofabb := ST.Pointed[S, func(func(A) B) B]()
fmap := ST.Functor[S, func(B) C, func(func(A) B) func(A) C]()
fapabb := ST.Applicative[S, func(A) B, B]()
fapabac := ST.Applicative[S, func(A) B, func(A) C]()
maa := ST.Monad[S, A, A]()
mab := ST.Monad[S, A, B]()
mac := ST.Monad[S, A, C]()
mbc := ST.Monad[S, B, C]()
return L.MonadAssertLaws(t,
ST.Eq(eqra)(s),
ST.Eq(eqrb)(s),
ST.Eq(eqrc)(s),
fofc,
fofaa,
fofbc,
fofabb,
fmap,
fapabb,
fapabac,
maa,
mab,
mac,
mbc,
ab,
bc,
)
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2024 - 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 testing
import (
"context"
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
)
func TestMonadLaws(t *testing.T) {
// some comparison
eqs := A.Eq(EQ.FromStrictEquals[string]())
eqa := EQ.FromStrictEquals[bool]()
eqb := EQ.FromStrictEquals[int]()
eqc := EQ.FromStrictEquals[string]()
ab := func(a bool) int {
if a {
return 1
}
return 0
}
bc := func(b int) string {
return fmt.Sprintf("value %d", b)
}
laws := AssertLaws(t, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), context.Background())
assert.True(t, laws(true))
assert.True(t, laws(false))
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2024 - 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 statereaderioresult
import (
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"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/iso/lens"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/state"
)
type (
// Endomorphism represents a function from A to A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lens is an optic that focuses on a field of type A within a structure of type S.
Lens[S, A any] = lens.Lens[S, A]
// State represents a stateful computation that takes an initial state S and returns
// a pair of the new state S and a value A.
State[S, A any] = state.State[S, A]
// Pair represents a tuple of two values.
Pair[L, R any] = pair.Pair[L, R]
// Reader represents a computation that depends on an environment/context of type R
// and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// Result represents a value that can be either an error or a success value.
// This is specialized to use [error] as the error type.
Result[A any] = result.Result[A]
// IO represents a computation that performs side effects and produces a value of type A.
IO[A any] = io.IO[A]
// IOResult represents a computation that performs side effects and can fail with an error
// or succeed with a value A.
IOResult[A any] = ioresult.IOResult[A]
// ReaderIOResult represents a computation that depends on a context.Context,
// performs side effects, and can fail with an error or succeed with a value A.
ReaderIOResult[A any] = RIORES.ReaderIOResult[A]
// StateReaderIOResult represents a stateful computation that:
// - Takes an initial state S
// - Depends on a [context.Context]
// - Performs side effects (IO)
// - Can fail with an [error] or succeed with a value A
// - Returns a pair of the new state S and the result
//
// This is the main type of this package, combining State, Reader, IO, and Result monads.
// It is a specialization of StateReaderIOEither with:
// - Context type fixed to [context.Context]
// - Error type fixed to [error]
StateReaderIOResult[S, A any] = Reader[S, ReaderIOResult[Pair[S, A]]]
// Kleisli represents a Kleisli arrow - a function that takes a value A and returns
// a StateReaderIOResult computation producing B.
// This is used for monadic composition via Chain.
Kleisli[S, A, B any] = Reader[A, StateReaderIOResult[S, B]]
// Operator represents a function that transforms one StateReaderIOResult into another.
// This is commonly used for building composable operations via Map, Chain, etc.
Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]
)

67
v2/coverage.txt Normal file
View File

@@ -0,0 +1,67 @@
mode: set
github.com/IBM/fp-go/v2/readerresult/array.go:33.74,35.2 1 0
github.com/IBM/fp-go/v2/readerresult/array.go:49.98,51.2 1 0
github.com/IBM/fp-go/v2/readerresult/array.go:68.76,70.2 1 1
github.com/IBM/fp-go/v2/readerresult/bind.go:42.22,44.2 1 1
github.com/IBM/fp-go/v2/readerresult/bind.go:93.49,95.2 1 1
github.com/IBM/fp-go/v2/readerresult/bind.go:103.49,105.2 1 0
github.com/IBM/fp-go/v2/readerresult/bind.go:113.49,115.2 1 0
github.com/IBM/fp-go/v2/readerresult/bind.go:122.22,124.2 1 0
github.com/IBM/fp-go/v2/readerresult/bind.go:172.49,174.2 1 1
github.com/IBM/fp-go/v2/readerresult/bind.go:211.21,213.2 1 0
github.com/IBM/fp-go/v2/readerresult/bind.go:252.21,254.2 1 0
github.com/IBM/fp-go/v2/readerresult/bind.go:288.21,290.2 1 0
github.com/IBM/fp-go/v2/readerresult/bind.go:321.21,323.2 1 0
github.com/IBM/fp-go/v2/readerresult/curry.go:35.64,37.2 1 1
github.com/IBM/fp-go/v2/readerresult/curry.go:46.81,48.2 1 1
github.com/IBM/fp-go/v2/readerresult/curry.go:58.98,60.2 1 1
github.com/IBM/fp-go/v2/readerresult/curry.go:69.115,71.2 1 1
github.com/IBM/fp-go/v2/readerresult/curry.go:81.83,83.2 1 1
github.com/IBM/fp-go/v2/readerresult/curry.go:92.100,94.2 1 1
github.com/IBM/fp-go/v2/readerresult/curry.go:103.117,105.2 1 1
github.com/IBM/fp-go/v2/readerresult/from.go:33.70,35.2 1 1
github.com/IBM/fp-go/v2/readerresult/from.go:45.80,47.2 1 1
github.com/IBM/fp-go/v2/readerresult/from.go:57.92,59.2 1 1
github.com/IBM/fp-go/v2/readerresult/from.go:69.104,71.2 1 1
github.com/IBM/fp-go/v2/readerresult/monoid.go:37.62,45.2 1 1
github.com/IBM/fp-go/v2/readerresult/monoid.go:64.70,69.2 1 1
github.com/IBM/fp-go/v2/readerresult/monoid.go:91.62,98.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:41.59,43.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:49.59,51.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:61.63,63.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:73.66,75.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:85.49,87.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:98.46,100.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:106.62,108.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:120.83,122.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:133.54,135.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:147.92,149.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:160.63,162.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:173.43,175.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:189.101,191.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:197.71,199.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:215.89,217.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:234.131,236.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:249.100,251.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:265.70,267.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:282.81,289.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:303.38,305.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:317.56,319.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:330.103,337.2 1 0
github.com/IBM/fp-go/v2/readerresult/reader.go:348.74,354.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:367.97,369.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:381.84,383.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:395.108,397.2 1 0
github.com/IBM/fp-go/v2/readerresult/reader.go:409.79,411.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:426.88,428.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:440.61,442.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:453.85,455.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:460.55,462.2 1 0
github.com/IBM/fp-go/v2/readerresult/reader.go:473.94,475.2 1 0
github.com/IBM/fp-go/v2/readerresult/reader.go:486.65,488.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:494.103,502.2 1 1
github.com/IBM/fp-go/v2/readerresult/reader.go:508.71,515.2 1 0
github.com/IBM/fp-go/v2/readerresult/sequence.go:35.78,40.2 1 1
github.com/IBM/fp-go/v2/readerresult/sequence.go:54.35,60.2 1 1
github.com/IBM/fp-go/v2/readerresult/sequence.go:75.38,82.2 1 1
github.com/IBM/fp-go/v2/readerresult/sequence.go:95.41,103.2 1 1

190
v2/either/applicative.go Normal file
View File

@@ -0,0 +1,190 @@
// Copyright (c) 2024 - 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 either
import (
"github.com/IBM/fp-go/v2/internal/applicative"
S "github.com/IBM/fp-go/v2/semigroup"
)
// eitherApplicative is the internal implementation of the Applicative type class for Either.
// It provides the basic applicative operations: Of (lift), Map (transform), and Ap (apply).
type eitherApplicative[E, A, B any] struct {
fof func(a A) Either[E, A]
fmap func(func(A) B) Operator[E, A, B]
fap func(Either[E, A]) Operator[E, func(A) B, B]
}
// Of lifts a pure value into a Right context.
func (o *eitherApplicative[E, A, B]) Of(a A) Either[E, A] {
return o.fof(a)
}
// Map applies a transformation function to the Right value, preserving Left values.
func (o *eitherApplicative[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
return o.fmap(f)
}
// Ap applies a wrapped function to a wrapped value.
// The behavior depends on which Ap implementation is used (fail-fast or validation).
func (o *eitherApplicative[E, A, B]) Ap(fa Either[E, A]) Operator[E, func(A) B, B] {
return o.fap(fa)
}
// Applicative creates a standard Applicative instance for Either with fail-fast error handling.
//
// This returns a lawful Applicative that satisfies all applicative laws:
// - Identity: Ap(Of(identity))(v) == v
// - Homomorphism: Ap(Of(f))(Of(x)) == Of(f(x))
// - Interchange: Ap(Of(f))(u) == Ap(Map(f => f(y))(u))(Of(y))
// - Composition: Ap(Ap(Map(compose)(f))(g))(x) == Ap(f)(Ap(g)(x))
//
// The Applicative operations behave as follows:
// - Of: lifts a value into Right
// - Map: transforms Right values, preserves Left (standard functor)
// - Ap: fails fast - if either operand is Left, returns the first Left encountered
//
// This is the standard Either applicative that stops at the first error, making it
// suitable for computations where you want to short-circuit on failure.
//
// Example - Fail-Fast Behavior:
//
// app := either.Applicative[error, int, string]()
//
// // Both succeed - function application works
// value := either.Right[error](42)
// fn := either.Right[error](strconv.Itoa)
// result := app.Ap(value)(fn)
// // result is Right("42")
//
// // First error stops computation
// err1 := either.Left[func(int) string](errors.New("error 1"))
// err2 := either.Left[int](errors.New("error 2"))
// result2 := app.Ap(err2)(err1)
// // result2 is Left(error 1) - only first error is returned
//
// Type Parameters:
// - E: The error type (Left value)
// - A: The input value type (Right value)
// - B: The output value type after transformation
func Applicative[E, A, B any]() applicative.Applicative[A, B, Either[E, A], Either[E, B], Either[E, func(A) B]] {
return &eitherApplicative[E, A, B]{
Of[E, A],
Map[E, A, B],
Ap[B, E, A],
}
}
// ApplicativeV creates an Applicative with validation-style error accumulation.
//
// This returns a lawful Applicative that accumulates errors using a Semigroup when
// combining independent computations with Ap. This is the "validation" pattern commonly
// used for form validation, configuration validation, and parallel error collection.
//
// The returned instance satisfies all applicative laws:
// - Identity: Ap(Of(identity))(v) == v
// - Homomorphism: Ap(Of(f))(Of(x)) == Of(f(x))
// - Interchange: Ap(Of(f))(u) == Ap(Map(f => f(y))(u))(Of(y))
// - Composition: Ap(Ap(Map(compose)(f))(g))(x) == Ap(f)(Ap(g)(x))
//
// Key behaviors:
// - Of: lifts a value into Right
// - Map: transforms Right values, preserves Left (standard functor)
// - Ap: when both operands are Left, combines errors using the Semigroup
//
// Comparison with standard Applicative:
// - Applicative: Ap fails fast (returns first error)
// - ApplicativeV: Ap accumulates errors (combines all errors via Semigroup)
//
// Use cases:
// - Form validation: collect all validation errors at once
// - Configuration validation: report all configuration problems
// - Parallel independent checks: accumulate all failures
//
// Example - Error Accumulation for Form Validation:
//
// type ValidationErrors []string
//
// // Define how to combine error lists
// sg := semigroup.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
// return append(append(ValidationErrors{}, a...), b...)
// })
//
// app := either.ApplicativeV[ValidationErrors, User, User](sg)
//
// // Validate multiple fields independently
// validateName := func(name string) Either[ValidationErrors, string] {
// if len(name) < 3 {
// return Left[string](ValidationErrors{"Name must be at least 3 characters"})
// }
// return Right[ValidationErrors](name)
// }
//
// validateAge := func(age int) Either[ValidationErrors, int] {
// if age < 18 {
// return Left[int](ValidationErrors{"Must be 18 or older"})
// }
// return Right[ValidationErrors](age)
// }
//
// validateEmail := func(email string) Either[ValidationErrors, string] {
// if !strings.Contains(email, "@") {
// return Left[string](ValidationErrors{"Invalid email format"})
// }
// return Right[ValidationErrors](email)
// }
//
// // Create a constructor function lifted into Either
// makeUser := func(name string) func(int) func(string) User {
// return func(age int) func(string) User {
// return func(email string) User {
// return User{Name: name, Age: age, Email: email}
// }
// }
// }
//
// // Apply validations - all errors are collected
// name := validateName("ab") // Left: name too short
// age := validateAge(16) // Left: age too low
// email := validateEmail("invalid") // Left: invalid email
//
// // Combine all validations using ApV
// result := app.Ap(name)(
// app.Ap(age)(
// app.Ap(email)(
// app.Of(makeUser),
// ),
// ),
// )
// // result is Left(ValidationErrors{
// // "Name must be at least 3 characters",
// // "Must be 18 or older",
// // "Invalid email format"
// // })
// // All three errors are collected!
//
// Type Parameters:
// - E: The error type that must have a Semigroup for combining errors
// - A: The input value type (Right value)
// - B: The output value type after transformation
// - sg: Semigroup instance for combining Left values when both operands of Ap are Left
func ApplicativeV[E, A, B any](sg S.Semigroup[E]) applicative.Applicative[A, B, Either[E, A], Either[E, B], Either[E, func(A) B]] {
return &eitherApplicative[E, A, B]{
Of[E, A],
Map[E, A, B],
ApV[B, A](sg),
}
}

View File

@@ -35,14 +35,18 @@ import (
// // result is Right([]int{1, 2, 3})
//
//go:inline
func TraverseArrayG[GA ~[]A, GB ~[]B, E, A, B any](f func(A) Either[E, B]) func(GA) Either[E, GB] {
return RA.Traverse[GA](
Of[E, GB],
Map[E, GB, func(B) GB],
Ap[GB, E, B],
f,
)
func TraverseArrayG[GA ~[]A, GB ~[]B, E, A, B any](f Kleisli[E, A, B]) Kleisli[E, GA, GB] {
return func(ga GA) Either[E, GB] {
bs := make(GB, len(ga))
for i, a := range ga {
b := f(a)
if b.isLeft {
return Left[GB](b.l)
}
bs[i] = b.r
}
return Of[E](bs)
}
}
// TraverseArray transforms an array by applying a function that returns an Either to each element.
@@ -59,7 +63,7 @@ func TraverseArrayG[GA ~[]A, GB ~[]B, E, A, B any](f func(A) Either[E, B]) func(
// // result is Right([]int{1, 2, 3})
//
//go:inline
func TraverseArray[E, A, B any](f func(A) Either[E, B]) func([]A) Either[E, []B] {
func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
return TraverseArrayG[[]A, []B](f)
}
@@ -80,14 +84,18 @@ func TraverseArray[E, A, B any](f func(A) Either[E, B]) func([]A) Either[E, []B]
// // result is Right([]string{"0:a", "1:b"})
//
//go:inline
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, E, A, B any](f func(int, A) Either[E, B]) func(GA) Either[E, GB] {
return RA.TraverseWithIndex[GA](
Of[E, GB],
Map[E, GB, func(B) GB],
Ap[GB, E, B],
f,
)
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, E, A, B any](f func(int, A) Either[E, B]) Kleisli[E, GA, GB] {
return func(ga GA) Either[E, GB] {
bs := make(GB, len(ga))
for i, a := range ga {
b := f(i, a)
if b.isLeft {
return Left[GB](b.l)
}
bs[i] = b.r
}
return Of[E](bs)
}
}
// TraverseArrayWithIndex transforms an array by applying an indexed function that returns an Either.
@@ -106,7 +114,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, E, A, B any](f func(int, A) Eithe
// // result is Right([]string{"0:a", "1:b"})
//
//go:inline
func TraverseArrayWithIndex[E, A, B any](f func(int, A) Either[E, B]) func([]A) Either[E, []B] {
func TraverseArrayWithIndex[E, A, B any](f func(int, A) Either[E, B]) Kleisli[E, []A, []B] {
return TraverseArrayWithIndexG[[]A, []B](f)
}

View File

@@ -22,9 +22,9 @@ import (
type (
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
Either[E, A any] struct {
r A
l E
isL bool
r A
l E
isLeft bool
}
)
@@ -32,7 +32,7 @@ type (
//
//go:noinline
func (s Either[E, A]) String() string {
if !s.isL {
if !s.isLeft {
return fmt.Sprintf("Right[%T](%v)", s.r, s.r)
}
return fmt.Sprintf("Left[%T](%v)", s.l, s.l)
@@ -61,7 +61,7 @@ func (s Either[E, A]) Format(f fmt.State, c rune) {
//
//go:inline
func IsLeft[E, A any](val Either[E, A]) bool {
return val.isL
return val.isLeft
}
// IsRight tests if the Either is a Right value.
@@ -75,7 +75,7 @@ func IsLeft[E, A any](val Either[E, A]) bool {
//
//go:inline
func IsRight[E, A any](val Either[E, A]) bool {
return !val.isL
return !val.isLeft
}
// Left creates a new Either representing a Left (error/failure) value.
@@ -87,7 +87,7 @@ func IsRight[E, A any](val Either[E, A]) bool {
//
//go:inline
func Left[A, E any](value E) Either[E, A] {
return Either[E, A]{l: value, isL: true}
return Either[E, A]{l: value, isLeft: true}
}
// Right creates a new Either representing a Right (success) value.
@@ -115,7 +115,7 @@ func Right[E, A any](value A) Either[E, A] {
//
//go:inline
func MonadFold[E, A, B any](ma Either[E, A], onLeft func(e E) B, onRight func(a A) B) B {
if !ma.isL {
if !ma.isLeft {
return onRight(ma.r)
}
return onLeft(ma.l)

View File

@@ -24,7 +24,6 @@ import (
F "github.com/IBM/fp-go/v2/function"
C "github.com/IBM/fp-go/v2/internal/chain"
FC "github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
)
@@ -38,7 +37,7 @@ import (
//
//go:inline
func Of[E, A any](value A) Either[E, A] {
return F.Pipe1(value, Right[E, A])
return Right[E](value)
}
// FromIO executes an IO operation and wraps the result in a Right value.
@@ -48,8 +47,10 @@ func Of[E, A any](value A) Either[E, A] {
//
// getValue := func() int { return 42 }
// result := either.FromIO[error](getValue) // Right(42)
//
// go: inline
func FromIO[E any, IO ~func() A, A any](f IO) Either[E, A] {
return F.Pipe1(f(), Right[E, A])
return Of[E](f())
}
// MonadAp applies a function wrapped in Either to a value wrapped in Either.
@@ -62,13 +63,19 @@ func FromIO[E any, IO ~func() A, A any](f IO) Either[E, A] {
// fa := either.Right[error](21)
// result := either.MonadAp(fab, fa) // Right(42)
func MonadAp[B, E, A any](fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
return MonadFold(fab, Left[B, E], func(ab func(A) B) Either[E, B] {
return MonadFold(fa, Left[B, E], F.Flow2(ab, Right[E, B]))
})
if fab.isLeft {
return Left[B](fab.l)
}
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](fab.r(fa.r))
}
// Ap is the curried version of [MonadAp].
// Returns a function that applies a wrapped function to the given wrapped value.
//
//go:inline
func Ap[B, E, A any](fa Either[E, A]) Operator[E, func(A) B, B] {
return F.Bind2nd(MonadAp[B, E, A], fa)
}
@@ -86,7 +93,10 @@ func Ap[B, E, A any](fa Either[E, A]) Operator[E, func(A) B, B] {
//
//go:inline
func MonadMap[E, A, B any](fa Either[E, A], f func(a A) B) Either[E, B] {
return MonadChain(fa, F.Flow2(f, Right[E, B]))
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](f(fa.r))
}
// MonadBiMap applies two functions: one to transform a Left value, another to transform a Right value.
@@ -100,13 +110,18 @@ func MonadMap[E, A, B any](fa Either[E, A], f func(a A) B) Either[E, B] {
// func(n int) string { return fmt.Sprint(n) },
// ) // Left("error")
func MonadBiMap[E1, E2, A, B any](fa Either[E1, A], f func(E1) E2, g func(a A) B) Either[E2, B] {
return MonadFold(fa, F.Flow2(f, Left[B, E2]), F.Flow2(g, Right[E2, B]))
if fa.isLeft {
return Left[B](f(fa.l))
}
return Of[E2](g(fa.r))
}
// BiMap is the curried version of [MonadBiMap].
// Maps a pair of functions over the two type arguments of the bifunctor.
func BiMap[E1, E2, A, B any](f func(E1) E2, g func(a A) B) func(Either[E1, A]) Either[E2, B] {
return Fold(F.Flow2(f, Left[B, E2]), F.Flow2(g, Right[E2, B]))
return func(fa Either[E1, A]) Either[E2, B] {
return MonadBiMap(fa, f, g)
}
}
// MonadMapTo replaces the Right value with a constant value.
@@ -116,12 +131,15 @@ func BiMap[E1, E2, A, B any](f func(E1) E2, g func(a A) B) func(Either[E1, A]) E
//
// result := either.MonadMapTo(either.Right[error](21), "success") // Right("success")
func MonadMapTo[E, A, B any](fa Either[E, A], b B) Either[E, B] {
return MonadMap(fa, F.Constant1[A](b))
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](b)
}
// MapTo is the curried version of [MonadMapTo].
func MapTo[E, A, B any](b B) Operator[E, A, B] {
return Map[E](F.Constant1[A](b))
return F.Bind2nd(MonadMapTo[E, A], b)
}
// MonadMapLeft applies a transformation function to the Left (error) value.
@@ -139,8 +157,8 @@ func MonadMapLeft[E1, A, E2 any](fa Either[E1, A], f func(E1) E2) Either[E2, A]
// Map is the curried version of [MonadMap].
// Transforms the Right value using the provided function.
func Map[E, A, B any](f func(a A) B) func(fa Either[E, A]) Either[E, B] {
return Chain(F.Flow2(f, Right[E, B]))
func Map[E, A, B any](f func(a A) B) Operator[E, A, B] {
return F.Bind2nd(MonadMap[E], f)
}
// MapLeft is the curried version of [MonadMapLeft].
@@ -163,8 +181,11 @@ func MapLeft[A, E1, E2 any](f func(E1) E2) func(fa Either[E1, A]) Either[E2, A]
// ) // Right(42)
//
//go:inline
func MonadChain[E, A, B any](fa Either[E, A], f func(a A) Either[E, B]) Either[E, B] {
return MonadFold(fa, Left[B, E], f)
func MonadChain[E, A, B any](fa Either[E, A], f Kleisli[E, A, B]) Either[E, B] {
if fa.isLeft {
return Left[B](fa.l)
}
return f(fa.r)
}
// MonadChainFirst executes a side-effect computation but returns the original value.
@@ -179,7 +200,7 @@ func MonadChain[E, A, B any](fa Either[E, A], f func(a A) Either[E, B]) Either[E
// return either.Right[error]("logged")
// },
// ) // Right(42) - original value preserved
func MonadChainFirst[E, A, B any](ma Either[E, A], f func(a A) Either[E, B]) Either[E, A] {
func MonadChainFirst[E, A, B any](ma Either[E, A], f Kleisli[E, A, B]) Either[E, A] {
return C.MonadChainFirst(
MonadChain[E, A, A],
MonadMap[E, B, A],
@@ -225,12 +246,12 @@ func ChainTo[A, E, B any](mb Either[E, B]) Operator[E, A, B] {
// Chain is the curried version of [MonadChain].
// Sequences two computations where the second depends on the first.
func Chain[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, B] {
return Fold(Left[B, E], f)
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B] {
return F.Bind2nd(MonadChain[E], f)
}
// ChainFirst is the curried version of [MonadChainFirst].
func ChainFirst[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, A] {
func ChainFirst[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, A] {
return C.ChainFirst(
Chain[E, A, A],
Map[E, B, A],
@@ -271,7 +292,7 @@ func TryCatch[FE func(error) E, E, A any](val A, err error, onThrow FE) Either[E
// result := either.TryCatchError(42, nil) // Right(42)
// result := either.TryCatchError(0, errors.New("fail")) // Left(error)
func TryCatchError[A any](val A, err error) Either[error, A] {
return TryCatch(val, err, E.IdentityError)
return TryCatch(val, err, E.Identity)
}
// Sequence2 sequences two Either values using a combining function.
@@ -335,7 +356,7 @@ func FromError[A any](f func(a A) error) func(A) Either[error, A] {
// err := either.ToError(either.Left[int](errors.New("fail"))) // error
// err := either.ToError(either.Right[error](42)) // nil
func ToError[A any](e Either[error, A]) error {
return MonadFold(e, E.IdentityError, F.Constant1[A, error](nil))
return MonadFold(e, E.Identity, F.Constant1[A, error](nil))
}
// Fold is the curried version of [MonadFold].
@@ -347,6 +368,8 @@ func ToError[A any](e Either[error, A]) error {
// func(err error) string { return "Error: " + err.Error() },
// func(n int) string { return fmt.Sprintf("Value: %d", n) },
// )(either.Right[error](42)) // "Value: 42"
//
//go:inline
func Fold[E, A, B any](onLeft func(E) B, onRight func(A) B) func(Either[E, A]) B {
return func(ma Either[E, A]) B {
return MonadFold(ma, onLeft, onRight)
@@ -410,10 +433,12 @@ func GetOrElse[E, A any](onLeft func(E) A) func(Either[E, A]) A {
// Reduce folds an Either into a single value using a reducer function.
// Returns the initial value for Left, or applies the reducer to the Right value.
func Reduce[E, A, B any](f func(B, A) B, initial B) func(Either[E, A]) B {
return Fold(
F.Constant1[E](initial),
F.Bind1st(f, initial),
)
return func(fa Either[E, A]) B {
if fa.isLeft {
return initial
}
return f(initial, fa.r)
}
}
// AltW provides an alternative Either if the first is Left, allowing different error types.
@@ -425,7 +450,7 @@ func Reduce[E, A, B any](f func(B, A) B, initial B) func(Either[E, A]) B {
// return either.Right[string](99)
// })
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
func AltW[E, E1, A any](that L.Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1, A] {
func AltW[E, E1, A any](that Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1, A] {
return Fold(F.Ignore1of1[E](that), Right[E1, A])
}
@@ -437,7 +462,7 @@ func AltW[E, E1, A any](that L.Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1
// return either.Right[error](99)
// })
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
func Alt[E, A any](that L.Lazy[Either[E, A]]) Operator[E, A, A] {
func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
return AltW[E](that)
}
@@ -480,23 +505,28 @@ func Memoize[E, A any](val Either[E, A]) Either[E, A] {
// MonadSequence2 sequences two Either values using a combining function.
// Short-circuits on the first Left encountered.
func MonadSequence2[E, T1, T2, R any](e1 Either[E, T1], e2 Either[E, T2], f func(T1, T2) Either[E, R]) Either[E, R] {
return MonadFold(e1, Left[R, E], func(t1 T1) Either[E, R] {
return MonadFold(e2, Left[R, E], func(t2 T2) Either[E, R] {
return f(t1, t2)
})
})
if e1.isLeft {
return Left[R](e1.l)
}
if e2.isLeft {
return Left[R](e2.l)
}
return f(e1.r, e2.r)
}
// MonadSequence3 sequences three Either values using a combining function.
// Short-circuits on the first Left encountered.
func MonadSequence3[E, T1, T2, T3, R any](e1 Either[E, T1], e2 Either[E, T2], e3 Either[E, T3], f func(T1, T2, T3) Either[E, R]) Either[E, R] {
return MonadFold(e1, Left[R, E], func(t1 T1) Either[E, R] {
return MonadFold(e2, Left[R, E], func(t2 T2) Either[E, R] {
return MonadFold(e3, Left[R, E], func(t3 T3) Either[E, R] {
return f(t1, t2, t3)
})
})
})
if e1.isLeft {
return Left[R](e1.l)
}
if e2.isLeft {
return Left[R](e2.l)
}
if e3.isLeft {
return Left[R](e3.l)
}
return f(e1.r, e2.r, e3.r)
}
// Swap exchanges the Left and Right type parameters.
@@ -524,6 +554,6 @@ func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
// MonadAlt provides an alternative Either if the first is Left.
// This is the monadic version of [Alt].
func MonadAlt[E, A any](fa Either[E, A], that L.Lazy[Either[E, A]]) Either[E, A] {
func MonadAlt[E, A any](fa Either[E, A], that Lazy[Either[E, A]]) Either[E, A] {
return MonadFold(fa, F.Ignore1of1[E](that), Of[E, A])
}

View File

@@ -201,7 +201,7 @@ func TestSwap(t *testing.T) {
// Test MonadFlap and Flap
func TestFlap(t *testing.T) {
fab := Right[error](func(x int) string { return strconv.Itoa(x) })
fab := Right[error](strconv.Itoa)
result := MonadFlap(fab, 42)
assert.Equal(t, Right[error]("42"), result)
@@ -615,7 +615,7 @@ func TestMonad(t *testing.T) {
assert.Equal(t, Right[error](42), result)
// Test Map
mapFn := m.Map(func(x int) string { return strconv.Itoa(x) })
mapFn := m.Map(strconv.Itoa)
result2 := mapFn(Right[error](42))
assert.Equal(t, Right[error]("42"), result2)
@@ -628,7 +628,7 @@ func TestMonad(t *testing.T) {
// Test Ap
apFn := m.Ap(Right[error](42))
result4 := apFn(Right[error](func(x int) string { return strconv.Itoa(x) }))
result4 := apFn(Right[error](strconv.Itoa))
assert.Equal(t, Right[error]("42"), result4)
}

33
v2/either/escape_test.go Normal file
View File

@@ -0,0 +1,33 @@
// 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 either
// Test functions to analyze escape behavior
//go:noinline
func testOf(x int) Either[error, int] {
return Of[error](x)
}
//go:noinline
func testRight(x int) Either[error, int] {
return Right[error](x)
}
//go:noinline
func testLeft(x int) Either[int, string] {
return Left[string](x)
}

View File

@@ -19,38 +19,116 @@ import (
"github.com/IBM/fp-go/v2/internal/monad"
)
type eitherMonad[E, A, B any] struct{}
func (o *eitherMonad[E, A, B]) Of(a A) Either[E, A] {
return Of[E](a)
// eitherMonad is the internal implementation of the Monad type class for Either.
// It extends eitherApplicative by adding the Chain operation for sequential composition.
type eitherMonad[E, A, B any] struct {
eitherApplicative[E, A, B]
fchain func(Kleisli[E, A, B]) Operator[E, A, B]
}
func (o *eitherMonad[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
return Map[E](f)
// Chain sequences dependent computations, failing fast on the first Left.
func (o *eitherMonad[E, A, B]) Chain(f Kleisli[E, A, B]) Operator[E, A, B] {
return o.fchain(f)
}
func (o *eitherMonad[E, A, B]) Chain(f func(A) Either[E, B]) Operator[E, A, B] {
return Chain(f)
}
func (o *eitherMonad[E, A, B]) Ap(fa Either[E, A]) Operator[E, func(A) B, B] {
return Ap[B](fa)
}
// Monad implements the monadic operations for Either.
// A monad combines the capabilities of Functor (Map), Applicative (Ap), and Chain (flatMap/bind).
// This allows for sequential composition of computations that may fail.
// Monad creates a lawful Monad instance for Either with fail-fast error handling.
//
// Example:
// A monad combines the capabilities of four type classes:
// - Functor (Map): transform the Right value
// - Pointed (Of): lift a pure value into a Right
// - Applicative (Ap): apply wrapped functions (fails fast on first Left)
// - Chainable (Chain): sequence dependent computations (fails fast on first Left)
//
// The Either monad is left-biased and fails fast: once a Left is encountered,
// no further computations are performed and the Left is propagated immediately.
// This makes it ideal for error handling where you want to stop at the first error.
//
// This implementation satisfies all monad laws:
//
// Monad Laws:
// - Left Identity: Chain(f)(Of(a)) == f(a)
// - Right Identity: Chain(Of)(m) == m
// - Associativity: Chain(g)(Chain(f)(m)) == Chain(x => Chain(g)(f(x)))(m)
//
// Additionally, it satisfies all prerequisite laws from Functor, Apply, and Applicative.
//
// Relationship to Applicative:
//
// This Monad uses the standard fail-fast Applicative (see Applicative function).
// In a lawful monad, Ap can be derived from Chain and Of:
//
// Ap(fa)(ff) == Chain(f => Chain(a => Of(f(a)))(fa))(ff)
//
// The Either monad satisfies this property, making it a true lawful monad.
//
// When to use Monad vs Applicative:
// - Use Monad when you need sequential dependent operations (Chain)
// - Use Applicative when you only need independent operations (Ap, Map)
// - Both fail fast on the first error
//
// When to use Monad vs ApplicativeV:
// - Use Monad for sequential error handling (fail-fast)
// - Use ApplicativeV for parallel validation (error accumulation)
// - Note: There is no "MonadV" because Chain inherently fails fast
//
// Example - Sequential Dependent Operations:
//
// m := either.Monad[error, int, string]()
//
// // Chain allows each step to depend on the previous result
// result := m.Chain(func(x int) either.Either[error, string] {
// if x > 0 {
// return either.Right[error](strconv.Itoa(x))
// }
// return either.Left[string](errors.New("negative"))
// return either.Left[string](errors.New("value must be positive"))
// })(either.Right[error](42))
// // result is Right("42")
//
// // Fails fast on first error
// result2 := m.Chain(func(x int) either.Either[error, string] {
// return either.Right[error](strconv.Itoa(x))
// })(either.Left[int](errors.New("initial error")))
// // result2 is Left("initial error") - Chain never executes
//
// Example - Combining with Applicative operations:
//
// m := either.Monad[error, int, int]()
//
// // Map transforms the value
// value := m.Map(func(x int) int { return x * 2 })(either.Right[error](21))
// // value is Right(42)
//
// // Ap applies wrapped functions (also fails fast)
// fn := either.Right[error](func(x int) int { return x + 1 })
// result := m.Ap(value)(fn)
// // result is Right(43)
//
// Example - Real-world usage with error handling:
//
// m := either.Monad[error, User, SavedUser]()
//
// // Pipeline of operations that can fail
// result := m.Chain(func(user User) either.Either[error, SavedUser] {
// // Save to database
// return saveToDatabase(user)
// })(m.Chain(func(user User) either.Either[error, User] {
// // Validate user
// return validateUser(user)
// })(either.Right[error](inputUser)))
//
// // If any step fails, the error propagates immediately
//
// Type Parameters:
// - E: The error type (Left value)
// - A: The input value type (Right value)
// - B: The output value type after transformation
func Monad[E, A, B any]() monad.Monad[A, B, Either[E, A], Either[E, B], Either[E, func(A) B]] {
return &eitherMonad[E, A, B]{}
return &eitherMonad[E, A, B]{
eitherApplicative[E, A, B]{
Of[E, A],
Map[E, A, B],
Ap[B, E, A],
},
Chain[E, A, B],
}
}

View File

@@ -16,7 +16,6 @@
package either
import (
L "github.com/IBM/fp-go/v2/lazy"
M "github.com/IBM/fp-go/v2/monoid"
)
@@ -51,7 +50,7 @@ func AlternativeMonoid[E, A any](m M.Monoid[A]) Monoid[E, A] {
// m := either.AltMonoid[error, int](zero)
// result := m.Concat(either.Left[int](errors.New("err1")), either.Right[error](42))
// // result is Right(42)
func AltMonoid[E, A any](zero L.Lazy[Either[E, A]]) Monoid[E, A] {
func AltMonoid[E, A any](zero Lazy[Either[E, A]]) Monoid[E, A] {
return M.AltMonoid(
zero,
MonadAlt[E, A],

View File

@@ -35,13 +35,18 @@ import (
// // result is Right(map[string]int{"a": 1, "b": 2})
//
//go:inline
func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, E, A, B any](f func(A) Either[E, B]) func(GA) Either[E, GB] {
return RR.Traverse[GA](
Of[E, GB],
Map[E, GB, func(B) GB],
Ap[GB, E, B],
f,
)
func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, E, A, B any](f Kleisli[E, A, B]) Kleisli[E, GA, GB] {
return func(ga GA) Either[E, GB] {
bs := make(GB, len(ga))
for i, a := range ga {
b := f(a)
if b.isLeft {
return Left[GB](b.l)
}
bs[i] = b.r
}
return Of[E](bs)
}
}
// TraverseRecord transforms a map by applying a function that returns an Either to each value.
@@ -58,7 +63,7 @@ func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, E, A, B any](f func
// // result is Right(map[string]int{"a": 1, "b": 2})
//
//go:inline
func TraverseRecord[K comparable, E, A, B any](f func(A) Either[E, B]) func(map[K]A) Either[E, map[K]B] {
func TraverseRecord[K comparable, E, A, B any](f Kleisli[E, A, B]) Kleisli[E, map[K]A, map[K]B] {
return TraverseRecordG[map[K]A, map[K]B](f)
}
@@ -79,13 +84,18 @@ func TraverseRecord[K comparable, E, A, B any](f func(A) Either[E, B]) func(map[
// // result is Right(map[string]string{"a": "a:1"})
//
//go:inline
func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, E, A, B any](f func(K, A) Either[E, B]) func(GA) Either[E, GB] {
return RR.TraverseWithIndex[GA](
Of[E, GB],
Map[E, GB, func(B) GB],
Ap[GB, E, B],
f,
)
func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, E, A, B any](f func(K, A) Either[E, B]) Kleisli[E, GA, GB] {
return func(ga GA) Either[E, GB] {
bs := make(GB, len(ga))
for i, a := range ga {
b := f(i, a)
if b.isLeft {
return Left[GB](b.l)
}
bs[i] = b.r
}
return Of[E](bs)
}
}
// TraverseRecordWithIndex transforms a map by applying an indexed function that returns an Either.
@@ -104,7 +114,7 @@ func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, E, A, B an
// // result is Right(map[string]string{"a": "a:1"})
//
//go:inline
func TraverseRecordWithIndex[K comparable, E, A, B any](f func(K, A) Either[E, B]) func(map[K]A) Either[E, map[K]B] {
func TraverseRecordWithIndex[K comparable, E, A, B any](f func(K, A) Either[E, B]) Kleisli[E, map[K]A, map[K]B] {
return TraverseRecordWithIndexG[map[K]A, map[K]B](f)
}

View File

@@ -15,10 +15,6 @@
package either
import (
F "github.com/IBM/fp-go/v2/function"
)
// WithResource constructs a function that creates a resource, operates on it, and then releases it.
// This ensures proper resource cleanup even if operations fail.
// The resource is released immediately after the operation completes.
@@ -43,25 +39,24 @@ import (
// // Use file here
// return either.Right[error]("data")
// })
func WithResource[E, R, A, ANY any](onCreate func() Either[E, R], onRelease Kleisli[E, R, ANY]) func(func(R) Either[E, A]) Either[E, A] {
func WithResource[A, E, R, ANY any](
onCreate func() Either[E, R],
onRelease Kleisli[E, R, ANY],
) Kleisli[E, Kleisli[E, R, A], A] {
return func(f func(R) Either[E, A]) Either[E, A] {
return MonadChain(
onCreate(), func(r R) Either[E, A] {
// run the code and make sure to release as quickly as possible
res := f(r)
released := onRelease(r)
// handle the errors
return MonadFold(
res,
Left[A, E],
func(a A) Either[E, A] {
return F.Pipe1(
released,
MapTo[E, ANY](a),
)
})
},
)
r := onCreate()
if r.isLeft {
return Left[A](r.l)
}
a := f(r.r)
n := onRelease(r.r)
if a.isLeft {
return Left[A](a.l)
}
if n.isLeft {
return Left[A](n.l)
}
return Of[E](a.r)
}
}

View File

@@ -40,7 +40,7 @@ func TestWithResource(t *testing.T) {
return Of[error](f.Name())
}
tempFile := WithResource[error, *os.File, string](onCreate, onDelete)
tempFile := WithResource[string](onCreate, onDelete)
resE := tempFile(onHandler)

View File

@@ -47,7 +47,7 @@ import (
// eqString := eq.FromStrictEquals[string]()
// eqError := eq.FromStrictEquals[error]()
//
// ab := func(x int) string { return strconv.Itoa(x) }
// ab := strconv.Itoa
// bc := func(s string) bool { return len(s) > 0 }
//
// testing.AssertLaws(t, eqError, eqInt, eqString, eq.FromStrictEquals[bool](), ab, bc)(42)

View File

@@ -17,6 +17,7 @@ package either
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
@@ -29,6 +30,7 @@ type (
Option[A any] = option.Option[A]
Lens[S, T any] = lens.Lens[S, T]
Endomorphism[T any] = endomorphism.Endomorphism[T]
Lazy[T any] = lazy.Lazy[T]
Kleisli[E, A, B any] = reader.Reader[A, Either[E, B]]
Operator[E, A, B any] = Kleisli[E, Either[E, A], B]

View File

@@ -72,14 +72,18 @@ import (
// fab3 := either.Right[string](N.Mul(2))
// fa3 := either.Right[string](21)
// result3 := applyV(fab3, fa3) // Right(42)
func MonadApV[B, E, A any](sg S.Semigroup[E]) func(fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
c := F.Bind2of2(sg.Concat)
func MonadApV[B, A, E any](sg S.Semigroup[E]) func(fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
return func(fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
return MonadFold(fab, func(eab E) Either[E, B] {
return MonadFold(fa, F.Flow2(c(eab), Left[B]), F.Constant1[A](Left[B](eab)))
}, func(ab func(A) B) Either[E, B] {
return MonadFold(fa, Left[B, E], F.Flow2(ab, Right[E, B]))
})
if fab.isLeft {
if fa.isLeft {
return Left[B](sg.Concat(fab.l, fa.l))
}
return Left[B](fab.l)
}
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](fab.r(fa.r))
}
}
@@ -130,13 +134,11 @@ func MonadApV[B, E, A any](sg S.Semigroup[E]) func(fab Either[E, func(a A) B], f
//
// result := applyValidation(value)(validator)
// // Left(ValidationError{Errors: []string{"invalid validator", "invalid input"}})
func ApV[B, E, A any](sg S.Semigroup[E]) func(fa Either[E, A]) Operator[E, func(A) B, B] {
c := F.Bind2of2(sg.Concat)
return func(fa Either[E, A]) Operator[E, func(A) B, B] {
return Fold(func(eab E) Either[E, B] {
return MonadFold(fa, F.Flow2(c(eab), Left[B]), F.Constant1[A](Left[B](eab)))
}, func(ab func(A) B) Either[E, B] {
return MonadFold(fa, Left[B, E], F.Flow2(ab, Right[E, B]))
})
//
//go:inline
func ApV[B, A, E any](sg S.Semigroup[E]) func(Either[E, A]) Operator[E, func(A) B, B] {
apv := MonadApV[B, A](sg)
return func(e Either[E, A]) Operator[E, func(A) B, B] {
return F.Bind2nd(apv, e)
}
}

View File

@@ -20,19 +20,18 @@ import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/semigroup"
SG "github.com/IBM/fp-go/v2/semigroup"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestMonadApV_BothRight tests MonadApV when both function and value are Right
func TestMonadApV_BothRight(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
applyV := MonadApV[int, int](sg)
// Both are Right - should apply function
fab := Right[string](N.Mul(2))
@@ -47,12 +46,10 @@ func TestMonadApV_BothRight(t *testing.T) {
// TestMonadApV_BothLeft tests MonadApV when both function and value are Left
func TestMonadApV_BothLeft(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
applyV := MonadApV[int, int](sg)
// Both are Left - should combine errors
fab := Left[func(int) int]("error1")
@@ -62,18 +59,16 @@ func TestMonadApV_BothLeft(t *testing.T) {
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
assert.Equal(t, Left[int]("error2; error1"), result)
assert.Equal(t, Left[int]("error1; error2"), result)
}
// TestMonadApV_LeftFunction tests MonadApV when function is Left and value is Right
func TestMonadApV_LeftFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
applyV := MonadApV[int, int](sg)
// Function is Left, value is Right - should return function's error
fab := Left[func(int) int]("function error")
@@ -88,12 +83,10 @@ func TestMonadApV_LeftFunction(t *testing.T) {
// TestMonadApV_LeftValue tests MonadApV when function is Right and value is Left
func TestMonadApV_LeftValue(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
applyV := MonadApV[int, int](sg)
// Function is Right, value is Left - should return value's error
fab := Right[string](N.Mul(2))
@@ -108,12 +101,12 @@ func TestMonadApV_LeftValue(t *testing.T) {
// TestMonadApV_WithSliceSemigroup tests MonadApV with a slice-based semigroup
func TestMonadApV_WithSliceSemigroup(t *testing.T) {
// Create a semigroup that concatenates slices
sg := S.MakeSemigroup(func(a, b []string) []string {
sg := SG.MakeSemigroup(func(a, b []string) []string {
return append(a, b...)
})
// Create the validation applicative
applyV := MonadApV[string, []string, string](sg)
applyV := MonadApV[string, string](sg)
// Both are Left with slice errors
fab := Left[func(string) string]([]string{"error1", "error2"})
@@ -123,19 +116,19 @@ func TestMonadApV_WithSliceSemigroup(t *testing.T) {
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa errors + fab errors
expected := Left[string]([]string{"error3", "error4", "error1", "error2"})
expected := Left[string]([]string{"error1", "error2", "error3", "error4"})
assert.Equal(t, expected, result)
}
// TestMonadApV_ComplexFunction tests MonadApV with a more complex function
func TestMonadApV_ComplexFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
sg := SG.MakeSemigroup(func(a, b string) string {
return a + " | " + b
})
// Create the validation applicative
applyV := MonadApV[string, string, int](sg)
applyV := MonadApV[string, int](sg)
// Test with a function that transforms the value
fab := Right[string](func(x int) string {
@@ -155,12 +148,10 @@ func TestMonadApV_ComplexFunction(t *testing.T) {
// TestApV_BothRight tests ApV when both function and value are Right
func TestApV_BothRight(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, string, int](sg)
applyV := ApV[int, int](sg)
// Both are Right - should apply function
fa := Right[string](21)
@@ -175,12 +166,10 @@ func TestApV_BothRight(t *testing.T) {
// TestApV_BothLeft tests ApV when both function and value are Left
func TestApV_BothLeft(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, string, int](sg)
applyV := ApV[int, int](sg)
// Both are Left - should combine errors
fa := Left[int]("error2")
@@ -190,18 +179,16 @@ func TestApV_BothLeft(t *testing.T) {
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
assert.Equal(t, Left[int]("error2; error1"), result)
assert.Equal(t, Left[int]("error1; error2"), result)
}
// TestApV_LeftFunction tests ApV when function is Left and value is Right
func TestApV_LeftFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, string, int](sg)
applyV := ApV[int, int](sg)
// Function is Left, value is Right - should return function's error
fa := Right[string](21)
@@ -216,12 +203,10 @@ func TestApV_LeftFunction(t *testing.T) {
// TestApV_LeftValue tests ApV when function is Right and value is Left
func TestApV_LeftValue(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, string, int](sg)
applyV := ApV[int, int](sg)
// Function is Right, value is Left - should return value's error
fa := Left[int]("value error")
@@ -236,12 +221,12 @@ func TestApV_LeftValue(t *testing.T) {
// TestApV_Composition tests ApV with function composition
func TestApV_Composition(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
sg := SG.MakeSemigroup(func(a, b string) string {
return a + " & " + b
})
// Create the validation applicative
applyV := ApV[string, string, int](sg)
applyV := ApV[string, int](sg)
// Test composition with pipe
fa := Right[string](10)
@@ -267,18 +252,18 @@ func TestApV_WithStructSemigroup(t *testing.T) {
}
// Create a semigroup that combines validation errors
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
sg := SG.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
return ValidationErrors{
Errors: append(append([]string{}, a.Errors...), b.Errors...),
}
})
// Create the validation applicative
applyV := ApV[int, ValidationErrors, int](sg)
applyV := ApV[int, int](sg)
// Both are Left with validation errors
fa := Left[int](ValidationErrors{Errors: []string{"field1: required"}})
fab := Left[func(int) int](ValidationErrors{Errors: []string{"field2: invalid"}})
fa := Left[int](ValidationErrors{Errors: []string{"field2: invalid"}})
fab := Left[func(int) int](ValidationErrors{Errors: []string{"field1: required"}})
result := applyV(fa)(fab)
@@ -293,12 +278,12 @@ func TestApV_WithStructSemigroup(t *testing.T) {
// TestApV_MultipleValidations tests ApV with multiple validation steps
func TestApV_MultipleValidations(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
sg := SG.MakeSemigroup(func(a, b string) string {
return a + ", " + b
})
// Create the validation applicative
applyV := ApV[int, string, int](sg)
applyV := ApV[int, int](sg)
// Simulate multiple validation failures
validation1 := Left[int]("age must be positive")
@@ -308,18 +293,16 @@ func TestApV_MultipleValidations(t *testing.T) {
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: validation1 error + validation2 error
assert.Equal(t, Left[int]("age must be positive, name is required"), result)
assert.Equal(t, Left[int]("name is required, age must be positive"), result)
}
// TestMonadApV_DifferentTypes tests MonadApV with different input and output types
func TestMonadApV_DifferentTypes(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + " + " + b
})
sg := S.IntersperseSemigroup(" + ")
// Create the validation applicative
applyV := MonadApV[string, string, int](sg)
applyV := MonadApV[string, int](sg)
// Function converts int to string
fab := Right[string](func(x int) string {
@@ -343,10 +326,10 @@ func TestMonadApV_DifferentTypes(t *testing.T) {
// TestApV_FirstSemigroup tests ApV with First semigroup (always returns first error)
func TestApV_FirstSemigroup(t *testing.T) {
// Use First semigroup which always returns the first value
sg := S.First[string]()
sg := SG.First[string]()
// Create the validation applicative
applyV := ApV[int, string, int](sg)
applyV := ApV[int, int](sg)
// Both are Left - should return first error
fa := Left[int]("error2")
@@ -355,17 +338,17 @@ func TestApV_FirstSemigroup(t *testing.T) {
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// First semigroup returns the first value, which is fa's error
assert.Equal(t, Left[int]("error2"), result)
// First semigroup returns the first value, which is fab's error
assert.Equal(t, Left[int]("error1"), result)
}
// TestApV_LastSemigroup tests ApV with Last semigroup (always returns last error)
func TestApV_LastSemigroup(t *testing.T) {
// Use Last semigroup which always returns the last value
sg := S.Last[string]()
sg := SG.Last[string]()
// Create the validation applicative
applyV := ApV[int, string, int](sg)
applyV := ApV[int, int](sg)
// Both are Left - should return last error
fa := Left[int]("error2")
@@ -374,8 +357,6 @@ func TestApV_LastSemigroup(t *testing.T) {
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// Last semigroup returns the last value, which is fab's error
assert.Equal(t, Left[int]("error1"), result)
// Last semigroup returns the last value, which is fa's error
assert.Equal(t, Left[int]("error2"), result)
}
// Made with Bob

View File

@@ -22,12 +22,12 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// IdentityError is the identity function specialized for error types.
// Identity is the identity function specialized for error types.
// It returns the error unchanged, useful in functional composition where
// an error needs to be passed through without modification.
//
// Example:
//
// err := errors.New("something went wrong")
// same := IdentityError(err) // returns the same error
var IdentityError = F.Identity[error]
// same := Identity(err) // returns the same error
var Identity = F.Identity[error]

View File

@@ -42,7 +42,10 @@ package function
// divide := func(a, b float64) float64 { return a / b }
// divideBy10 := Bind1st(divide, 10.0)
// result := divideBy10(2.0) // 5.0 (10 / 2)
//
//go:inline
func Bind1st[T1, T2, R any](f func(T1, T2) R, t1 T1) func(T2) R {
//go:inline
return func(t2 T2) R {
return f(t1, t2)
}
@@ -75,7 +78,10 @@ func Bind1st[T1, T2, R any](f func(T1, T2) R, t1 T1) func(T2) R {
// divide := func(a, b float64) float64 { return a / b }
// halve := Bind2nd(divide, 2.0)
// result := halve(10.0) // 5.0 (10 / 2)
//
//go:inline
func Bind2nd[T1, T2, R any](f func(T1, T2) R, t2 T2) func(T1) R {
//go:inline
return func(t1 T1) R {
return f(t1, t2)
}
@@ -104,6 +110,8 @@ func Bind2nd[T1, T2, R any](f func(T1, T2) R, t2 T2) func(T1) R {
//
// result := SK(42, "hello") // "hello"
// result := SK(true, 100) // 100
//
//go:inline
func SK[T1, T2 any](_ T1, t2 T2) T2 {
return t2
}

View File

@@ -103,4 +103,28 @@ var (
// body := Body(fullResp)
// content := string(body)
Body = P.Tail[*H.Response, []byte]
// FromResponse creates a function that constructs a FullResponse from
// a given *http.Response. It returns a function that takes a body byte
// slice and combines it with the response to create a FullResponse.
//
// This is useful for functional composition where you want to partially
// apply the response and later provide the body.
//
// Example:
// makeFullResp := FromResponse(response)
// fullResp := makeFullResp(bodyBytes)
FromResponse = P.FromHead[[]byte, *H.Response]
// FromBody creates a function that constructs a FullResponse from
// a given body byte slice. It returns a function that takes an
// *http.Response and combines it with the body to create a FullResponse.
//
// This is useful for functional composition where you want to partially
// apply the body and later provide the response.
//
// Example:
// makeFullResp := FromBody(bodyBytes)
// fullResp := makeFullResp(response)
FromBody = P.FromTail[*H.Response, []byte]
)

505
v2/idiomatic/doc.go Normal file
View File

@@ -0,0 +1,505 @@
// 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 idiomatic provides functional programming constructs optimized for idiomatic Go.
//
// # Overview
//
// The idiomatic package reimagines functional programming patterns using Go's native tuple return
// values instead of wrapper structs. This approach provides better performance, lower memory
// overhead, and a more familiar API for Go developers while maintaining functional programming
// principles.
//
// # Key Differences from Standard Packages
//
// Unlike the standard fp-go packages (option, either, result) which use struct wrappers,
// the idiomatic package uses Go's native tuple patterns:
//
// Standard either: Either[E, A] (struct wrapper)
// Idiomatic result: (A, error) (native Go tuple)
//
// Standard option: Option[A] (struct wrapper)
// Idiomatic option: (A, bool) (native Go tuple)
//
// # Performance Benefits
//
// The idiomatic approach offers several performance advantages:
//
// - Zero allocation for creating values (no heap allocations)
// - Better CPU cache locality (no pointer indirection)
// - Native Go compiler optimizations for tuples
// - Reduced garbage collection pressure
// - Smaller memory footprint
//
// Benchmarks show 2-10x performance improvements for common operations compared to struct-based
// implementations, especially for simple operations like Map, Chain, and Fold.
//
// # Design Philosophy
//
// The idiomatic packages follow these design principles:
//
// 1. Native Go Idioms: Use Go's built-in patterns (tuples, error handling)
// 2. Zero-Cost Abstraction: No runtime overhead for functional patterns
// 3. Composability: All operations compose naturally with standard Go code
// 4. Familiarity: API feels natural to Go developers
// 5. Type Safety: Full compile-time type checking
//
// # Subpackages
//
// The idiomatic package includes three main subpackages:
//
// ## idiomatic/option
//
// Implements the Option monad using (value, bool) tuples where the boolean indicates
// presence (true) or absence (false). This is similar to Go's map lookup pattern.
//
// Example usage:
//
// import "github.com/IBM/fp-go/v2/idiomatic/option"
//
// // Creating options
// some := option.Some(42) // (42, true)
// none := option.None[int]() // (0, false)
//
// // Transforming values
// double := option.Map(func(x int) int { return x * 2 })
// result := double(some) // (84, true)
// result = double(none) // (0, false)
//
// // Chaining operations
// validate := option.Chain(func(x int) (int, bool) {
// if x > 0 { return x * 2, true }
// return 0, false
// })
// result = validate(some) // (84, true)
//
// // Pattern matching
// value := option.GetOrElse(func() int { return 0 })(some) // 42
//
// ## idiomatic/result
//
// Implements the Either/Result monad using (value, error) tuples, leveraging Go's standard
// error handling pattern. By convention, (value, nil) represents success and (zero, error)
// represents failure.
//
// Example usage:
//
// import "github.com/IBM/fp-go/v2/idiomatic/result"
//
// // Creating results
// success := result.Right(42) // (42, nil)
// failure := result.Left[int](errors.New("oops")) // (0, error)
//
// // Transforming values
// double := result.Map(func(x int) int { return x * 2 })
// res := double(success) // (84, nil)
// res = double(failure) // (0, error)
//
// // Chaining operations (short-circuits on error)
// validate := result.Chain(func(x int) (int, error) {
// if x > 0 { return x * 2, nil }
// return 0, errors.New("negative")
// })
// res = validate(success) // (84, nil)
//
// // Pattern matching
// output := result.Fold(
// func(err error) string { return "Error: " + err.Error() },
// func(n int) string { return fmt.Sprintf("Success: %d", n) },
// )(success) // "Success: 42"
//
// // Direct integration with Go error handling
// value, err := result.Right(42)
// if err != nil {
// // handle error
// }
//
// ## idiomatic/ioresult
//
// Implements the IOResult monad using func() (value, error) for IO operations that can fail.
// This combines IO effects (side-effectful operations) with Go's standard error handling pattern.
// It's the idiomatic version of IOEither, representing computations that perform side effects
// and may fail.
//
// Example usage:
//
// import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
//
// // Creating IOResult values
// success := ioresult.Of(42) // func() (int, error) returning (42, nil)
// failure := ioresult.Left[int](errors.New("oops")) // func() (int, error) returning (0, error)
//
// // Reading a file with IOResult
// readConfig := ioresult.FromIO(func() string {
// return "config.json"
// })
//
// // Transforming IO operations
// processFile := F.Pipe2(
// readConfig,
// ioresult.Map(strings.ToUpper),
// ioresult.Chain(func(path string) ioresult.IOResult[[]byte] {
// return func() ([]byte, error) {
// return os.ReadFile(path)
// }
// }),
// )
//
// // Execute the IO operation
// content, err := processFile()
// if err != nil {
// log.Fatal(err)
// }
//
// // Resource management with Bracket
// result, err := ioresult.Bracket(
// func() (*os.File, error) { return os.Open("data.txt") },
// func(f *os.File, err error) ioresult.IOResult[any] {
// return func() (any, error) { return nil, f.Close() }
// },
// func(f *os.File) ioresult.IOResult[[]byte] {
// return func() ([]byte, error) { return io.ReadAll(f) }
// },
// )()
//
// Key features:
// - Lazy evaluation: Operations are not executed until the IOResult is called
// - Composable: Chain IO operations that may fail
// - Error handling: Automatic error propagation and recovery
// - Resource safety: Bracket ensures proper resource cleanup
// - Parallel execution: ApPar and TraverseArrayPar for concurrent operations
//
// # Type Signatures
//
// The idiomatic packages use function types that work naturally with Go tuples:
//
// For option package:
//
// Operator[A, B any] = func(A, bool) (B, bool) // Transform Option[A] to Option[B]
// Kleisli[A, B any] = func(A) (B, bool) // Monadic function from A to Option[B]
//
// For result package:
//
// Operator[A, B any] = func(A, error) (B, error) // Transform Result[A] to Result[B]
// Kleisli[A, B any] = func(A) (B, error) // Monadic function from A to Result[B]
//
// For ioresult package:
//
// IOResult[A any] = func() (A, error) // IO operation returning A or error
// Operator[A, B any] = func(IOResult[A]) IOResult[B] // Transform IOResult[A] to IOResult[B]
// Kleisli[A, B any] = func(A) IOResult[B] // Monadic function from A to IOResult[B]
//
// # When to Use Idiomatic vs Standard Packages
//
// Use idiomatic packages when:
// - Performance is critical (hot paths, tight loops)
// - You want zero-allocation functional patterns
// - You prefer Go's native error handling style
// - You're integrating with existing Go code that uses tuples
// - Memory efficiency matters (embedded systems, high-scale services)
// - You need IO operations with error handling (use ioresult)
//
// Use standard packages when:
// - You need full algebraic data type semantics
// - You're porting code from other FP languages
// - You want explicit Either[E, A] with custom error types
// - You need the complete suite of FP abstractions
// - Code clarity outweighs performance concerns
//
// # Choosing Between result and ioresult
//
// Use result when:
// - Operations are pure (same input always produces same output)
// - No side effects are involved (no IO, no state mutation)
// - You want to represent success/failure without execution delay
//
// Use ioresult when:
// - Operations perform IO (file system, network, database)
// - Side effects are part of the computation
// - You need lazy evaluation (defer execution until needed)
// - You want to compose IO operations that may fail
// - Resource management is required (files, connections, locks)
//
// # Performance Comparison
//
// Benchmark results comparing idiomatic vs standard packages (examples):
//
// Operation Standard Idiomatic Improvement
// --------- -------- --------- -----------
// Right/Some 3.2 ns/op 0.5 ns/op 6.4x faster
// Left/None 3.5 ns/op 0.5 ns/op 7.0x faster
// Map (Right/Some) 5.8 ns/op 1.2 ns/op 4.8x faster
// Map (Left/None) 3.8 ns/op 1.0 ns/op 3.8x faster
// Chain (success) 8.2 ns/op 2.1 ns/op 3.9x faster
// Fold 6.5 ns/op 1.8 ns/op 3.6x faster
//
// Memory allocations:
//
// Operation Standard Idiomatic
// --------- -------- ---------
// Right/Some 16 B/op 0 B/op
// Map 16 B/op 0 B/op
// Chain 32 B/op 0 B/op
//
// # Interoperability
//
// The idiomatic packages provide conversion functions for working with standard packages:
//
// // Converting between idiomatic.option and standard option
// import (
// stdOption "github.com/IBM/fp-go/v2/option"
// "github.com/IBM/fp-go/v2/idiomatic/option"
// )
//
// // Standard to idiomatic (conceptually - check actual API)
// stdOpt := stdOption.Some(42)
// idiomaticOpt := stdOption.Unwrap(stdOpt) // Returns (42, true)
//
// // Idiomatic to standard (conceptually - check actual API)
// value, ok := option.Some(42)
// stdOpt = stdOption.FromTuple(value, ok)
//
// // Converting between idiomatic.result and standard result
// import (
// stdResult "github.com/IBM/fp-go/v2/result"
// "github.com/IBM/fp-go/v2/idiomatic/result"
// )
//
// // The conversion is straightforward with Unwrap/UnwrapError
// stdRes := stdResult.Right[error](42)
// value, err := stdResult.UnwrapError(stdRes) // (42, nil)
//
// // And back
// stdRes = stdResult.TryCatchError(value, err)
//
// # Common Patterns
//
// ## Pipeline Composition
//
// Build complex data transformations using function composition:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/idiomatic/result"
// )
//
// output, err := F.Pipe3(
// parseInput(input),
// result.Map(validate),
// result.Chain(process),
// result.Map(format),
// )
//
// ## IO Pipeline with IOResult
//
// Compose IO operations that may fail:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/idiomatic/ioresult"
// )
//
// // Define IO operations
// readFile := func(path string) ioresult.IOResult[[]byte] {
// return func() ([]byte, error) {
// return os.ReadFile(path)
// }
// }
//
// parseJSON := func(data []byte) ioresult.IOResult[Config] {
// return func() (Config, error) {
// var cfg Config
// err := json.Unmarshal(data, &cfg)
// return cfg, err
// }
// }
//
// // Compose operations (not executed yet)
// loadConfig := F.Pipe1(
// readFile("config.json"),
// ioresult.Chain(parseJSON),
// ioresult.Map(validateConfig),
// )
//
// // Execute the IO pipeline
// config, err := loadConfig()
// if err != nil {
// log.Fatal(err)
// }
//
// ## Error Accumulation with Validation
//
// The idiomatic/result package supports validation patterns for accumulating multiple errors:
//
// import "github.com/IBM/fp-go/v2/idiomatic/result"
//
// results := []error{
// validate1(input),
// validate2(input),
// validate3(input),
// }
// allErrors := result.ValidationErrors(results)
//
// ## Working with Collections
//
// Transform arrays while handling errors or missing values:
//
// import "github.com/IBM/fp-go/v2/idiomatic/option"
//
// // Transform array, short-circuit on first None
// input := []int{1, 2, 3}
// output, ok := option.TraverseArray(func(x int) (int, bool) {
// if x > 0 { return x * 2, true }
// return 0, false
// })(input) // ([2, 4, 6], true)
//
// import "github.com/IBM/fp-go/v2/idiomatic/result"
//
// // Transform array, short-circuit on first error
// output, err := result.TraverseArray(func(x int) (int, error) {
// if x > 0 { return x * 2, nil }
// return 0, errors.New("invalid")
// })(input) // ([2, 4, 6], nil)
//
// # Integration with Standard Library
//
// The idiomatic packages integrate seamlessly with Go's standard library:
//
// // File operations with result
// readFile := result.Chain(func(path string) ([]byte, error) {
// return os.ReadFile(path)
// })
// content, err := readFile("config.json", nil)
//
// // HTTP requests with result
// import "github.com/IBM/fp-go/v2/idiomatic/result/http"
//
// resp, err := http.MakeRequest(http.GET, "https://api.example.com/data")
//
// // Database queries with option
// findUser := func(id int) (User, bool) {
// user, err := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
// if err != nil {
// return User{}, false
// }
// return user, true
// }
//
// // File operations with IOResult and resource safety
// import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
//
// processFile := ioresult.Bracket(
// // Acquire resource
// func() (*os.File, error) {
// return os.Open("data.txt")
// },
// // Release resource (always called)
// func(f *os.File, err error) ioresult.IOResult[any] {
// return func() (any, error) {
// return nil, f.Close()
// }
// },
// // Use resource
// func(f *os.File) ioresult.IOResult[string] {
// return func() (string, error) {
// data, err := io.ReadAll(f)
// return string(data), err
// }
// },
// )
// content, err := processFile()
//
// // System command execution with IOResult
// import ioexec "github.com/IBM/fp-go/v2/idiomatic/ioresult/exec"
//
// version := F.Pipe1(
// ioexec.Command("git")([]string{"version"})([]byte{}),
// ioresult.Map(func(output exec.CommandOutput) string {
// return string(exec.StdOut(output))
// }),
// )
// result, err := version()
//
// # Best Practices
//
// 1. Use descriptive error messages:
//
// result.Left[User](fmt.Errorf("user %d not found", id))
//
// 2. Prefer composition over complex logic:
//
// F.Pipe3(input,
// result.Map(step1),
// result.Chain(step2),
// result.Map(step3),
// )
//
// 3. Use Fold for final value extraction:
//
// output := result.Fold(
// func(err error) Response { return ErrorResponse(err) },
// func(data Data) Response { return SuccessResponse(data) },
// )(result)
//
// 4. Leverage GetOrElse for defaults:
//
// value := option.GetOrElse(func() Config { return defaultConfig })(maybeConfig)
//
// 5. Use FromPredicate for validation:
//
// positiveInt := result.FromPredicate(
// func(x int) bool { return x > 0 },
// func(x int) error { return fmt.Errorf("%d is not positive", x) },
// )
//
// # Testing
//
// Testing code using idiomatic packages is straightforward:
//
// func TestTransformation(t *testing.T) {
// input := 21
// result, err := F.Pipe2(
// input,
// result.Right[int],
// result.Map(func(x int) int { return x * 2 }),
// )
// assert.NoError(t, err)
// assert.Equal(t, 42, result)
// }
//
// func TestOptionHandling(t *testing.T) {
// value, ok := F.Pipe2(
// 42,
// option.Some[int],
// option.Map(func(x int) int { return x * 2 }),
// )
// assert.True(t, ok)
// assert.Equal(t, 84, value)
// }
//
// # Resources
//
// For more information on functional programming patterns in Go:
// - fp-go documentation: https://github.com/IBM/fp-go
// - Standard option package: github.com/IBM/fp-go/v2/option
// - Standard either package: github.com/IBM/fp-go/v2/either
// - Standard result package: github.com/IBM/fp-go/v2/result
// - Standard ioeither package: github.com/IBM/fp-go/v2/ioeither
//
// See the subpackage documentation for detailed API references:
// - idiomatic/option: Option monad using (value, bool) tuples
// - idiomatic/result: Result/Either monad using (value, error) tuples
// - idiomatic/ioresult: IOResult monad using func() (value, error) for IO operations
package idiomatic

View File

@@ -0,0 +1,196 @@
# IOResult Benchmark Results
Performance benchmarks for the `idiomatic/ioresult` package.
**Test Environment:**
- CPU: AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics
- OS: Windows
- Architecture: amd64
- Go version: go1.23+
## Summary
The `idiomatic/ioresult` package demonstrates exceptional performance with **zero allocations** for most core operations. The package achieves sub-nanosecond performance for basic operations like `Of`, `Map`, and `Chain`.
## Core Operations
### Basic Construction
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **Of** (Success) | 0.22 | 0 | 0 |
| **Left** (Error) | 0.22 | 0 | 0 |
| **FromIO** | 0.48 | 0 | 0 |
**Analysis:** Creating IOResult values has effectively zero overhead with no allocations.
### Functor Operations
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **Map** (Success) | 0.22 | 0 | 0 |
| **Map** (Error) | 0.22 | 0 | 0 |
| **Functor.Map** | 133.30 | 80 | 3 |
**Analysis:** Direct `Map` operation has zero overhead. Using the `Functor` interface adds some allocation overhead due to interface wrapping.
### Monad Operations
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **Chain** (Success) | 0.21 | 0 | 0 |
| **Chain** (Error) | 0.22 | 0 | 0 |
| **Monad.Chain** | 317.70 | 104 | 4 |
| **Pointed.Of** | 35.32 | 24 | 1 |
**Analysis:** Direct monad operations are extremely fast. Using the `Monad` interface adds overhead but is still performant for real-world use.
### Applicative Operations
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **ApFirst** | 41.02 | 48 | 2 |
| **ApSecond** | 92.43 | 104 | 4 |
| **MonadApFirst** | 96.61 | 80 | 3 |
| **MonadApSecond** | 216.50 | 104 | 4 |
**Analysis:** Applicative operations involve parallel execution and have modest allocation overhead.
### Error Handling
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **Alt** | 0.55 | 0 | 0 |
| **GetOrElse** | 0.62 | 0 | 0 |
| **Fold** | 168.20 | 128 | 4 |
**Analysis:** Error recovery operations like `Alt` and `GetOrElse` are extremely efficient. `Fold` has overhead due to wrapping both branches in IO.
### Chain Operations
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **ChainIOK** | 215.30 | 128 | 5 |
| **ChainFirst** | 239.90 | 128 | 5 |
**Analysis:** Specialized chain operations have predictable allocation patterns.
## Pipeline Performance
### Simple Pipelines
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **Pipeline** (3 Maps) | 0.87 | 0 | 0 |
| **ChainSequence** (3 Chains) | 0.95 | 0 | 0 |
**Analysis:** Composing operations through pipes has zero allocation overhead. The cost is purely computational.
### Execution Performance
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **Execute** (Simple) | 0.22 | 0 | 0 |
| **ExecutePipeline** (3 Maps) | 5.67 | 0 | 0 |
**Analysis:** Executing IOResult operations is very fast. Even complex pipelines execute in nanoseconds.
## Collection Operations
| Operation | ns/op | B/op | allocs/op | Items |
|-----------|-------|------|-----------|-------|
| **TraverseArray** | 1,883 | 1,592 | 59 | 10 |
| **SequenceArray** | 1,051 | 808 | 30 | 5 |
**Analysis:** Collection operations have O(n) allocation behavior. Performance scales linearly with input size.
## Advanced Patterns
### Bind Operations
| Operation | ns/op | B/op | allocs/op |
|-----------|-------|------|-----------|
| **Bind** | 167.40 | 184 | 7 |
| **Bind** (with allocation tracking) | 616.10 | 336 | 13 |
| **DirectChainMap** | 82.42 | 48 | 2 |
**Analysis:** `Bind` provides do-notation convenience at a modest performance cost. Direct chaining is more efficient when performance is critical.
### Pattern Performance
#### Map Patterns
| Pattern | ns/op | B/op | allocs/op |
|---------|-------|------|-----------|
| SimpleFunction | 0.55 | 0 | 0 |
| InlinedLambda | 0.69 | 0 | 0 |
| NestedMaps (3x) | 10.54 | 0 | 0 |
#### Of Patterns
| Pattern | ns/op | B/op | allocs/op |
|---------|-------|------|-----------|
| IntValue | 0.44 | 0 | 0 |
| StructValue | 0.43 | 0 | 0 |
| PointerValue | 0.46 | 0 | 0 |
#### Chain Patterns
| Pattern | ns/op | B/op | allocs/op |
|---------|-------|------|-----------|
| SimpleChain | 0.46 | 0 | 0 |
| ChainSequence (5x) | 47.75 | 24 | 1 |
### Error Path Performance
| Path | ns/op | B/op | allocs/op |
|------|-------|------|-----------|
| **SuccessPath** | 0.91 | 0 | 0 |
| **ErrorPath** | 1.44 | 0 | 0 |
**Analysis:** Error paths are slightly slower than success paths but still sub-nanosecond. Both paths have zero allocations.
## Performance Characteristics
### Zero-Allocation Operations
The following operations have **zero heap allocations**:
- `Of`, `Left` (construction)
- `Map`, `Chain` (transformation)
- `Alt`, `GetOrElse` (error recovery)
- `FromIO` (conversion)
- Pipeline composition
- Execution of simple operations
### Low-Allocation Operations
The following operations have minimal allocations:
- Interface-based operations (Functor, Monad, Pointed): 1-4 allocations
- Applicative operations (ApFirst, ApSecond): 2-4 allocations
- Collection operations: O(n) allocations based on input size
### Optimization Opportunities
1. **Prefer direct functions over interfaces**: Using `Map` directly is ~600x faster than `Functor.Map` due to interface overhead
2. **Avoid unnecessary Bind**: Direct chaining with `Chain` and `Map` is 7-10x faster than `Bind`
3. **Use parallel operations judiciously**: ApFirst/ApSecond have overhead; only use when parallelism is beneficial
4. **Batch collection operations**: TraverseArray is efficient for bulk operations rather than multiple individual operations
## Comparison with Standard IOEither
The idiomatic implementation aims for:
- **Zero allocations** for basic operations (vs 1-2 allocations in standard)
- **Sub-nanosecond** performance for core operations
- **Native Go idioms** using (value, error) tuples
## Conclusions
The `idiomatic/ioresult` package delivers exceptional performance:
1. **Ultra-fast core operations**: Most operations complete in sub-nanosecond time
2. **Zero-allocation design**: Core operations don't allocate memory
3. **Predictable performance**: Overhead is consistent and measurable
4. **Scalable**: Collection operations scale linearly
5. **Production-ready**: Performance characteristics suitable for high-throughput systems
The package successfully provides functional programming abstractions with minimal runtime overhead, making it suitable for performance-critical applications while maintaining composability and type safety.
---
*Benchmarks run on: 2025-11-19*
*Command: `go test -bench=. -benchmem -benchtime=1s`*

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"github.com/IBM/fp-go/v2/internal/apply"
)
// MonadApFirst combines two effectful actions, keeping only the result of the first.
//
//go:inline
func MonadApFirst[A, B any](first IOResult[A], second IOResult[B]) IOResult[A] {
return apply.MonadApFirst(
MonadAp[A, B],
MonadMap[A, func(B) A],
first,
second,
)
}
// ApFirst combines two effectful actions, keeping only the result of the first.
//
//go:inline
func ApFirst[A, B any](second IOResult[B]) Operator[A, A] {
return apply.ApFirst(
Ap[A, B],
Map[A, func(B) A],
second,
)
}
// MonadApSecond combines two effectful actions, keeping only the result of the second.
//
//go:inline
func MonadApSecond[A, B any](first IOResult[A], second IOResult[B]) IOResult[B] {
return apply.MonadApSecond(
MonadAp[B, B],
MonadMap[A, func(B) B],
first,
second,
)
}
// ApSecond combines two effectful actions, keeping only the result of the second.
//
//go:inline
func ApSecond[A, B any](second IOResult[B]) Operator[A, B] {
return apply.ApSecond(
Ap[B, B],
Map[A, func(B) B],
second,
)
}

View File

@@ -0,0 +1,430 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestMonadApFirst(t *testing.T) {
t.Run("Both Right - keeps first value", func(t *testing.T) {
first := Of("first")
second := Of("second")
result := MonadApFirst(first, second)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "first", val)
})
t.Run("First Right, Second Left - returns second's error", func(t *testing.T) {
first := Of(42)
second := Left[string](errors.New("second error"))
result := MonadApFirst(first, second)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "second error", err.Error())
})
t.Run("First Left, Second Right - returns first's error", func(t *testing.T) {
first := Left[int](errors.New("first error"))
second := Of("success")
result := MonadApFirst(first, second)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "first error", err.Error())
})
t.Run("Both Left - returns first error", func(t *testing.T) {
first := Left[int](errors.New("first error"))
second := Left[string](errors.New("second error"))
result := MonadApFirst(first, second)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "first error", err.Error())
})
t.Run("Different types", func(t *testing.T) {
first := Of(123)
second := Of("text")
result := MonadApFirst(first, second)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 123, val)
})
t.Run("Side effects execute for both", func(t *testing.T) {
firstCalled := false
secondCalled := false
first := func() (int, error) {
firstCalled = true
return 1, nil
}
second := func() (string, error) {
secondCalled = true
return "test", nil
}
result := MonadApFirst(first, second)
val, err := result()
assert.True(t, firstCalled)
assert.True(t, secondCalled)
assert.NoError(t, err)
assert.Equal(t, 1, val)
})
}
func TestApFirst(t *testing.T) {
t.Run("Both Right - keeps first value", func(t *testing.T) {
result := F.Pipe1(
Of("first"),
ApFirst[string](Of("second")),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "first", val)
})
t.Run("First Right, Second Left - returns error", func(t *testing.T) {
result := F.Pipe1(
Of(100),
ApFirst[int](Left[string](errors.New("error"))),
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "error", err.Error())
})
t.Run("First Left, Second Right - returns error", func(t *testing.T) {
result := F.Pipe1(
Left[int](errors.New("first error")),
ApFirst[int](Of("success")),
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "first error", err.Error())
})
t.Run("Chain multiple ApFirst", func(t *testing.T) {
result := F.Pipe2(
Of("value"),
ApFirst[string](Of(1)),
ApFirst[string](Of(true)),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "value", val)
})
t.Run("With Map composition", func(t *testing.T) {
result := F.Pipe2(
Of(5),
ApFirst[int](Of("ignored")),
Map(func(x int) int { return x * 2 }),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 10, val)
})
t.Run("Side effect in second executes", func(t *testing.T) {
effectExecuted := false
second := func() (string, error) {
effectExecuted = true
return "effect", nil
}
result := F.Pipe1(
Of(42),
ApFirst[int](second),
)
val, err := result()
assert.True(t, effectExecuted)
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
}
func TestMonadApSecond(t *testing.T) {
t.Run("Both Right - keeps second value", func(t *testing.T) {
first := Of("first")
second := Of("second")
result := MonadApSecond(first, second)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "second", val)
})
t.Run("First Right, Second Left - returns second's error", func(t *testing.T) {
first := Of(42)
second := Left[string](errors.New("second error"))
result := MonadApSecond(first, second)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "second error", err.Error())
})
t.Run("First Left, Second Right - returns first's error", func(t *testing.T) {
first := Left[int](errors.New("first error"))
second := Of("success")
result := MonadApSecond(first, second)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "first error", err.Error())
})
t.Run("Both Left - returns first error", func(t *testing.T) {
first := Left[int](errors.New("first error"))
second := Left[string](errors.New("second error"))
result := MonadApSecond(first, second)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "first error", err.Error())
})
t.Run("Different types", func(t *testing.T) {
first := Of(123)
second := Of("text")
result := MonadApSecond(first, second)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "text", val)
})
t.Run("Side effects execute for both", func(t *testing.T) {
firstCalled := false
secondCalled := false
first := func() (int, error) {
firstCalled = true
return 1, nil
}
second := func() (string, error) {
secondCalled = true
return "test", nil
}
result := MonadApSecond(first, second)
val, err := result()
assert.True(t, firstCalled)
assert.True(t, secondCalled)
assert.NoError(t, err)
assert.Equal(t, "test", val)
})
}
func TestApSecond(t *testing.T) {
t.Run("Both Right - keeps second value", func(t *testing.T) {
result := F.Pipe1(
Of("first"),
ApSecond[string](Of("second")),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "second", val)
})
t.Run("First Right, Second Left - returns error", func(t *testing.T) {
result := F.Pipe1(
Of(100),
ApSecond[int](Left[string](errors.New("error"))),
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "error", err.Error())
})
t.Run("First Left, Second Right - returns error", func(t *testing.T) {
result := F.Pipe1(
Left[int](errors.New("first error")),
ApSecond[int](Of("success")),
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "first error", err.Error())
})
t.Run("Chain multiple ApSecond", func(t *testing.T) {
result := F.Pipe2(
Of("initial"),
ApSecond[string](Of("middle")),
ApSecond[string](Of("final")),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "final", val)
})
t.Run("With Map composition", func(t *testing.T) {
result := F.Pipe2(
Of(1),
ApSecond[int](Of(5)),
Map(func(x int) int { return x * 2 }),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 10, val)
})
t.Run("Side effect in first executes", func(t *testing.T) {
effectExecuted := false
first := func() (int, error) {
effectExecuted = true
return 1, nil
}
result := F.Pipe1(
first,
ApSecond[int](Of("result")),
)
val, err := result()
assert.True(t, effectExecuted)
assert.NoError(t, err)
assert.Equal(t, "result", val)
})
}
func TestApFirstApSecondInteraction(t *testing.T) {
t.Run("ApFirst then ApSecond", func(t *testing.T) {
// ApFirst keeps "first", then ApSecond discards it for "second"
result := F.Pipe2(
Of("first"),
ApFirst[string](Of("middle")),
ApSecond[string](Of("second")),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "second", val)
})
t.Run("ApSecond then ApFirst", func(t *testing.T) {
// ApSecond picks "middle", then ApFirst keeps that over "ignored"
result := F.Pipe2(
Of("first"),
ApSecond[string](Of("middle")),
ApFirst[string](Of("ignored")),
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "middle", val)
})
t.Run("Error propagation in chain", func(t *testing.T) {
result := F.Pipe2(
Of("first"),
ApFirst[string](Left[int](errors.New("error in middle"))),
ApSecond[string](Of("second")),
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "error in middle", err.Error())
})
}
func TestApSequencingBehavior(t *testing.T) {
t.Run("MonadApFirst executes both operations", func(t *testing.T) {
executionOrder := make([]string, 2)
first := func() (int, error) {
executionOrder[0] = "first"
return 1, nil
}
second := func() (string, error) {
executionOrder[1] = "second"
return "test", nil
}
result := MonadApFirst(first, second)
_, err := result()
assert.NoError(t, err)
// Note: execution order is second then first due to applicative implementation
assert.Len(t, executionOrder, 2)
assert.Contains(t, executionOrder, "first")
assert.Contains(t, executionOrder, "second")
})
t.Run("MonadApSecond executes both operations", func(t *testing.T) {
executionOrder := make([]string, 2)
first := func() (int, error) {
executionOrder[0] = "first"
return 1, nil
}
second := func() (string, error) {
executionOrder[1] = "second"
return "test", nil
}
result := MonadApSecond(first, second)
_, err := result()
assert.NoError(t, err)
// Note: execution order is second then first due to applicative implementation
assert.Len(t, executionOrder, 2)
assert.Contains(t, executionOrder, "first")
assert.Contains(t, executionOrder, "second")
})
t.Run("Error in first stops second from affecting result in MonadApFirst", func(t *testing.T) {
secondExecuted := false
first := Left[int](errors.New("first error"))
second := func() (string, error) {
secondExecuted = true
return "test", nil
}
result := MonadApFirst(first, second)
_, err := result()
// Second still executes but error is from first
assert.True(t, secondExecuted)
assert.Error(t, err)
assert.Equal(t, "first error", err.Error())
})
}

View File

@@ -0,0 +1,50 @@
2025/11/19 15:31:45 Data:
{
"key": "key",
"value": "value"
}
2025/11/19 15:31:45 t1: foo, t2: 1, t3: foo1
goos: windows
goarch: amd64
pkg: github.com/IBM/fp-go/v2/idiomatic/ioresult
cpu: AMD Ryzen 7 PRO 7840U w/ Radeon 780M Graphics
BenchmarkOf-16 1000000000 0.2241 ns/op 0 B/op 0 allocs/op
BenchmarkMap-16 1000000000 0.2151 ns/op 0 B/op 0 allocs/op
BenchmarkChain-16 1000000000 0.2100 ns/op 0 B/op 0 allocs/op
BenchmarkBind-16 7764834 167.4 ns/op 184 B/op 7 allocs/op
BenchmarkPipeline-16 1000000000 0.8716 ns/op 0 B/op 0 allocs/op
BenchmarkExecute-16 1000000000 0.2231 ns/op 0 B/op 0 allocs/op
BenchmarkExecutePipeline-16 231766047 5.671 ns/op 0 B/op 0 allocs/op
BenchmarkChainSequence-16 1000000000 0.9532 ns/op 0 B/op 0 allocs/op
BenchmarkLeft-16 1000000000 0.2233 ns/op 0 B/op 0 allocs/op
BenchmarkMapWithError-16 1000000000 0.2172 ns/op 0 B/op 0 allocs/op
BenchmarkChainWithError-16 1000000000 0.2151 ns/op 0 B/op 0 allocs/op
BenchmarkApFirst-16 31478382 41.02 ns/op 48 B/op 2 allocs/op
BenchmarkApSecond-16 13940001 92.43 ns/op 104 B/op 4 allocs/op
BenchmarkMonadApFirst-16 18065812 96.61 ns/op 80 B/op 3 allocs/op
BenchmarkMonadApSecond-16 8618354 216.5 ns/op 104 B/op 4 allocs/op
BenchmarkFunctor-16 9963952 133.3 ns/op 80 B/op 3 allocs/op
BenchmarkMonad-16 7325534 317.7 ns/op 104 B/op 4 allocs/op
BenchmarkPointed-16 30711267 35.32 ns/op 24 B/op 1 allocs/op
BenchmarkTraverseArray-16 652627 1883 ns/op 1592 B/op 59 allocs/op
BenchmarkSequenceArray-16 1000000 1051 ns/op 808 B/op 30 allocs/op
BenchmarkAlt-16 1000000000 0.5538 ns/op 0 B/op 0 allocs/op
BenchmarkGetOrElse-16 1000000000 0.6245 ns/op 0 B/op 0 allocs/op
BenchmarkFold-16 7670691 168.2 ns/op 128 B/op 4 allocs/op
BenchmarkFromIO-16 1000000000 0.4832 ns/op 0 B/op 0 allocs/op
BenchmarkChainIOK-16 5704785 215.3 ns/op 128 B/op 5 allocs/op
BenchmarkChainFirst-16 4841002 239.9 ns/op 128 B/op 5 allocs/op
BenchmarkBindAllocations/Bind-16 1988592 616.1 ns/op 336 B/op 13 allocs/op
BenchmarkBindAllocations/DirectChainMap-16 14671629 82.42 ns/op 48 B/op 2 allocs/op
BenchmarkMapPatterns/SimpleFunction-16 1000000000 0.5509 ns/op 0 B/op 0 allocs/op
BenchmarkMapPatterns/InlinedLambda-16 1000000000 0.6880 ns/op 0 B/op 0 allocs/op
BenchmarkMapPatterns/NestedMaps-16 100000000 10.54 ns/op 0 B/op 0 allocs/op
BenchmarkOfPatterns/IntValue-16 1000000000 0.4405 ns/op 0 B/op 0 allocs/op
BenchmarkOfPatterns/StructValue-16 1000000000 0.4252 ns/op 0 B/op 0 allocs/op
BenchmarkOfPatterns/PointerValue-16 1000000000 0.4587 ns/op 0 B/op 0 allocs/op
BenchmarkChainPatterns/SimpleChain-16 1000000000 0.4593 ns/op 0 B/op 0 allocs/op
BenchmarkChainPatterns/ChainSequence-16 25868368 47.75 ns/op 24 B/op 1 allocs/op
BenchmarkErrorPaths/SuccessPath-16 1000000000 0.9135 ns/op 0 B/op 0 allocs/op
BenchmarkErrorPaths/ErrorPath-16 787269846 1.438 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/IBM/fp-go/v2/idiomatic/ioresult 44.835s

View File

@@ -0,0 +1,301 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
)
func BenchmarkOf(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Of(42)
}
}
func BenchmarkMap(b *testing.B) {
io := Of(42)
f := func(x int) int { return x * 2 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Map(f)(io)
}
}
func BenchmarkChain(b *testing.B) {
io := Of(42)
f := func(x int) IOResult[int] { return Of(x * 2) }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Chain(f)(io)
}
}
func BenchmarkBind(b *testing.B) {
type Data struct {
Value int
}
io := Of(Data{Value: 0})
f := func(d Data) IOResult[int] { return Of(d.Value * 2) }
setter := func(v int) func(Data) Data {
return func(d Data) Data {
d.Value = v
return d
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Bind(setter, f)(io)
}
}
func BenchmarkPipeline(b *testing.B) {
f1 := func(x int) int { return x + 1 }
f2 := func(x int) int { return x * 2 }
f3 := func(x int) int { return x - 3 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = F.Pipe3(
Of(42),
Map(f1),
Map(f2),
Map(f3),
)
}
}
func BenchmarkExecute(b *testing.B) {
io := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = io()
}
}
func BenchmarkExecutePipeline(b *testing.B) {
f1 := func(x int) int { return x + 1 }
f2 := func(x int) int { return x * 2 }
f3 := func(x int) int { return x - 3 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
io := F.Pipe3(
Of(42),
Map(f1),
Map(f2),
Map(f3),
)
_, _ = io()
}
}
func BenchmarkChainSequence(b *testing.B) {
f1 := func(x int) IOResult[int] { return Of(x + 1) }
f2 := func(x int) IOResult[int] { return Of(x * 2) }
f3 := func(x int) IOResult[int] { return Of(x - 3) }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = F.Pipe3(
Of(42),
Chain(f1),
Chain(f2),
Chain(f3),
)
}
}
func BenchmarkLeft(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Left[int](F.Constant[error](nil)())
}
}
func BenchmarkMapWithError(b *testing.B) {
io := Left[int](F.Constant[error](nil)())
f := func(x int) int { return x * 2 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Map(f)(io)
}
}
func BenchmarkChainWithError(b *testing.B) {
io := Left[int](F.Constant[error](nil)())
f := func(x int) IOResult[int] { return Of(x * 2) }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Chain(f)(io)
}
}
func BenchmarkApFirst(b *testing.B) {
first := Of(42)
second := Of("test")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ApFirst[int](second)(first)
}
}
func BenchmarkApSecond(b *testing.B) {
first := Of(42)
second := Of("test")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ApSecond[int](second)(first)
}
}
func BenchmarkMonadApFirst(b *testing.B) {
first := Of(42)
second := Of("test")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadApFirst(first, second)
}
}
func BenchmarkMonadApSecond(b *testing.B) {
first := Of(42)
second := Of("test")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadApSecond(first, second)
}
}
func BenchmarkFunctor(b *testing.B) {
functor := Functor[int, int]()
io := Of(42)
f := func(x int) int { return x * 2 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = functor.Map(f)(io)
}
}
func BenchmarkMonad(b *testing.B) {
monad := Monad[int, int]()
f := func(x int) IOResult[int] { return Of(x * 2) }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = monad.Chain(f)(monad.Of(42))
}
}
func BenchmarkPointed(b *testing.B) {
pointed := Pointed[int]()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = pointed.Of(42)
}
}
func BenchmarkTraverseArray(b *testing.B) {
data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
f := func(x int) IOResult[int] { return Of(x * 2) }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = TraverseArray(f)(data)
}
}
func BenchmarkSequenceArray(b *testing.B) {
data := []IOResult[int]{Of(1), Of(2), Of(3), Of(4), Of(5)}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = SequenceArray(data)
}
}
func BenchmarkAlt(b *testing.B) {
first := Left[int](F.Constant[error](nil)())
second := func() IOResult[int] { return Of(42) }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Alt(second)(first)
}
}
func BenchmarkGetOrElse(b *testing.B) {
io := Of(42)
def := func(error) func() int { return func() int { return 0 } }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = GetOrElse(def)(io)()
}
}
func BenchmarkFold(b *testing.B) {
io := Of(42)
onLeft := func(error) func() int { return func() int { return 0 } }
onRight := func(x int) func() int { return func() int { return x } }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Fold(onLeft, onRight)(io)()
}
}
func BenchmarkFromIO(b *testing.B) {
ioVal := func() int { return 42 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = FromIO(ioVal)
}
}
func BenchmarkChainIOK(b *testing.B) {
io := Of(42)
f := func(x int) func() int { return func() int { return x * 2 } }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ChainIOK(f)(io)
}
}
func BenchmarkChainFirst(b *testing.B) {
io := Of(42)
f := func(x int) IOResult[string] { return Of("test") }
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ChainFirst(f)(io)
}
}

View File

@@ -0,0 +1,134 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
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"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do starts a do-notation computation with an initial state.
// This is the entry point for building complex computations using the do-notation style.
//
//go:inline
func Do[S any](
empty S,
) IOResult[S] {
return Of(empty)
}
// Bind adds a computation step in do-notation, extending the state with a new field.
// The setter function determines how the new value is added to the state.
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return chain.Bind(
Chain[S1, S2],
Map[T, S2],
setter,
f,
)
}
// Let adds a pure transformation step in do-notation.
// Unlike Bind, the function does not return an IOResult, making it suitable for pure computations.
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[S1, S2] {
return functor.Let(
Map[S1, S2],
setter,
f,
)
}
// LetTo adds a constant value to the state in do-notation.
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[S1, S2] {
return functor.LetTo(
Map[S1, S2],
setter,
b,
)
}
// BindTo wraps a value in an initial state structure.
// This is typically the first operation after creating an IOResult in do-notation.
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return chain.BindTo(
Map[T, S1],
setter,
)
}
// ApS applies an IOResult to extend the state in do-notation.
// This is used to add independent computations that don't depend on previous results.
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa IOResult[T],
) Operator[S1, S2] {
return apply.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
setter,
fa,
)
}
// ApSL applies an IOResult using a lens to update a specific field in the state.
func ApSL[S, T any](
lens L.Lens[S, T],
fa IOResult[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
}
// BindL binds a computation using a lens to focus on a specific field.
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
}
// LetL applies a pure transformation using a lens to update a specific field.
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[S, S] {
return Let(lens.Set, F.Flow2(lens.Get, f))
}
// LetToL sets a field to a constant value using a lens.
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return LetTo(lens.Set, b)
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/stretchr/testify/assert"
)
func getLastName(s utils.Initial) IOResult[string] {
return Of("Doe")
}
func getGivenName(s utils.WithLastName) IOResult[string] {
return Of("John")
}
func TestBind(t *testing.T) {
res := F.Pipe3(
Do(utils.Empty),
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map(utils.GetFullName),
)
result, err := res()
assert.NoError(t, err)
assert.Equal(t, "John Doe", result)
}
func TestApS(t *testing.T) {
res := F.Pipe3(
Do(utils.Empty),
ApS(utils.SetLastName, Of("Doe")),
ApS(utils.SetGivenName, Of("John")),
Map(utils.GetFullName),
)
result, err := res()
assert.NoError(t, err)
assert.Equal(t, "John Doe", result)
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
// whether the body action returns and error or not.
func Bracket[A, B, ANY any](
acquire IOResult[A],
use Kleisli[A, B],
release func(B, error) func(A) IOResult[ANY],
) IOResult[B] {
return func() (B, error) {
a, aerr := acquire()
if aerr != nil {
return result.Left[B](aerr)
}
b, berr := use(a)()
_, rerr := release(b, berr)(a)()
if berr != nil {
return result.Left[B](berr)
}
if rerr != nil {
return result.Left[B](rerr)
}
return result.Of(b)
}
}

View File

@@ -0,0 +1,302 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBracket(t *testing.T) {
t.Run("successful acquire, use, and release", func(t *testing.T) {
acquired := false
used := false
released := false
acquire := func() (string, error) {
acquired = true
return "resource", nil
}
use := func(r string) IOResult[int] {
return func() (int, error) {
used = true
assert.Equal(t, "resource", r)
return 42, nil
}
}
release := func(b int, err error) func(string) IOResult[any] {
return func(r string) IOResult[any] {
return func() (any, error) {
released = true
assert.Equal(t, "resource", r)
assert.Equal(t, 42, b)
assert.NoError(t, err)
return nil, nil
}
}
}
result := Bracket(acquire, use, release)
val, err := result()
assert.True(t, acquired)
assert.True(t, used)
assert.True(t, released)
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
t.Run("acquire fails - use and release not called", func(t *testing.T) {
used := false
released := false
acquire := func() (string, error) {
return "", errors.New("acquire failed")
}
use := func(r string) IOResult[int] {
return func() (int, error) {
used = true
return 42, nil
}
}
release := func(b int, err error) func(string) IOResult[any] {
return func(r string) IOResult[any] {
return func() (any, error) {
released = true
return nil, nil
}
}
}
result := Bracket(acquire, use, release)
_, err := result()
assert.False(t, used)
assert.False(t, released)
assert.Error(t, err)
assert.Equal(t, "acquire failed", err.Error())
})
t.Run("use fails - release is still called", func(t *testing.T) {
acquired := false
released := false
acquire := func() (string, error) {
acquired = true
return "resource", nil
}
use := func(r string) IOResult[int] {
return func() (int, error) {
return 0, errors.New("use failed")
}
}
release := func(b int, err error) func(string) IOResult[any] {
return func(r string) IOResult[any] {
return func() (any, error) {
released = true
assert.Equal(t, "resource", r)
assert.Equal(t, 0, b)
assert.Error(t, err)
assert.Equal(t, "use failed", err.Error())
return nil, nil
}
}
}
result := Bracket(acquire, use, release)
_, err := result()
assert.True(t, acquired)
assert.True(t, released)
assert.Error(t, err)
assert.Equal(t, "use failed", err.Error())
})
t.Run("use succeeds but release fails", func(t *testing.T) {
acquired := false
used := false
released := false
acquire := func() (string, error) {
acquired = true
return "resource", nil
}
use := func(r string) IOResult[int] {
return func() (int, error) {
used = true
return 42, nil
}
}
release := func(b int, err error) func(string) IOResult[any] {
return func(r string) IOResult[any] {
return func() (any, error) {
released = true
return nil, errors.New("release failed")
}
}
}
result := Bracket(acquire, use, release)
_, err := result()
assert.True(t, acquired)
assert.True(t, used)
assert.True(t, released)
assert.Error(t, err)
assert.Equal(t, "release failed", err.Error())
})
t.Run("both use and release fail - use error is returned", func(t *testing.T) {
acquire := func() (string, error) {
return "resource", nil
}
use := func(r string) IOResult[int] {
return func() (int, error) {
return 0, errors.New("use failed")
}
}
release := func(b int, err error) func(string) IOResult[any] {
return func(r string) IOResult[any] {
return func() (any, error) {
assert.Error(t, err)
assert.Equal(t, "use failed", err.Error())
return nil, errors.New("release failed")
}
}
}
result := Bracket(acquire, use, release)
_, err := result()
assert.Error(t, err)
// use error takes precedence
assert.Equal(t, "use failed", err.Error())
})
t.Run("resource cleanup with file-like resource", func(t *testing.T) {
type File struct {
name string
closed bool
}
var file *File
acquire := func() (*File, error) {
file = &File{name: "test.txt", closed: false}
return file, nil
}
use := func(f *File) IOResult[string] {
return func() (string, error) {
assert.False(t, f.closed)
return "file content", nil
}
}
release := func(content string, err error) func(*File) IOResult[any] {
return func(f *File) IOResult[any] {
return func() (any, error) {
f.closed = true
return nil, nil
}
}
}
result := Bracket(acquire, use, release)
content, err := result()
assert.NoError(t, err)
assert.Equal(t, "file content", content)
assert.True(t, file.closed)
})
t.Run("release receives both value and error from use", func(t *testing.T) {
var receivedValue int
var receivedError error
acquire := func() (string, error) {
return "resource", nil
}
use := func(r string) IOResult[int] {
return func() (int, error) {
return 100, errors.New("use error")
}
}
release := func(b int, err error) func(string) IOResult[any] {
return func(r string) IOResult[any] {
return func() (any, error) {
receivedValue = b
receivedError = err
return nil, nil
}
}
}
result := Bracket(acquire, use, release)
_, _ = result()
assert.Equal(t, 100, receivedValue)
assert.Error(t, receivedError)
assert.Equal(t, "use error", receivedError.Error())
})
t.Run("release receives zero value and nil when use succeeds", func(t *testing.T) {
var receivedValue int
var receivedError error
acquire := func() (string, error) {
return "resource", nil
}
use := func(r string) IOResult[int] {
return func() (int, error) {
return 42, nil
}
}
release := func(b int, err error) func(string) IOResult[any] {
return func(r string) IOResult[any] {
return func() (any, error) {
receivedValue = b
receivedError = err
return nil, nil
}
}
}
result := Bracket(acquire, use, release)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 42, val)
assert.Equal(t, 42, receivedValue)
assert.NoError(t, receivedError)
})
}

View File

@@ -0,0 +1,378 @@
mode: set
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:23.80,31.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:34.59,41.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:44.81,52.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:55.60,62.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:30.15,32.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:39.20,46.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:53.20,59.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:65.20,71.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:77.19,82.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:89.20,96.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:102.18,104.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:110.18,112.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:118.18,120.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:126.18,128.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:26.15,27.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:27.27,29.18 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:29.18,31.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:32.3,34.18 3 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:34.18,36.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.3,37.18 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.18,39.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:40.3,40.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:26.74,27.51 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:27.51,29.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:35.58,37.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:12.70,13.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:13.28,15.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:18.78,19.33 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:19.33,20.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:20.28,22.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:26.90,27.40 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:27.40,28.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:28.28,30.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:34.102,35.47 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:35.47,36.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:36.28,38.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:45.30,50.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:55.30,60.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:65.30,70.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:73.86,78.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:81.89,86.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:89.89,94.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:97.126,98.61 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:98.61,104.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:108.129,109.61 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:109.61,115.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:119.129,120.61 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:120.61,126.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:133.34,140.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:146.34,153.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:159.34,166.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:169.108,175.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:178.111,184.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:187.111,193.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:196.176,197.69 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:197.69,205.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:209.179,210.69 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:210.69,218.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:222.179,223.69 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:223.69,231.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:239.38,248.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:255.38,264.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:271.38,280.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:283.130,290.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:293.133,300.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:303.133,310.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:313.226,314.77 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:314.77,324.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:328.229,329.77 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:329.77,339.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:343.229,344.77 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:344.77,354.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:363.42,374.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:382.42,393.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:401.42,412.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:415.152,423.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:426.155,434.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:437.155,445.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:448.276,449.85 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:449.85,461.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:465.279,466.85 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:466.85,478.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:482.279,483.85 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:483.85,495.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:505.46,518.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:527.46,540.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:549.46,562.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:565.174,574.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:577.177,586.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:589.177,598.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:601.326,602.93 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:602.93,616.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:620.329,621.93 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:621.93,635.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:639.329,640.93 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:640.93,654.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:665.50,680.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:690.50,705.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:715.50,730.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:733.196,743.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:746.199,756.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:759.199,769.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:772.376,773.101 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:773.101,789.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:793.379,794.101 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:794.101,810.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:814.379,815.101 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:815.101,831.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:843.54,860.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:871.54,888.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:899.54,916.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:919.218,930.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:933.221,944.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:947.221,958.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:961.426,962.109 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:962.109,980.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:984.429,985.109 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:985.109,1003.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1007.429,1008.109 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1008.109,1026.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1039.58,1058.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1070.58,1089.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1101.58,1120.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1123.240,1135.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1138.243,1150.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1153.243,1165.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1168.476,1169.117 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1169.117,1189.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1193.479,1194.117 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1194.117,1214.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1218.479,1219.117 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1219.117,1239.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1253.62,1274.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1287.62,1308.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1321.62,1342.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1345.262,1358.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1361.265,1374.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1377.265,1390.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1393.526,1394.125 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1394.125,1416.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1420.529,1421.125 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1421.125,1443.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1447.529,1448.125 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1448.125,1470.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1485.68,1508.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1522.68,1545.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1559.68,1582.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1585.290,1599.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1602.293,1616.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1619.293,1633.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1636.588,1637.137 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1637.137,1661.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1665.591,1666.137 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1666.137,1690.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1694.591,1695.137 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1695.137,1719.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:32.39,33.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:33.27,35.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:40.36,41.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:41.27,43.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:47.33,49.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:52.38,54.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:58.46,59.31 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:59.31,62.3 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:67.43,68.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:68.27,70.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:75.49,76.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:76.27,78.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:83.52,84.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:84.27,86.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:92.70,93.40 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:93.40,94.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:94.28,95.10 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:95.10,97.5 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:98.4,98.35 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:105.88,106.50 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:106.50,107.42 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:107.42,108.29 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:108.29,110.19 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:110.19,112.6 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:113.5,114.11 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:114.11,116.6 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:117.5,117.36 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:125.78,126.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:126.27,128.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:128.17,130.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:131.3,131.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:136.60,138.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:141.61,143.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:146.42,148.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:151.46,153.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:157.66,158.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:158.27,160.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:160.17,162.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:163.3,163.25 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:169.48,171.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:174.60,176.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:179.42,181.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:185.72,186.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:186.27,188.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:188.17,190.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:191.3,191.16 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:198.54,200.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:204.93,205.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:205.27,207.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:207.17,209.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:210.3,210.29 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:215.75,217.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:221.86,222.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:222.27,224.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:224.17,226.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:227.3,227.14 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:232.68,234.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:237.77,239.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:242.58,244.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:248.80,249.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:249.27,256.13 5 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:256.13,259.4 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:261.3,264.20 3 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:264.20,266.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.3,267.19 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.19,269.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:271.3,271.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:276.61,278.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:282.80,283.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:283.27,285.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:285.17,287.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:289.3,290.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:290.17,292.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:294.3,294.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:299.76,301.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:304.60,306.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:310.49,316.16 4 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:316.16,318.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.2,320.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.27,323.3 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:328.77,329.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:329.27,331.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:331.17,333.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:334.3,334.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:339.59,341.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:344.91,345.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:345.27,347.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:347.17,349.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:350.3,350.25 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:355.73,357.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:360.97,362.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:366.73,367.36 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:367.36,368.19 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:368.19,370.18 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:370.18,372.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:373.4,373.12 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:379.73,381.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:384.55,386.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:390.77,391.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:391.27,393.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:393.17,395.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:396.3,397.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:397.17,399.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:400.3,400.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:405.70,407.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:410.59,412.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:415.52,417.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:419.98,420.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:420.27,422.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:422.17,424.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:425.3,426.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:426.17,428.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:429.3,429.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:434.80,436.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:438.91,439.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:439.27,441.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:441.17,443.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:444.3,445.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:445.17,447.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:448.3,448.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:453.73,455.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:458.83,459.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:459.27,461.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:461.17,463.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:464.3,465.22 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:470.65,472.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:475.91,477.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:480.73,482.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:485.84,487.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:490.66,492.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:495.76,497.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:500.58,502.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:506.100,507.18 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:507.18,509.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:509.17,511.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:512.3,512.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:523.29,524.43 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:524.43,525.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:525.28,527.19 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:527.19,529.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:530.4,532.19 3 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:532.19,534.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.4,535.19 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.19,537.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:538.4,538.23 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:545.41,546.29 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:546.29,549.3 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:554.54,555.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:555.27,557.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:562.79,563.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:563.27,565.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:565.17,567.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:568.3,568.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:573.58,575.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:578.68,580.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:583.49,585.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:590.55,591.42 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:591.42,592.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:592.28,595.4 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:602.55,603.42 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:603.42,604.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:604.28,607.33 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:607.33,609.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:610.4,610.15 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:617.77,618.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:618.27,620.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:620.17,622.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:623.3,623.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:628.59,630.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:634.85,635.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:635.27,637.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:637.17,640.4 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:641.3,641.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:646.78,648.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:650.67,652.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:655.60,657.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:27.52,28.33 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:28.33,32.30 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:32.30,33.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:33.22,35.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:36.4,37.28 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:33.50,35.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:38.51,40.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:43.63,45.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:48.69,50.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:53.73,55.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:58.65,60.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:65.55,67.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:72.74,74.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:79.89,81.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:31.13,38.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:45.13,52.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:59.13,66.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:31.15,32.73 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:32.73,33.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:33.27,35.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:37.26,39.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.2,40.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.27,42.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/semigroup.go:29.41,33.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:25.66,26.42 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:26.42,27.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:27.28,30.4 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:27.65,35.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:40.85,48.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:53.59,55.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:60.88,68.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:73.106,81.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:86.82,88.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:93.68,101.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:105.88,113.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:117.62,119.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:123.91,131.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:135.109,143.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:147.85,149.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:154.68,162.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:166.88,174.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:178.62,180.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:184.91,192.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:196.109,204.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:208.85,210.2 1 0

View File

@@ -0,0 +1,378 @@
mode: set
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:23.80,31.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:34.59,41.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:44.81,52.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ap.go:55.60,62.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:30.15,32.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:39.20,46.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:53.20,59.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:65.20,71.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:77.19,82.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:89.20,96.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:102.18,104.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:110.18,112.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:118.18,120.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bind.go:126.18,128.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:26.15,27.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:27.27,29.18 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:29.18,31.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:32.3,34.18 3 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:34.18,36.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.3,37.18 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:37.18,39.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/bracket.go:40.3,40.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:26.74,27.51 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:27.51,29.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/eq.go:35.58,37.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:12.70,13.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:13.28,15.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:18.78,19.33 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:19.33,20.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:20.28,22.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:26.90,27.40 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:27.40,28.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:28.28,30.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:34.102,35.47 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:35.47,36.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:36.28,38.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:45.30,50.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:55.30,60.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:65.30,70.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:73.86,78.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:81.89,86.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:89.89,94.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:97.126,98.61 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:98.61,104.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:108.129,109.61 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:109.61,115.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:119.129,120.61 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:120.61,126.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:133.34,140.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:146.34,153.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:159.34,166.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:169.108,175.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:178.111,184.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:187.111,193.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:196.176,197.69 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:197.69,205.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:209.179,210.69 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:210.69,218.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:222.179,223.69 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:223.69,231.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:239.38,248.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:255.38,264.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:271.38,280.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:283.130,290.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:293.133,300.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:303.133,310.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:313.226,314.77 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:314.77,324.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:328.229,329.77 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:329.77,339.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:343.229,344.77 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:344.77,354.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:363.42,374.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:382.42,393.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:401.42,412.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:415.152,423.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:426.155,434.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:437.155,445.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:448.276,449.85 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:449.85,461.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:465.279,466.85 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:466.85,478.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:482.279,483.85 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:483.85,495.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:505.46,518.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:527.46,540.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:549.46,562.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:565.174,574.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:577.177,586.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:589.177,598.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:601.326,602.93 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:602.93,616.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:620.329,621.93 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:621.93,635.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:639.329,640.93 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:640.93,654.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:665.50,680.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:690.50,705.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:715.50,730.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:733.196,743.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:746.199,756.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:759.199,769.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:772.376,773.101 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:773.101,789.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:793.379,794.101 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:794.101,810.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:814.379,815.101 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:815.101,831.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:843.54,860.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:871.54,888.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:899.54,916.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:919.218,930.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:933.221,944.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:947.221,958.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:961.426,962.109 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:962.109,980.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:984.429,985.109 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:985.109,1003.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1007.429,1008.109 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1008.109,1026.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1039.58,1058.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1070.58,1089.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1101.58,1120.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1123.240,1135.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1138.243,1150.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1153.243,1165.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1168.476,1169.117 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1169.117,1189.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1193.479,1194.117 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1194.117,1214.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1218.479,1219.117 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1219.117,1239.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1253.62,1274.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1287.62,1308.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1321.62,1342.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1345.262,1358.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1361.265,1374.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1377.265,1390.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1393.526,1394.125 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1394.125,1416.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1420.529,1421.125 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1421.125,1443.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1447.529,1448.125 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1448.125,1470.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1485.68,1508.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1522.68,1545.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1559.68,1582.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1585.290,1599.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1602.293,1616.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1619.293,1633.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1636.588,1637.137 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1637.137,1661.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1665.591,1666.137 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1666.137,1690.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1694.591,1695.137 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/gen.go:1695.137,1719.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:32.39,33.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:33.27,35.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:40.36,41.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:41.27,43.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:47.33,49.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:52.38,54.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:58.46,59.31 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:59.31,62.3 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:67.43,68.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:68.27,70.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:75.49,76.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:76.27,78.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:83.52,84.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:84.27,86.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:92.70,93.40 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:93.40,94.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:94.28,95.10 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:95.10,97.5 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:98.4,98.35 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:105.88,106.50 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:106.50,107.42 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:107.42,108.29 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:108.29,110.19 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:110.19,112.6 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:113.5,114.11 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:114.11,116.6 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:117.5,117.36 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:125.78,126.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:126.27,128.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:128.17,130.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:131.3,131.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:136.60,138.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:141.61,143.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:146.42,148.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:151.46,153.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:157.66,158.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:158.27,160.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:160.17,162.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:163.3,163.25 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:169.48,171.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:174.60,176.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:179.42,181.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:185.72,186.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:186.27,188.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:188.17,190.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:191.3,191.16 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:198.54,200.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:204.93,205.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:205.27,207.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:207.17,209.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:210.3,210.29 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:215.75,217.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:221.86,222.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:222.27,224.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:224.17,226.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:227.3,227.14 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:232.68,234.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:237.77,239.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:242.58,244.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:248.80,249.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:249.27,256.13 5 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:256.13,259.4 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:261.3,264.20 3 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:264.20,266.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.3,267.19 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:267.19,269.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:271.3,271.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:276.61,278.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:282.80,283.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:283.27,285.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:285.17,287.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:289.3,290.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:290.17,292.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:294.3,294.28 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:299.76,301.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:304.60,306.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:310.49,316.16 4 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:316.16,318.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.2,320.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:320.27,323.3 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:328.77,329.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:329.27,331.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:331.17,333.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:334.3,334.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:339.59,341.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:344.91,345.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:345.27,347.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:347.17,349.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:350.3,350.25 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:355.73,357.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:360.97,362.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:366.73,367.36 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:367.36,368.19 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:368.19,370.18 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:370.18,372.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:373.4,373.12 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:379.73,381.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:384.55,386.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:390.77,391.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:391.27,393.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:393.17,395.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:396.3,397.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:397.17,399.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:400.3,400.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:405.70,407.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:410.59,412.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:415.52,417.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:419.98,420.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:420.27,422.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:422.17,424.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:425.3,426.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:426.17,428.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:429.3,429.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:434.80,436.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:438.91,439.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:439.27,441.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:441.17,443.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:444.3,445.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:445.17,447.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:448.3,448.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:453.73,455.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:458.83,459.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:459.27,461.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:461.17,463.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:464.3,465.22 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:470.65,472.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:475.91,477.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:480.73,482.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:485.84,487.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:490.66,492.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:495.76,497.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:500.58,502.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:506.100,507.18 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:507.18,509.17 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:509.17,511.4 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:512.3,512.22 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:523.29,524.43 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:524.43,525.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:525.28,527.19 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:527.19,529.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:530.4,532.19 3 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:532.19,534.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.4,535.19 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:535.19,537.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:538.4,538.23 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:545.41,546.29 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:546.29,549.3 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:554.54,555.27 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:555.27,557.3 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:562.79,563.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:563.27,565.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:565.17,567.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:568.3,568.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:573.58,575.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:578.68,580.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:583.49,585.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:590.55,591.42 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:591.42,592.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:592.28,595.4 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:602.55,603.42 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:603.42,604.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:604.28,607.33 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:607.33,609.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:610.4,610.15 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:617.77,618.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:618.27,620.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:620.17,622.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:623.3,623.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:628.59,630.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:634.85,635.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:635.27,637.17 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:637.17,640.4 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:641.3,641.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:646.78,648.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:650.67,652.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/ioeither.go:655.60,657.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:27.52,28.33 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:28.33,32.30 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:32.30,33.22 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:33.22,35.5 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/logging.go:36.4,37.28 2 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:33.50,35.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:38.51,40.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:43.63,45.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:48.69,50.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:53.73,55.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:58.65,60.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:65.55,67.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:72.74,74.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monad.go:79.89,81.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:31.13,38.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:45.13,52.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/monoid.go:59.13,66.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:31.15,32.73 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:32.73,33.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:33.27,35.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:37.26,39.4 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.2,40.27 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/retry.go:40.27,42.3 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/semigroup.go:29.41,33.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:25.66,26.42 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:26.42,27.28 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/sync.go:27.28,30.4 2 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:27.65,35.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:40.85,48.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:53.59,55.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:60.88,68.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:73.106,81.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:86.82,88.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:93.68,101.2 1 1
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:105.88,113.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:117.62,119.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:123.91,131.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:135.109,143.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:147.85,149.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:154.68,162.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:166.88,174.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:178.62,180.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:184.91,192.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:196.109,204.2 1 0
github.com/IBM/fp-go/v2/idiomatic/ioresult/traverse.go:208.85,210.2 1 0

View File

@@ -0,0 +1,198 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package ioresult provides functional programming combinators for working with IO operations
// that can fail with errors, following Go's idiomatic (value, error) tuple pattern.
//
// # Overview
//
// IOResult[A] represents a computation that performs IO and returns either a value of type A
// or an error. It is defined as:
//
// type IOResult[A any] = func() (A, error)
//
// This is the idiomatic Go version of IOEither, using Go's standard error handling pattern
// instead of the Either monad. It combines:
// - IO effects (functions that perform side effects)
// - Error handling via Go's (value, error) tuple return pattern
//
// # Why Parameterless Functions Represent IO Operations
//
// The key insight behind IOResult is that a function returning a value without taking any
// input can only produce that value through side effects. Consider:
//
// func() int { return 42 } // Pure: always returns 42
// func() int { return readFromFile() } // Impure: result depends on external state
//
// When a parameterless function returns different values on different calls, or when it
// interacts with the outside world (filesystem, network, random number generator, current
// time, database), it is performing a side effect - an observable interaction with state
// outside the function's scope.
//
// # Lazy Evaluation and Referential Transparency
//
// IOResult provides two critical benefits:
//
// 1. **Lazy Evaluation**: The side effect doesn't happen when you create the IOResult,
// only when you call it (execute it). This allows you to build complex computations
// as pure data structures and defer execution until needed.
//
// // This doesn't read the file yet, just describes how to read it
// readConfig := func() (Config, error) { return os.ReadFile("config.json") }
//
// // Still hasn't read the file, just composed operations
// parsed := Map(parseJSON)(readConfig)
//
// // NOW it reads the file and parses it
// config, err := parsed()
//
// 2. **Referential Transparency of the Description**: While the IO operation itself has
// side effects, the IOResult value (the function) is referentially transparent. You can
// pass it around, compose it, and reason about it without triggering the side effect.
// The side effect only occurs when you explicitly call the function.
//
// # Distinguishing Pure from Impure Operations
//
// The type system makes the distinction clear:
//
// // Pure function: always returns the same output for the same input
// func double(x int) int { return x * 2 }
//
// // Impure operation: encapsulated in IOResult
// func readFile(path string) IOResult[[]byte] {
// return func() ([]byte, error) {
// return os.ReadFile(path) // Side effect: file system access
// }
// }
//
// The IOResult type explicitly marks operations as having side effects, making the
// distinction between pure and impure code visible in the type system. This allows
// developers to:
// - Identify which parts of the code interact with external state
// - Test pure logic separately from IO operations
// - Compose IO operations while keeping them lazy
// - Control when and where side effects occur
//
// # Examples of Side Effects Captured by IOResult
//
// IOResult is appropriate for operations that:
// - Read from or write to files, databases, or network
// - Generate random numbers
// - Read the current time
// - Modify mutable state
// - Interact with external APIs
// - Execute system commands
// - Acquire or release resources
//
// Example:
//
// // Each call potentially returns a different value
// getCurrentTime := func() (time.Time, error) {
// return time.Now(), nil // Side effect: reads system clock
// }
//
// // Each call reads from external state
// readDatabase := func() (User, error) {
// return db.Query("SELECT * FROM users WHERE id = ?", 1)
// }
//
// // Composes multiple IO operations
// pipeline := F.Pipe2(
// getCurrentTime,
// Chain(func(t time.Time) IOResult[string] {
// return func() (string, error) {
// return fmt.Sprintf("Time: %v", t), nil
// }
// }),
// )
//
// # Core Concepts
//
// IOResult follows functional programming principles:
// - Functor: Transform successful values with Map
// - Applicative: Combine multiple IOResults with Ap, ApS
// - Monad: Chain dependent computations with Chain, Bind
// - Error recovery: Handle errors with ChainLeft, Alt
//
// # Basic Usage
//
// Creating IOResult values:
//
// success := Of(42) // Right value
// failure := Left[int](errors.New("error")) // Left (error) value
//
// Transforming values:
//
// doubled := Map(func(x int) int { return x * 2 })(success)
//
// Chaining computations:
//
// result := Chain(func(x int) IOResult[string] {
// return Of(fmt.Sprintf("%d", x))
// })(success)
//
// # Do Notation
//
// The package supports do-notation style composition for building complex computations:
//
// result := F.Pipe5(
// Of("John"),
// BindTo(T.Of[string]),
// ApS(T.Push1[string, int], Of(42)),
// Bind(T.Push2[string, int, string], func(t T.Tuple2[string, int]) IOResult[string] {
// return Of(fmt.Sprintf("%s: %d", t.F1, t.F2))
// }),
// Map(transform),
// )
//
// # Error Handling
//
// IOResult provides several ways to handle errors:
// - ChainLeft: Transform error values into new computations
// - Alt: Provide alternative computations when an error occurs
// - GetOrElse: Extract values with a default for errors
// - Fold: Handle both success and error cases explicitly
//
// # Concurrency
//
// IOResult supports both sequential and parallel execution:
// - ApSeq, TraverseArraySeq: Sequential execution
// - ApPar, TraverseArrayPar: Parallel execution (default)
// - Ap, TraverseArray: Defaults to parallel execution
//
// # Resource Management
//
// The package provides resource management utilities:
// - Bracket: Acquire, use, and release resources safely
// - WithResource: Scoped resource management
// - WithLock: Execute operations within a lock scope
//
// # Conversion Functions
//
// IOResult interoperates with other types:
// - FromEither: Convert Either to IOResult
// - FromResult: Convert (value, error) tuple to IOResult
// - FromOption: Convert Option to IOResult
// - FromIO: Convert pure IO to IOResult (always succeeds)
//
// # Examples
//
// See the example tests for detailed usage patterns:
// - examples_create_test.go: Creating IOResult values
// - examples_do_test.go: Using do-notation
// - examples_extract_test.go: Extracting values from IOResult
package ioresult
//go:generate go run .. ioeither --count 10 --filename gen.go

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
// Eq implements the equals predicate for values contained in the IOEither monad
// Eq constructs an equality predicate for IOResult values.
// The comparison function receives (value, error) tuples from both IOResults.
func Eq[A any](eq func(A, error) func(A, error) bool) EQ.Eq[IOResult[A]] {
return EQ.FromEquals(func(l, r IOResult[A]) bool {
return eq(l())(r())
})
}
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
// FromStrictEquals constructs an Eq from Go's built-in equality (==) for comparable types.
// Both the value and error must match for two IOResults to be considered equal.
func FromStrictEquals[A comparable]() EQ.Eq[IOResult[A]] {
return Eq(result.FromStrictEquals[A]())
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEq(t *testing.T) {
r1 := Of(1)
r2 := Of(1)
r3 := Of(2)
err1 := errors.New("a")
e1 := Left[int](err1)
e2 := Left[int](err1) // Same error instance
e3 := Left[int](errors.New("b"))
eq := FromStrictEquals[int]()
assert.True(t, eq.Equals(r1, r1))
assert.True(t, eq.Equals(r1, r2))
assert.False(t, eq.Equals(r1, r3))
assert.False(t, eq.Equals(r1, e1))
assert.True(t, eq.Equals(e1, e1))
assert.True(t, eq.Equals(e1, e2))
assert.False(t, eq.Equals(e1, e3))
assert.False(t, eq.Equals(e2, r2))
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"fmt"
E "github.com/IBM/fp-go/v2/either"
)
func ExampleIOResult_creation() {
// Build an IOResult
leftValue := Left[string](fmt.Errorf("some error"))
rightValue := Right("value")
// Convert from Either
eitherValue := E.Of[error](42)
ioFromEither := FromEither(eitherValue)
// some predicate
isEven := func(num int) (int, error) {
if num%2 == 0 {
return num, nil
}
return 0, fmt.Errorf("%d is an odd number", num)
}
fromEven := Eitherize1(isEven)
leftFromPred := fromEven(3)
rightFromPred := fromEven(4)
// Convert results to Either for display
val1, err1 := leftValue()
if err1 != nil {
fmt.Printf("Left[*errors.errorString](%s)\n", err1.Error())
} else {
fmt.Printf("Right[string](%s)\n", val1)
}
val2, err2 := rightValue()
if err2 != nil {
fmt.Printf("Left[*errors.errorString](%s)\n", err2.Error())
} else {
fmt.Printf("Right[string](%s)\n", val2)
}
val3, err3 := ioFromEither()
if err3 != nil {
fmt.Printf("Left[*errors.errorString](%s)\n", err3.Error())
} else {
fmt.Printf("Right[int](%d)\n", val3)
}
val4, err4 := leftFromPred()
if err4 != nil {
fmt.Printf("Left[*errors.errorString](%s)\n", err4.Error())
} else {
fmt.Printf("Right[int](%d)\n", val4)
}
val5, err5 := rightFromPred()
if err5 != nil {
fmt.Printf("Left[*errors.errorString](%s)\n", err5.Error())
} else {
fmt.Printf("Right[int](%d)\n", val5)
}
// Output:
// Left[*errors.errorString](some error)
// Right[string](value)
// Right[int](42)
// Left[*errors.errorString](3 is an odd number)
// Right[int](4)
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"fmt"
"log"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
T "github.com/IBM/fp-go/v2/tuple"
)
func ExampleIOResult_do() {
foo := Of("foo")
bar := Of(1)
// quux consumes the state of three bindings and returns an [IO] instead of an [IOResult]
quux := func(t T.Tuple3[string, int, string]) IO[any] {
return io.FromImpure(func() {
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
})
}
transform := func(t T.Tuple3[string, int, string]) int {
return len(t.F1) + t.F2 + len(t.F3)
}
b := F.Pipe5(
foo,
BindTo(T.Of[string]),
ApS(T.Push1[string, int], bar),
Bind(T.Push2[string, int, string], func(t T.Tuple2[string, int]) IOResult[string] {
return Of(fmt.Sprintf("%s%d", t.F1, t.F2))
}),
ChainFirstIOK(quux),
Map(transform),
)
val, err := b()
if err != nil {
fmt.Printf("Left[error](%s)\n", err.Error())
} else {
fmt.Printf("Right[int](%d)\n", val)
}
// Output:
// Right[int](8)
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"fmt"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
)
func ExampleIOResult_extraction() {
// IOResult
someIOResult := Right(42)
value, err := someIOResult() // (42, nil)
if err == nil {
fmt.Println(E.Of[error](value)) // Convert to Either for display
}
// Or more directly using GetOrElse
infallibleIO := GetOrElse(F.Constant1[error](io.Of(0)))(someIOResult) // => io returns 42
valueFromIO := infallibleIO() // => 42
fmt.Println(value)
fmt.Println(valueFromIO)
// Output:
// Right[int](42)
// 42
// 42
}

View File

@@ -0,0 +1,152 @@
// 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 exec provides utilities for executing system commands with IOResult-based error handling.
//
// This package wraps system command execution in the IOResult monad, which represents
// IO operations that can fail with errors using Go's idiomatic (value, error) tuple pattern.
// Unlike the result/exec package, these operations explicitly acknowledge their side-effectful
// nature by returning IOResult instead of plain Result.
//
// # Overview
//
// The exec package is designed for executing system commands as IO operations that may fail.
// Since command execution is inherently side-effectful (it interacts with the operating system,
// may produce different results over time, and has observable effects), IOResult is the
// appropriate abstraction.
//
// # Basic Usage
//
// The primary function is Command, which executes a system command:
//
// import (
// "github.com/IBM/fp-go/v2/bytes"
// "github.com/IBM/fp-go/v2/exec"
// "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/idiomatic/ioresult"
// ioexec "github.com/IBM/fp-go/v2/idiomatic/ioresult/exec"
// )
//
// // Execute a command and get the output
// version := F.Pipe1(
// ioexec.Command("openssl")([]string{"version"})([]byte{}),
// ioresult.Map(F.Flow2(
// exec.StdOut,
// bytes.ToString,
// )),
// )
//
// // Run the IO operation
// result, err := version()
// if err != nil {
// log.Fatal(err)
// }
// fmt.Println(result)
//
// # Command Output
//
// Commands return exec.CommandOutput, which contains both stdout and stderr as byte slices.
// Use exec.StdOut and exec.StdErr to extract the respective streams:
//
// output := ioexec.Command("ls")([]string{"-la"})([]byte{})
// result := ioresult.Map(func(out exec.CommandOutput) string {
// stdout := exec.StdOut(out)
// stderr := exec.StdErr(out)
// return bytes.ToString(stdout)
// })(output)
//
// # Composing Commands
//
// Commands can be composed using IOResult combinators:
//
// // Chain multiple commands together
// pipeline := F.Pipe2(
// ioexec.Command("echo")([]string{"hello world"})([]byte{}),
// ioresult.Chain(func(out exec.CommandOutput) ioexec.IOResult[exec.CommandOutput] {
// input := exec.StdOut(out)
// return ioexec.Command("tr")([]string{"a-z", "A-Z"})(input)
// }),
// ioresult.Map(F.Flow2(exec.StdOut, bytes.ToString)),
// )
//
// # Error Handling
//
// Commands return errors for various failure conditions:
// - Command not found
// - Non-zero exit status
// - Permission errors
// - System resource errors
//
// Handle errors using IOResult's error handling combinators:
//
// safeCommand := F.Pipe1(
// ioexec.Command("risky-command")([]string{})([]byte{}),
// ioresult.Alt(func() (exec.CommandOutput, error) {
// // Fallback on error
// return exec.CommandOutput{}, nil
// }),
// )
package exec
import (
"context"
"github.com/IBM/fp-go/v2/exec"
"github.com/IBM/fp-go/v2/function"
INTE "github.com/IBM/fp-go/v2/internal/exec"
)
var (
// Command executes a system command with side effects and returns an IOResult.
//
// This function is curried to allow partial application. It takes three parameters:
// - name: The command name or path to execute
// - args: Command-line arguments as a slice of strings
// - in: Input bytes to send to the command's stdin
//
// Returns IOResult[exec.CommandOutput] which, when executed, will run the command
// and return either the command output or an error.
//
// The command is executed using the system's default shell context. The output
// contains both stdout and stderr as byte slices, accessible via exec.StdOut
// and exec.StdErr respectively.
//
// Example:
//
// // Simple command execution
// version := Command("node")([]string{"--version"})([]byte{})
// result, err := version()
//
// // With input piped to stdin
// echo := Command("cat")([]string{})([]byte("hello world"))
//
// // Partial application for reuse
// git := Command("git")
// status := git([]string{"status"})([]byte{})
// log := git([]string{"log", "--oneline"})([]byte{})
//
// // Composed with IOResult combinators
// result := F.Pipe1(
// Command("openssl")([]string{"version"})([]byte{}),
// ioresult.Map(F.Flow2(exec.StdOut, bytes.ToString)),
// )
Command = function.Curry3(command)
)
func command(name string, args []string, in []byte) IOResult[exec.CommandOutput] {
return func() (exec.CommandOutput, error) {
return INTE.Exec(context.Background(), name, args, in)
}
}

View File

@@ -0,0 +1,44 @@
// 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 exec
import (
"strings"
"testing"
RA "github.com/IBM/fp-go/v2/array"
B "github.com/IBM/fp-go/v2/bytes"
"github.com/IBM/fp-go/v2/exec"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/stretchr/testify/assert"
)
func TestOpenSSL(t *testing.T) {
// execute the openSSL binary
version := F.Pipe1(
Command("openssl")(RA.From("version"))(B.Monoid.Empty()),
ioresult.Map(F.Flow3(
exec.StdOut,
B.ToString,
strings.TrimSpace,
)),
)
result, err := version()
assert.NoError(t, err)
assert.NotEmpty(t, result)
}

View File

@@ -0,0 +1,63 @@
// 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 exec
import (
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
type (
// IOResult represents an IO operation that may fail with an error.
// It is defined as func() (T, error), following Go's idiomatic error handling pattern.
//
// This type is re-exported from the ioresult package for convenience when working
// with command execution, allowing users to reference exec.IOResult instead of
// importing the ioresult package separately.
IOResult[T any] = ioresult.IOResult[T]
// Kleisli represents a function from A to IOResult[B].
// Named after Heinrich Kleisli, it represents a monadic arrow in the IOResult monad.
//
// Kleisli functions are useful for chaining operations where each step may perform
// IO and may fail. They can be composed using IOResult's Chain function.
//
// Example:
//
// type Kleisli[A, B any] func(A) IOResult[B]
//
// parseConfig := func(path string) IOResult[Config] { ... }
// validateConfig := func(cfg Config) IOResult[Config] { ... }
//
// // Compose Kleisli functions
// loadAndValidate := Chain(validateConfig)(parseConfig("/config.yml"))
Kleisli[A, B any] = ioresult.Kleisli[A, B]
// Operator represents a function that transforms one IOResult into another.
// It maps IOResult[A] to IOResult[B], useful for defining reusable transformations.
//
// Example:
//
// type Operator[A, B any] func(IOResult[A]) IOResult[B]
//
// addLogging := func(io IOResult[string]) IOResult[string] {
// return func() (string, error) {
// result, err := io()
// log.Printf("Result: %v, Error: %v", result, err)
// return result, err
// }
// }
Operator[A, B any] = ioresult.Operator[A, B]
)

View File

@@ -0,0 +1,34 @@
// 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 file
import (
"os"
)
// MkdirAll create a sequence of directories, see [os.MkdirAll]
func MkdirAll(path string, perm os.FileMode) IOResult[string] {
return func() (string, error) {
return path, os.MkdirAll(path, perm)
}
}
// Mkdir create a directory, see [os.Mkdir]
func Mkdir(path string, perm os.FileMode) IOResult[string] {
return func() (string, error) {
return path, os.Mkdir(path, perm)
}
}

View File

@@ -0,0 +1,128 @@
// 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 file
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMkdir(t *testing.T) {
t.Run("successful mkdir", func(t *testing.T) {
tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "testdir")
result := Mkdir(newDir, 0755)
path, err := result()
assert.NoError(t, err)
assert.Equal(t, newDir, path)
// Verify directory was created
info, err := os.Stat(newDir)
assert.NoError(t, err)
assert.True(t, info.IsDir())
})
t.Run("mkdir with existing directory", func(t *testing.T) {
tmpDir := t.TempDir()
result := Mkdir(tmpDir, 0755)
_, err := result()
assert.Error(t, err)
})
t.Run("mkdir with parent directory not existing", func(t *testing.T) {
result := Mkdir("/non/existent/parent/child", 0755)
_, err := result()
assert.Error(t, err)
})
}
func TestMkdirAll(t *testing.T) {
t.Run("successful mkdir all", func(t *testing.T) {
tmpDir := t.TempDir()
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
result := MkdirAll(nestedDir, 0755)
path, err := result()
assert.NoError(t, err)
assert.Equal(t, nestedDir, path)
// Verify all directories were created
info, err := os.Stat(nestedDir)
assert.NoError(t, err)
assert.True(t, info.IsDir())
// Verify intermediate directories
level1 := filepath.Join(tmpDir, "level1")
info1, err := os.Stat(level1)
assert.NoError(t, err)
assert.True(t, info1.IsDir())
level2 := filepath.Join(tmpDir, "level1", "level2")
info2, err := os.Stat(level2)
assert.NoError(t, err)
assert.True(t, info2.IsDir())
})
t.Run("mkdirall with existing directory", func(t *testing.T) {
tmpDir := t.TempDir()
result := MkdirAll(tmpDir, 0755)
path, err := result()
// MkdirAll should succeed even if directory exists
assert.NoError(t, err)
assert.Equal(t, tmpDir, path)
})
t.Run("mkdirall single level", func(t *testing.T) {
tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "single")
result := MkdirAll(newDir, 0755)
path, err := result()
assert.NoError(t, err)
assert.Equal(t, newDir, path)
info, err := os.Stat(newDir)
assert.NoError(t, err)
assert.True(t, info.IsDir())
})
t.Run("mkdirall with file in path", func(t *testing.T) {
tmpDir := t.TempDir()
filePath := filepath.Join(tmpDir, "file.txt")
// Create a file
err := os.WriteFile(filePath, []byte("content"), 0644)
assert.NoError(t, err)
// Try to create a directory where file exists
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755)
_, err = result()
assert.Error(t, err)
})
}

View File

@@ -0,0 +1,55 @@
// 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 file
import (
"io"
"os"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
var (
// Open opens a file for reading
Open = ioresult.Eitherize1(os.Open)
// Create opens a file for writing
Create = ioresult.Eitherize1(os.Create)
// ReadFile reads the context of a file
ReadFile = ioresult.Eitherize1(os.ReadFile)
)
// WriteFile writes a data blob to a file
func WriteFile(dstName string, perm os.FileMode) Kleisli[[]byte, []byte] {
return func(data []byte) IOResult[[]byte] {
return func() ([]byte, error) {
return data, os.WriteFile(dstName, data, perm)
}
}
}
// Remove removes a file by name
func Remove(name string) IOResult[string] {
return func() (string, error) {
return name, os.Remove(name)
}
}
// Close closes an object
func Close[C io.Closer](c C) IOResult[any] {
return func() (any, error) {
return c, c.Close()
}
}

View File

@@ -0,0 +1,234 @@
// 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 file
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOpen(t *testing.T) {
t.Run("successful open", func(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "test-open-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
// Write some content
err = os.WriteFile(tmpPath, []byte("test content"), 0644)
require.NoError(t, err)
// Test Open
result := Open(tmpPath)
file, err := result()
assert.NoError(t, err)
assert.NotNil(t, file)
file.Close()
})
t.Run("open non-existent file", func(t *testing.T) {
result := Open("/path/that/does/not/exist.txt")
_, err := result()
assert.Error(t, err)
})
}
func TestCreate(t *testing.T) {
t.Run("successful create", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "new-file.txt")
result := Create(testPath)
file, err := result()
assert.NoError(t, err)
assert.NotNil(t, file)
// Verify file was created
_, statErr := os.Stat(testPath)
assert.NoError(t, statErr)
file.Close()
})
t.Run("create in non-existent directory", func(t *testing.T) {
result := Create("/non/existent/directory/file.txt")
_, err := result()
assert.Error(t, err)
})
}
func TestReadFile(t *testing.T) {
t.Run("successful read", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-read-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
expectedContent := []byte("Hello, World!")
_, err = tmpFile.Write(expectedContent)
require.NoError(t, err)
tmpFile.Close()
result := ReadFile(tmpPath)
content, err := result()
assert.NoError(t, err)
assert.Equal(t, expectedContent, content)
})
t.Run("read non-existent file", func(t *testing.T) {
result := ReadFile("/non/existent/file.txt")
_, err := result()
assert.Error(t, err)
})
t.Run("read empty file", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-empty-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
result := ReadFile(tmpPath)
content, err := result()
assert.NoError(t, err)
assert.Empty(t, content)
})
}
func TestWriteFile(t *testing.T) {
t.Run("successful write", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "write-test.txt")
testData := []byte("test data")
result := WriteFile(testPath, 0644)(testData)
returnedData, err := result()
assert.NoError(t, err)
assert.Equal(t, testData, returnedData)
// Verify file content
content, err := os.ReadFile(testPath)
require.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("write to invalid path", func(t *testing.T) {
testData := []byte("test data")
result := WriteFile("/non/existent/dir/file.txt", 0644)(testData)
_, err := result()
assert.Error(t, err)
})
t.Run("overwrite existing file", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-overwrite-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
// Write initial content
err = os.WriteFile(tmpPath, []byte("initial"), 0644)
require.NoError(t, err)
// Overwrite with new content
newData := []byte("overwritten")
result := WriteFile(tmpPath, 0644)(newData)
returnedData, err := result()
assert.NoError(t, err)
assert.Equal(t, newData, returnedData)
// Verify new content
content, err := os.ReadFile(tmpPath)
require.NoError(t, err)
assert.Equal(t, newData, content)
})
}
func TestRemove(t *testing.T) {
t.Run("successful remove", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-remove-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
result := Remove(tmpPath)
name, err := result()
assert.NoError(t, err)
assert.Equal(t, tmpPath, name)
// Verify file is removed
_, statErr := os.Stat(tmpPath)
assert.True(t, os.IsNotExist(statErr))
})
t.Run("remove non-existent file", func(t *testing.T) {
result := Remove("/non/existent/file.txt")
_, err := result()
assert.Error(t, err)
})
}
func TestClose(t *testing.T) {
t.Run("successful close", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-close-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
result := Close(tmpFile)
_, err = result()
assert.NoError(t, err)
// Verify file is closed by attempting to write
_, writeErr := tmpFile.Write([]byte("test"))
assert.Error(t, writeErr)
})
t.Run("close already closed file", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-close-twice-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
// Close once
tmpFile.Close()
// Close again via Close function
result := Close(tmpFile)
_, err = result()
assert.Error(t, err)
})
}

View File

@@ -0,0 +1,40 @@
// 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 file
import (
"io"
FL "github.com/IBM/fp-go/v2/file"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
var (
// readAll is the adapted version of [io.ReadAll]
readAll = ioresult.Eitherize1(io.ReadAll)
)
// ReadAll uses a generator function to create a stream, reads it and closes it
func ReadAll[R io.ReadCloser](acquire IOResult[R]) IOResult[[]byte] {
return F.Pipe1(
F.Flow2(
FL.ToReader[R],
readAll,
),
ioresult.WithResource[[]byte](acquire, Close[R]),
)
}

View File

@@ -0,0 +1,137 @@
// 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 file
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReadAll(t *testing.T) {
t.Run("successful read all", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-readall-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
expectedContent := []byte("Hello, ReadAll!")
_, err = tmpFile.Write(expectedContent)
require.NoError(t, err)
tmpFile.Close()
result := ReadAll(Open(tmpPath))
content, err := result()
assert.NoError(t, err)
assert.Equal(t, expectedContent, content)
})
t.Run("read all ensures file is closed", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-readall-close-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
testContent := []byte("test data for close")
_, err = tmpFile.Write(testContent)
require.NoError(t, err)
tmpFile.Close()
var capturedFile *os.File
acquire := func() (*os.File, error) {
f, err := os.Open(tmpPath)
capturedFile = f
return f, err
}
result := ReadAll(acquire)
content, err := result()
assert.NoError(t, err)
assert.Equal(t, testContent, content)
// Verify file is closed by trying to read
buf := make([]byte, 10)
_, readErr := capturedFile.Read(buf)
assert.Error(t, readErr)
})
t.Run("read all with open failure", func(t *testing.T) {
result := ReadAll(Open("/non/existent/file.txt"))
_, err := result()
assert.Error(t, err)
})
t.Run("read all empty file", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-readall-empty-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)
result := ReadAll(Open(tmpPath))
content, err := result()
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("read all large file", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "large-file.txt")
// Create a larger file
largeContent := make([]byte, 10000)
for i := range largeContent {
largeContent[i] = byte('A' + (i % 26))
}
err := os.WriteFile(testPath, largeContent, 0644)
require.NoError(t, err)
result := ReadAll(Open(testPath))
content, err := result()
assert.NoError(t, err)
assert.Equal(t, largeContent, content)
assert.Len(t, content, 10000)
})
t.Run("read all with binary data", func(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-readall-binary-*.bin")
require.NoError(t, err)
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
// Write binary data
binaryContent := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
_, err = tmpFile.Write(binaryContent)
require.NoError(t, err)
tmpFile.Close()
result := ReadAll(Open(tmpPath))
content, err := result()
assert.NoError(t, err)
assert.Equal(t, binaryContent, content)
})
}

View File

@@ -0,0 +1,44 @@
// 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 file
import (
"os"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/io"
IOF "github.com/IBM/fp-go/v2/io/file"
)
var (
// CreateTemp created a temp file with proper parametrization
CreateTemp = ioresult.Eitherize2(os.CreateTemp)
// onCreateTempFile creates a temp file with sensible defaults
onCreateTempFile = CreateTemp("", "*")
// destroy handler
onReleaseTempFile = F.Flow4(
IOF.Close[*os.File],
io.Map((*os.File).Name),
ioresult.FromIO[string],
ioresult.Chain(Remove),
)
)
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
func WithTempFile[A any](f Kleisli[*os.File, A]) IOResult[A] {
return ioresult.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
}

View File

@@ -0,0 +1,53 @@
// 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 file
import (
"os"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/stretchr/testify/assert"
)
func TestWithTempFile(t *testing.T) {
res := F.Pipe2(
[]byte("Carsten"),
onWriteAll[*os.File],
WithTempFile,
)
result, err := res()
assert.NoError(t, err)
assert.Equal(t, []byte("Carsten"), result)
}
func TestWithTempFileOnClosedFile(t *testing.T) {
res := WithTempFile(func(f *os.File) IOResult[[]byte] {
return F.Pipe2(
f,
onWriteAll[*os.File]([]byte("Carsten")),
ioresult.ChainFirst(F.Constant1[[]byte](Close(f))),
)
})
result, err := res()
assert.NoError(t, err)
assert.Equal(t, []byte("Carsten"), result)
}

View File

@@ -0,0 +1,11 @@
package file
import (
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
type (
IOResult[T any] = ioresult.IOResult[T]
Kleisli[A, B any] = ioresult.Kleisli[A, B]
Operator[A, B any] = ioresult.Operator[A, B]
)

View File

@@ -0,0 +1,50 @@
// 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 file
import (
"io"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
return func(w W) IOResult[[]byte] {
return func() ([]byte, error) {
_, err := w.Write(data)
return data, err
}
}
}
// WriteAll uses a generator function to create a stream, writes data to it and closes it
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
onWrite := onWriteAll[W](data)
return func(onCreate IOResult[W]) IOResult[[]byte] {
return ioresult.WithResource[[]byte](
onCreate,
Close[W])(
onWrite,
)
}
}
// Write uses a generator function to create a stream, writes data to it and closes it
func Write[R any, W io.WriteCloser](acquire IOResult[W]) Kleisli[Kleisli[W, R], R] {
return ioresult.WithResource[R](
acquire,
Close[W])
}

View File

@@ -0,0 +1,200 @@
// 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 file
import (
"os"
"path/filepath"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWriteAll(t *testing.T) {
t.Run("successful write all", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "writeall-test.txt")
testData := []byte("Hello, WriteAll!")
acquire := Create(testPath)
result := F.Pipe1(
acquire,
WriteAll[*os.File](testData),
)
returnedData, err := result()
assert.NoError(t, err)
assert.Equal(t, testData, returnedData)
// Verify file content
content, err := os.ReadFile(testPath)
require.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("write all ensures file is closed", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "writeall-close-test.txt")
testData := []byte("test data")
var capturedFile *os.File
acquire := func() (*os.File, error) {
f, err := os.Create(testPath)
capturedFile = f
return f, err
}
result := F.Pipe1(
ioresult.FromResult(acquire()),
WriteAll[*os.File](testData),
)
_, err := result()
assert.NoError(t, err)
// Verify file is closed by trying to write to it
_, writeErr := capturedFile.Write([]byte("more"))
assert.Error(t, writeErr)
})
t.Run("write all with acquire failure", func(t *testing.T) {
testData := []byte("test data")
acquire := Create("/non/existent/dir/file.txt")
result := F.Pipe1(
acquire,
WriteAll[*os.File](testData),
)
_, err := result()
assert.Error(t, err)
})
t.Run("write all with empty data", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "empty-writeall.txt")
acquire := Create(testPath)
result := F.Pipe1(
acquire,
WriteAll[*os.File]([]byte{}),
)
returnedData, err := result()
assert.NoError(t, err)
assert.Empty(t, returnedData)
// Verify file exists and is empty
content, err := os.ReadFile(testPath)
require.NoError(t, err)
assert.Empty(t, content)
})
}
func TestWrite(t *testing.T) {
t.Run("successful write with resource management", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "write-test.txt")
testData := []byte("test content")
acquire := Create(testPath)
useFile := func(f *os.File) IOResult[int] {
return func() (int, error) {
return f.Write(testData)
}
}
result := Write[int](acquire)(useFile)
bytesWritten, err := result()
assert.NoError(t, err)
assert.Equal(t, len(testData), bytesWritten)
// Verify file content
content, err := os.ReadFile(testPath)
require.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("write ensures cleanup on success", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "write-cleanup-test.txt")
var capturedFile *os.File
acquire := func() (*os.File, error) {
f, err := os.Create(testPath)
capturedFile = f
return f, err
}
useFile := func(f *os.File) IOResult[string] {
return func() (string, error) {
_, err := f.Write([]byte("data"))
return "success", err
}
}
result := Write[string](ioresult.FromResult(acquire()))(useFile)
_, err := result()
assert.NoError(t, err)
// Verify file is closed
_, writeErr := capturedFile.Write([]byte("more"))
assert.Error(t, writeErr)
})
t.Run("write ensures cleanup on failure", func(t *testing.T) {
tmpDir := t.TempDir()
testPath := filepath.Join(tmpDir, "write-fail-test.txt")
var capturedFile *os.File
acquire := func() (*os.File, error) {
f, err := os.Create(testPath)
capturedFile = f
return f, err
}
useFile := func(f *os.File) IOResult[string] {
return ioresult.Left[string](assert.AnError)
}
result := Write[string](ioresult.FromResult(acquire()))(useFile)
_, err := result()
assert.Error(t, err)
// Verify file is still closed even on error
_, writeErr := capturedFile.Write([]byte("more"))
assert.Error(t, writeErr)
})
t.Run("write with acquire failure", func(t *testing.T) {
useFile := func(f *os.File) IOResult[string] {
return ioresult.Of("should not run")
}
result := Write[string](Create("/non/existent/dir/file.txt"))(useFile)
_, err := result()
assert.Error(t, err)
})
}

1840
v2/idiomatic/ioresult/gen.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
// 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 builder
import (
"bytes"
"net/http"
"strconv"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/http/builder"
H "github.com/IBM/fp-go/v2/http/headers"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
IOEH "github.com/IBM/fp-go/v2/idiomatic/ioresult/http"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
func Requester(builder *R.Builder) IOEH.Requester {
withBody := F.Curry3(func(data []byte, url string, method string) IOResult[*http.Request] {
return func() (*http.Request, error) {
req, err := http.NewRequest(method, url, bytes.NewReader(data))
if err == nil {
req.Header.Set(H.ContentLength, strconv.Itoa(len(data)))
H.Monoid.Concat(req.Header, builder.GetHeaders())
}
return req, err
}
})
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
return func() (*http.Request, error) {
req, err := http.NewRequest(method, url, nil)
if err == nil {
H.Monoid.Concat(req.Header, builder.GetHeaders())
}
return req, err
}
})
return F.Pipe5(
builder.GetBody(),
option.Fold(lazy.Of(result.Of(withoutBody)), result.Map(withBody)),
result.Ap[func(string) IOResult[*http.Request]](builder.GetTargetURL()),
result.Flap[IOResult[*http.Request]](builder.GetMethod()),
result.GetOrElse(ioresult.Left[*http.Request]),
ioresult.Map(func(req *http.Request) *http.Request {
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
return req
}),
)
}

View File

@@ -0,0 +1,58 @@
// 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 builder
import (
"net/http"
"net/url"
"testing"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/http/builder"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
E "github.com/IBM/fp-go/v2/idiomatic/result"
"github.com/IBM/fp-go/v2/io"
"github.com/stretchr/testify/assert"
)
func TestBuilderWithQuery(t *testing.T) {
// add some query
withLimit := R.WithQueryArg("limit")("10")
withURL := R.WithURL("http://www.example.org?a=b")
b := F.Pipe2(
R.Default,
withLimit,
withURL,
)
req := F.Pipe3(
b,
Requester,
ioresult.Map(func(r *http.Request) *url.URL {
return r.URL
}),
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[any] {
return io.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))
assert.Equal(t, "b", q.Get("a"))
})
}),
)
assert.True(t, E.IsRight(req()))
}

View File

@@ -0,0 +1,22 @@
// 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 builder
import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
type (
IOResult[A any] = ioresult.IOResult[A]
)

View File

@@ -0,0 +1,137 @@
// 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 http
import (
"bytes"
"io"
"net/http"
B "github.com/IBM/fp-go/v2/bytes"
FL "github.com/IBM/fp-go/v2/file"
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/http"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
IOEF "github.com/IBM/fp-go/v2/idiomatic/ioresult/file"
J "github.com/IBM/fp-go/v2/json"
P "github.com/IBM/fp-go/v2/pair"
)
type (
client struct {
delegate *http.Client
doIOE Kleisli[*http.Request, *http.Response]
}
)
var (
// MakeRequest is an eitherized version of [http.NewRequest]
MakeRequest = ioresult.Eitherize3(http.NewRequest)
makeRequest = F.Bind13of3(MakeRequest)
// specialize
MakeGetRequest = makeRequest("GET", nil)
)
// MakeBodyRequest creates a request that carries a body
func MakeBodyRequest(method string, body IOResult[[]byte]) Kleisli[string, *http.Request] {
onBody := F.Pipe1(
body,
ioresult.Map(F.Flow2(
bytes.NewReader,
FL.ToReader[*bytes.Reader],
)),
)
onRelease := ioresult.Of[io.Reader]
withMethod := F.Bind1of3(MakeRequest)(method)
return F.Flow2(
F.Bind1of2(withMethod),
ioresult.WithResource[*http.Request](onBody, onRelease),
)
}
func (client client) Do(req Requester) IOResult[*http.Response] {
return F.Pipe1(
req,
ioresult.Chain(client.doIOE),
)
}
func MakeClient(httpClient *http.Client) Client {
return client{delegate: httpClient, doIOE: ioresult.Eitherize1(httpClient.Do)}
}
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
return F.Flow3(
client.Do,
ioresult.ChainEitherK(H.ValidateResponse),
ioresult.Chain(func(resp *http.Response) IOResult[H.FullResponse] {
// var x R.Reader[*http.Response, IOResult[[]byte]] = F.Flow3(
// H.GetBody,
// ioresult.Of,
// IOEF.ReadAll,
// )
return F.Pipe1(
F.Pipe3(
resp,
H.GetBody,
ioresult.Of,
IOEF.ReadAll,
),
ioresult.Map(F.Bind1st(P.MakePair[*http.Response, []byte], resp)),
)
}),
)
}
// ReadAll sends a request and reads the response as bytes
func ReadAll(client Client) Kleisli[Requester, []byte] {
return F.Flow2(
ReadFullResponse(client),
ioresult.Map(H.Body),
)
}
// ReadText sends a request, reads the response and represents the response as a text string
func ReadText(client Client) Kleisli[Requester, string] {
return F.Flow2(
ReadAll(client),
ioresult.Map(B.ToString),
)
}
// readJSON sends a request, reads the response and parses the response as a []byte
func readJSON(client Client) Kleisli[Requester, []byte] {
return F.Flow3(
ReadFullResponse(client),
ioresult.ChainFirstEitherK(F.Flow2(
H.Response,
H.ValidateJSONResponse,
)),
ioresult.Map(H.Body),
)
}
// ReadJSON sends a request, reads the response and parses the response as JSON
func ReadJSON[A any](client Client) Kleisli[Requester, A] {
return F.Flow2(
readJSON(client),
ioresult.ChainEitherK(J.Unmarshal[A]),
)
}

View File

@@ -0,0 +1,71 @@
// 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 http
import (
"net"
"net/http"
"testing"
"time"
AR "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
E "github.com/IBM/fp-go/v2/idiomatic/result"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
var expLogBackoff = R.ExponentialBackoff(250 * time.Millisecond)
// our retry policy with a 1s cap
var testLogPolicy = R.CapDelay(
2*time.Second,
R.Monoid.Concat(expLogBackoff, R.LimitRetries(20)),
)
type PostItem struct {
UserID uint `json:"userId"`
Id uint `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
}
func TestRetryHttp(t *testing.T) {
// URLs to try, the first URLs have an invalid hostname
urls := AR.From("https://jsonplaceholder1.typicode.com/posts/1", "https://jsonplaceholder2.typicode.com/posts/1", "https://jsonplaceholder3.typicode.com/posts/1", "https://jsonplaceholder4.typicode.com/posts/1", "https://jsonplaceholder.typicode.com/posts/1")
client := MakeClient(&http.Client{})
action := func(status R.RetryStatus) IOResult[*PostItem] {
return F.Pipe1(
MakeGetRequest(urls[status.IterNumber]),
ReadJSON[*PostItem](client),
)
}
check := E.Fold(
F.Flow2(
errors.As[*net.DNSError](),
O.IsSome[*net.DNSError],
),
F.Constant1[*PostItem](false),
)
_, err := ioresult.Retrying(testLogPolicy, action, check)()
assert.NoError(t, err)
}

View File

@@ -0,0 +1,17 @@
package http
import (
"net/http"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
type (
IOResult[A any] = ioresult.IOResult[A]
Kleisli[A, B any] = ioresult.Kleisli[A, B]
Requester = IOResult[*http.Request]
Client interface {
Do(Requester) IOResult[*http.Response]
}
)

View File

@@ -0,0 +1,796 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"sync"
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/result"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/fromio"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/io"
RES "github.com/IBM/fp-go/v2/result"
)
const (
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
useParallel = true
)
// Left creates an IOResult that represents a failed computation with the given error.
// When executed, it returns the zero value for type A and the provided error.
func Left[A any](l error) IOResult[A] {
return func() (A, error) {
return result.Left[A](l)
}
}
// Right creates an IOResult that represents a successful computation with the given value.
// When executed, it returns the provided value and nil error.
func Right[A any](r A) IOResult[A] {
return func() (A, error) {
return result.Of(r)
}
}
// Of creates an IOResult that represents a successful computation with the given value.
// This is an alias for Right and is the Pointed functor implementation.
//
//go:inline
func Of[A any](r A) IOResult[A] {
return Right(r)
}
// MonadOf is a monadic constructor that wraps a value in an IOResult.
// This is an alias for Of and provides the standard monad interface.
//
//go:inline
func MonadOf[A any](r A) IOResult[A] {
return Of(r)
}
// LeftIO creates an IOResult from an IO computation that produces an error.
// The error from the IO is used as the Left value.
func LeftIO[A any](ml IO[error]) IOResult[A] {
return func() (a A, e error) {
e = ml()
return
}
}
// RightIO creates an IOResult from an IO computation that produces a value.
// The IO is executed and its result is wrapped in a successful IOResult.
func RightIO[A any](mr IO[A]) IOResult[A] {
return func() (A, error) {
return result.Of(mr())
}
}
// FromEither converts an Either (Result[A]) to an IOResult.
// Either's Left becomes an error, Either's Right becomes a successful value.
func FromEither[A any](e Result[A]) IOResult[A] {
return func() (A, error) {
return RES.Unwrap(e)
}
}
// FromResult converts a (value, error) tuple to an IOResult.
// This is the primary way to convert Go's standard error handling pattern to IOResult.
func FromResult[A any](a A, err error) IOResult[A] {
return func() (A, error) {
return a, err
}
}
// FromOption converts an Option (represented as value, bool) to an IOResult.
// If the bool is true, the value is wrapped in a successful IOResult.
// If the bool is false, onNone is called to generate the error.
func FromOption[A any](onNone Lazy[error]) func(A, bool) IOResult[A] {
return func(a A, ok bool) IOResult[A] {
return func() (A, error) {
if ok {
return result.Of(a)
}
return result.Left[A](onNone())
}
}
}
// ChainOptionK chains a function that returns an Option (value, bool).
// The None case (false) is converted to an error using onNone.
func ChainOptionK[A, B any](onNone Lazy[error]) func(func(A) (B, bool)) Operator[A, B] {
return func(f func(A) (B, bool)) Operator[A, B] {
return func(i IOResult[A]) IOResult[B] {
return func() (B, error) {
a, err := i()
if err != nil {
return result.Left[B](err)
}
b, ok := f(a)
if ok {
return result.Of(b)
}
return result.Left[B](onNone())
}
}
}
}
// MonadChainIOK chains an IO kleisli function to an IOResult.
// If the IOResult fails, the function is not executed. Otherwise, the IO is executed and wrapped.
func MonadChainIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[B] {
return fromio.MonadChainIOK(
MonadChain[A, B],
FromIO[B],
ma,
f,
)
}
// ChainIOK returns an operator that chains an IO kleisli function to an IOResult.
// The IO computation is wrapped in a successful IOResult context.
//
//go:inline
func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
return fromio.ChainIOK(
Chain[A, B],
FromIO[B],
f,
)
}
// ChainLazyK returns an operator that chains a lazy computation to an IOResult.
// This is an alias for ChainIOK since Lazy and IO are equivalent.
//
//go:inline
func ChainLazyK[A, B any](f func(A) Lazy[B]) Operator[A, B] {
return ChainIOK(f)
}
// FromIO converts an IO computation to an IOResult that always succeeds.
// This is an alias for RightIO.
//
//go:inline
func FromIO[A any](mr IO[A]) IOResult[A] {
return RightIO(mr)
}
// FromLazy converts a lazy computation to an IOResult that always succeeds.
// This is an alias for FromIO since Lazy and IO are equivalent.
//
//go:inline
func FromLazy[A any](mr Lazy[A]) IOResult[A] {
return FromIO(mr)
}
// MonadMap transforms the value inside an IOResult using the given function.
// If the IOResult is a Left (error), the function is not applied.
func MonadMap[A, B any](fa IOResult[A], f func(A) B) IOResult[B] {
return func() (B, error) {
a, err := fa()
if err != nil {
return result.Left[B](err)
}
return result.Of(f(a))
}
}
// Map returns an operator that transforms values using the given function.
// This is the Functor map operation for IOResult.
func Map[A, B any](f func(A) B) Operator[A, B] {
return function.Bind2nd(MonadMap[A, B], f)
}
// MonadMapTo replaces the value in an IOResult with a constant value.
// If the IOResult is an error, the error is preserved.
//
//go:inline
func MonadMapTo[A, B any](fa IOResult[A], b B) IOResult[B] {
return MonadMap(fa, function.Constant1[A](b))
}
// MapTo returns an operator that replaces the value with a constant.
// This is useful for discarding the result while keeping the computational context.
//
//go:inline
func MapTo[A, B any](b B) Operator[A, B] {
return function.Bind2nd(MonadMapTo[A, B], b)
}
// MonadChain chains a kleisli function that depends on the current value.
// This is the Monad bind operation for IOResult.
func MonadChain[A, B any](fa IOResult[A], f Kleisli[A, B]) IOResult[B] {
return func() (B, error) {
a, err := fa()
if err != nil {
return result.Left[B](err)
}
return f(a)()
}
}
// Chain returns an operator that chains a kleisli function.
// This enables dependent computations where the next step depends on the previous result.
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return function.Bind2nd(MonadChain[A, B], f)
}
// MonadChainEitherK chains a function that returns an Either.
// The Either is converted to IOResult: Left becomes error, Right becomes success.
func MonadChainEitherK[A, B any](ma IOResult[A], f either.Kleisli[error, A, B]) IOResult[B] {
return func() (B, error) {
a, err := ma()
if err != nil {
return result.Left[B](err)
}
return either.Unwrap(f(a))
}
}
// ChainEitherK returns an operator that chains a function returning Either.
// Either's Left becomes an error, Right becomes a successful value.
//
//go:inline
func ChainEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
return function.Bind2nd(MonadChainEitherK[A, B], f)
}
// MonadChainResultK chains a function that returns a (value, error) tuple.
// This allows chaining standard Go functions that return errors.
func MonadChainResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[B] {
return func() (B, error) {
a, err := ma()
if err != nil {
return result.Left[B](err)
}
return f(a)
}
}
// ChainResultK returns an operator that chains a function returning (value, error).
// This enables integration with standard Go error handling patterns.
//
//go:inline
func ChainResultK[A, B any](f result.Kleisli[A, B]) Operator[A, B] {
return function.Bind2nd(MonadChainResultK[A, B], f)
}
// MonadAp applies a function wrapped in IOResult to a value wrapped in IOResult.
// This is the Applicative apply operation. The implementation delegates to either
// the parallel (MonadApPar) or sequential (MonadApSeq) version based on useParallel flag.
//
//go:inline
func MonadAp[B, A any](mab IOResult[func(A) B], ma IOResult[A]) IOResult[B] {
if useParallel {
return MonadApPar(mab, ma)
}
return MonadApSeq(mab, ma)
}
// Ap returns an operator that applies a function in IOResult context.
// This enables applicative-style programming with IOResult values.
//
//go:inline
func Ap[B, A any](ma IOResult[A]) Operator[func(A) B, B] {
if useParallel {
return ApPar[B](ma)
}
return ApSeq[B](ma)
}
// MonadApPar applies a function to a value, executing both in parallel.
// Both IOResults are executed concurrently for better performance.
func MonadApPar[B, A any](mab IOResult[func(A) B], ma IOResult[A]) IOResult[B] {
return func() (B, error) {
var wg sync.WaitGroup
wg.Add(1)
var fab func(A) B
var faberr error
go func() {
defer wg.Done()
fab, faberr = mab()
}()
fa, faerr := ma()
wg.Wait()
if faberr != nil {
return result.Left[B](faberr)
}
if faerr != nil {
return result.Left[B](faerr)
}
return result.Of(fab(fa))
}
}
// ApPar returns an operator that applies a function to a value in parallel.
// This is the operator form of MonadApPar.
//
//go:inline
func ApPar[B, A any](ma IOResult[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadApPar[B, A], ma)
}
// MonadApSeq applies a function to a value sequentially.
// The function IOResult is executed first, then the value IOResult.
func MonadApSeq[B, A any](mab IOResult[func(A) B], ma IOResult[A]) IOResult[B] {
return func() (B, error) {
fab, err := mab()
if err != nil {
return result.Left[B](err)
}
fa, err := ma()
if err != nil {
return result.Left[B](err)
}
return result.Of(fab(fa))
}
}
// ApSeq returns an operator that applies a function to a value sequentially.
// This is the operator form of MonadApSeq.
//
//go:inline
func ApSeq[B, A any](ma IOResult[A]) func(IOResult[func(A) B]) IOResult[B] {
return function.Bind2nd(MonadApSeq[B, A], ma)
}
// Flatten removes one level of nesting from a nested IOResult.
// This is equivalent to joining or flattening the structure: IOResult[IOResult[A]] -> IOResult[A].
//
//go:inline
func Flatten[A any](mma IOResult[IOResult[A]]) IOResult[A] {
return MonadChain(mma, function.Identity[IOResult[A]])
}
// Memoize caches the result of an IOResult so it only executes once.
// Subsequent calls return the cached result without re-executing the computation.
func Memoize[A any](ma IOResult[A]) IOResult[A] {
// synchronization primitives
var once sync.Once
var fa A
var faerr error
// callback
gen := func() {
fa, faerr = ma()
}
// returns our memoized wrapper
return func() (A, error) {
once.Do(gen)
return fa, faerr
}
}
// MonadMapLeft transforms the error value using the given function.
// The success value is left unchanged.
func MonadMapLeft[A any](fa IOResult[A], f Endomorphism[error]) IOResult[A] {
return func() (A, error) {
a, err := fa()
if err != nil {
return result.Left[A](f(err))
}
return result.Of(a)
}
}
// MapLeft returns an operator that transforms the error using the given function.
// This is useful for error wrapping or enrichment.
//
//go:inline
func MapLeft[A any](f Endomorphism[error]) Operator[A, A] {
return function.Bind2nd(MonadMapLeft[A], f)
}
// MonadBiMap transforms both the error (left) and success (right) values.
func MonadBiMap[A, B any](fa IOResult[A], f Endomorphism[error], g func(A) B) IOResult[B] {
return func() (B, error) {
a, err := fa()
if err != nil {
return result.Left[B](f(err))
}
return result.Of(g(a))
}
}
// BiMap returns an operator that transforms both error and success values.
// This is the Bifunctor map operation for IOResult.
//
//go:inline
func BiMap[A, B any](f Endomorphism[error], g func(A) B) Operator[A, B] {
return function.Bind23of3(MonadBiMap[A, B])(f, g)
}
// Fold returns a function that handles both error and success cases, converting to IO.
// This is the operator form of MonadFold for pattern matching on IOResult.
//
//go:inline
func Fold[A, B any](onLeft func(error) IO[B], onRight io.Kleisli[A, B]) func(IOResult[A]) IO[B] {
return function.Bind23of3(MonadFold[A, B])(onLeft, onRight)
}
// GetOrElse extracts the value from an IOResult, using a default IO for error cases.
// This converts an IOResult to an IO that cannot fail.
func GetOrElse[A any](onLeft func(error) IO[A]) func(IOResult[A]) IO[A] {
return func(fa IOResult[A]) IO[A] {
return func() A {
a, err := fa()
if err != nil {
return onLeft(err)()
}
return a
}
}
}
// MonadChainTo chains two IOResults, discarding the first value if successful.
// This is useful for sequencing computations where only the second result matters.
//
//go:inline
func MonadChainTo[A, B any](fa IOResult[A], fb IOResult[B]) IOResult[B] {
return MonadChain(fa, function.Constant1[A](fb))
}
// ChainTo returns an operator that sequences two computations, keeping only the second result.
// This is the operator form of MonadChainTo.
//
//go:inline
func ChainTo[A, B any](fb IOResult[B]) Operator[A, B] {
return function.Bind2nd(MonadChainTo[A, B], fb)
}
// MonadChainFirst chains a computation but returns the original value if both succeed.
// If either computation fails, the error is returned.
func MonadChainFirst[A, B any](ma IOResult[A], f Kleisli[A, B]) IOResult[A] {
return chain.MonadChainFirst(
MonadChain[A, A],
MonadMap[B, A],
ma,
f,
)
}
// MonadTap executes a side effect but returns the original value.
// This is an alias for MonadChainFirst, useful for logging or side effects.
//
//go:inline
func MonadTap[A, B any](ma IOResult[A], f Kleisli[A, B]) IOResult[A] {
return MonadChainFirst(ma, f)
}
// ChainFirst returns an operator that runs a computation for side effects but keeps the original value.
// This is useful for operations like validation or logging.
//
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(
Chain[A, A],
Map[B, A],
f,
)
}
// Tap returns an operator that executes side effects while preserving the original value.
// This is an alias for ChainFirst.
//
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return ChainFirst(f)
}
// MonadChainFirstEitherK runs an Either computation for side effects but returns the original value.
// The Either computation must succeed for the original value to be returned.
func MonadChainFirstEitherK[A, B any](ma IOResult[A], f either.Kleisli[error, A, B]) IOResult[A] {
return func() (A, error) {
a, err := ma()
if err != nil {
return result.Left[A](err)
}
_, err = either.Unwrap(f(a))
if err != nil {
return result.Left[A](err)
}
return result.Of(a)
}
}
// ChainFirstEitherK returns an operator that runs an Either computation for side effects.
// This is useful for validation that may fail.
//
//go:inline
func ChainFirstEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
return function.Bind2nd(MonadChainFirstEitherK[A, B], f)
}
// MonadChainFirstResultK runs a Result computation for side effects but returns the original value.
// The Result computation must succeed for the original value to be returned.
func MonadChainFirstResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
return func() (A, error) {
a, err := ma()
if err != nil {
return result.Left[A](err)
}
_, err = f(a)
if err != nil {
return result.Left[A](err)
}
return result.Of(a)
}
}
// ChainFirstResultK returns an operator that runs a Result computation for side effects.
// This integrates with standard Go error-returning functions.
//
//go:inline
func ChainFirstResultK[A, B any](f result.Kleisli[A, B]) Operator[A, A] {
return function.Bind2nd(MonadChainFirstResultK[A, B], f)
}
// MonadChainFirstIOK runs an IO computation for side effects but returns the original value.
// The IO computation always succeeds, so errors from the original IOResult are preserved.
func MonadChainFirstIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[A] {
return fromio.MonadChainFirstIOK(
MonadChain[A, A],
MonadMap[B, A],
FromIO[B],
ma,
f,
)
}
// ChainFirstIOK returns an operator that runs an IO computation for side effects.
// This is useful for operations like logging that cannot fail.
//
//go:inline
func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return fromio.ChainFirstIOK(
Chain[A, A],
Map[B, A],
FromIO[B],
f,
)
}
// MonadTapEitherK executes an Either computation for side effects.
// This is an alias for MonadChainFirstEitherK.
//
//go:inline
func MonadTapEitherK[A, B any](ma IOResult[A], f either.Kleisli[error, A, B]) IOResult[A] {
return MonadChainFirstEitherK(ma, f)
}
// TapEitherK returns an operator that executes an Either computation for side effects.
// This is an alias for ChainFirstEitherK.
//
//go:inline
func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
return ChainFirstEitherK(f)
}
// MonadTapResultK executes a Result computation for side effects.
// This is an alias for MonadChainFirstResultK.
//
//go:inline
func MonadTapResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
return MonadChainFirstResultK(ma, f)
}
// TapResultK returns an operator that executes a Result computation for side effects.
// This is an alias for ChainFirstResultK.
//
//go:inline
func TapResultK[A, B any](f result.Kleisli[A, B]) Operator[A, A] {
return ChainFirstResultK(f)
}
// MonadTapIOK executes an IO computation for side effects.
// This is an alias for MonadChainFirstIOK.
//
//go:inline
func MonadTapIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[A] {
return MonadChainFirstIOK(ma, f)
}
// TapIOK returns an operator that executes an IO computation for side effects.
// This is an alias for ChainFirstIOK.
//
//go:inline
func TapIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return ChainFirstIOK(f)
}
// MonadFold handles both error and success cases explicitly, converting to an IO.
// This is useful for pattern matching on the IOResult.
func MonadFold[A, B any](ma IOResult[A], onLeft func(error) IO[B], onRight io.Kleisli[A, B]) IO[B] {
return func() B {
a, err := ma()
if err != nil {
return onLeft(err)()
}
return onRight(a)()
}
}
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
// WithResource constructs a bracket pattern for resource management.
// It ensures resources are properly acquired, used, and released even if errors occur.
// The release function is always called, similar to defer.
func WithResource[A, R, ANY any](
onCreate IOResult[R],
onRelease Kleisli[R, ANY],
) Kleisli[Kleisli[R, A], A] {
return func(k Kleisli[R, A]) IOResult[A] {
return func() (A, error) {
r, rerr := onCreate()
if rerr != nil {
return result.Left[A](rerr)
}
a, aerr := k(r)()
_, nerr := onRelease(r)()
if aerr != nil {
return result.Left[A](aerr)
}
if nerr != nil {
return result.Left[A](nerr)
}
return result.Of(a)
}
}
}
// FromImpure converts an impure side-effecting function into an IOResult.
// The function is executed when the IOResult runs, and always succeeds with nil.
func FromImpure(f func()) IOResult[any] {
return function.Pipe2(
f,
io.FromImpure,
FromIO[any],
)
}
// Defer defers the creation of an IOResult until it is executed.
// This allows lazy evaluation of the IOResult itself.
func Defer[A any](gen Lazy[IOResult[A]]) IOResult[A] {
return func() (A, error) {
return gen()()
}
}
// MonadAlt tries the first IOResult, and if it fails, tries the second.
// This provides a fallback mechanism for error recovery.
func MonadAlt[A any](first IOResult[A], second Lazy[IOResult[A]]) IOResult[A] {
return func() (A, error) {
a, err := first()
if err != nil {
return second()()
}
return result.Of(a)
}
}
// Alt returns an operator that provides a fallback for error recovery.
// This identifies an associative operation on a type constructor for the Alternative instance.
func Alt[A any](second Lazy[IOResult[A]]) Operator[A, A] {
return function.Bind2nd(MonadAlt[A], second)
}
// MonadFlap applies a fixed argument to a function inside an IOResult.
// This is the reverse of Ap: the function is wrapped and the argument is fixed.
//
//go:inline
func MonadFlap[B, A any](fab IOResult[func(A) B], a A) IOResult[B] {
return functor.MonadFlap(MonadMap[func(A) B, B], fab, a)
}
// Flap returns an operator that applies a fixed argument to a wrapped function.
// This is useful for partial application with wrapped functions.
//
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return functor.Flap(Map[func(A) B, B], a)
}
// Delay creates an operator that delays execution by the specified duration.
// The IOResult is executed after waiting for the given duration.
func Delay[A any](delay time.Duration) Operator[A, A] {
return func(fa IOResult[A]) IOResult[A] {
return func() (A, error) {
time.Sleep(delay)
return fa()
}
}
}
// After creates an operator that delays execution until the specified timestamp.
// If the timestamp is in the past, the IOResult executes immediately.
func After[A any](timestamp time.Time) Operator[A, A] {
return func(fa IOResult[A]) IOResult[A] {
return func() (A, error) {
// check if we need to wait
current := time.Now()
if current.Before(timestamp) {
time.Sleep(timestamp.Sub(current))
}
return fa()
}
}
}
// MonadChainLeft handles the error case by chaining to a new computation.
// If the IOResult succeeds, it passes through unchanged.
func MonadChainLeft[A any](fa IOResult[A], f Kleisli[error, A]) IOResult[A] {
return func() (A, error) {
a, err := fa()
if err != nil {
return f(err)()
}
return result.Of(a)
}
}
// ChainLeft returns an operator that handles errors with a recovery computation.
// This enables error recovery and transformation.
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
return function.Bind2nd(MonadChainLeft[A], f)
}
// MonadChainFirstLeft runs a computation on the error but always returns the original error.
// This is useful for side effects like logging errors without recovery.
func MonadChainFirstLeft[A, B any](ma IOResult[A], f Kleisli[error, B]) IOResult[A] {
return func() (A, error) {
a, err := ma()
if err != nil {
_, _ = f(err)()
return result.Left[A](err)
}
return result.Of(a)
}
}
// MonadTapLeft executes a side effect on errors without changing the error.
// This is an alias for MonadChainFirstLeft, useful for error logging.
//
//go:inline
func MonadTapLeft[A, B any](ma IOResult[A], f Kleisli[error, B]) IOResult[A] {
return MonadChainFirstLeft(ma, f)
}
// ChainFirstLeft returns an operator that runs a computation on errors for side effects.
// The original error is always preserved.
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return function.Bind2nd(MonadChainFirstLeft[A, B], f)
}
// TapLeft returns an operator that executes side effects on errors.
// This is an alias for ChainFirstLeft.
//
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return ChainFirstLeft[A](f)
}

View File

@@ -0,0 +1,452 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"fmt"
"testing"
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/stretchr/testify/assert"
)
func TestMap(t *testing.T) {
result, err := F.Pipe1(
Of(1),
Map(utils.Double),
)()
assert.NoError(t, err)
assert.Equal(t, 2, result)
}
func TestChainEitherK(t *testing.T) {
f := ChainResultK(func(n int) (int, error) {
if n > 0 {
return n, nil
}
return 0, errors.New("a")
})
result1, err1 := f(Right(1))()
assert.NoError(t, err1)
assert.Equal(t, 1, result1)
_, err2 := f(Right(-1))()
assert.Error(t, err2)
assert.Equal(t, "a", err2.Error())
_, err3 := f(Left[int](fmt.Errorf("b")))()
assert.Error(t, err3)
assert.Equal(t, "b", err3.Error())
}
func TestChainOptionK(t *testing.T) {
f := ChainOptionK[int, int](func() error { return fmt.Errorf("a") })(func(n int) (int, bool) {
if n > 0 {
return n, true
}
return 0, false
})
result1, err1 := f(Right(1))()
assert.NoError(t, err1)
assert.Equal(t, 1, result1)
_, err2 := f(Right(-1))()
assert.Error(t, err2)
assert.Equal(t, "a", err2.Error())
_, err3 := f(Left[int](fmt.Errorf("b")))()
assert.Error(t, err3)
assert.Equal(t, "b", err3.Error())
}
func TestFromOption(t *testing.T) {
f := FromOption[int](func() error { return errors.New("a") })
result1, err1 := f(1, true)()
assert.NoError(t, err1)
assert.Equal(t, 1, result1)
_, err2 := f(0, false)()
assert.Error(t, err2)
assert.Equal(t, "a", err2.Error())
}
func TestChainIOK(t *testing.T) {
f := ChainIOK(func(n int) IO[string] {
return func() string {
return fmt.Sprintf("%d", n)
}
})
result1, err1 := f(Right(1))()
assert.NoError(t, err1)
assert.Equal(t, "1", result1)
_, err2 := f(Left[int](errors.New("b")))()
assert.Error(t, err2)
assert.Equal(t, "b", err2.Error())
}
func TestChainWithIO(t *testing.T) {
r := F.Pipe1(
Of("test"),
ChainIOK(func(s string) IO[bool] {
return func() bool {
return len(s) > 0
}
}),
)
result, err := r()
assert.NoError(t, err)
assert.True(t, result)
}
func TestChainFirst(t *testing.T) {
f := func(a string) IOResult[int] {
if len(a) > 2 {
return Of(len(a))
}
return Left[int](errors.New("foo"))
}
good := Of("foo")
bad := Of("a")
ch := ChainFirst(f)
result1, err1 := F.Pipe1(good, ch)()
assert.NoError(t, err1)
assert.Equal(t, "foo", result1)
_, err2 := F.Pipe1(bad, ch)()
assert.Error(t, err2)
assert.Equal(t, "foo", err2.Error())
}
func TestChainFirstIOK(t *testing.T) {
f := func(a string) IO[int] {
return io.Of(len(a))
}
good := Of("foo")
ch := ChainFirstIOK(f)
result, err := F.Pipe1(good, ch)()
assert.NoError(t, err)
assert.Equal(t, "foo", result)
}
func TestMonadChainLeft(t *testing.T) {
// Test with Left value - should apply the function
t.Run("Left value applies function", func(t *testing.T) {
result := MonadChainLeft(
Left[int](errors.New("error1")),
func(e error) IOResult[int] {
return Left[int](errors.New("transformed: " + e.Error()))
},
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "transformed: error1", err.Error())
})
// Test with Left value - function returns Right (error recovery)
t.Run("Left value recovers to Right", func(t *testing.T) {
result := MonadChainLeft(
Left[int](errors.New("recoverable")),
func(e error) IOResult[int] {
if e.Error() == "recoverable" {
return Right(42)
}
return Left[int](e)
},
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
// Test with Right value - should pass through unchanged
t.Run("Right value passes through", func(t *testing.T) {
result := MonadChainLeft(
Right(100),
func(e error) IOResult[int] {
return Left[int](errors.New("should not be called"))
},
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 100, val)
})
// Test error type transformation
t.Run("Error type transformation", func(t *testing.T) {
result := MonadChainLeft(
Left[int](errors.New("404")),
func(e error) IOResult[int] {
return Left[int](errors.New("404"))
},
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "404", err.Error())
})
}
func TestChainLeft(t *testing.T) {
// Test with Left value - should apply the function
t.Run("Left value applies function", func(t *testing.T) {
chainFn := ChainLeft(func(e error) IOResult[int] {
return Left[int](errors.New("chained: " + e.Error()))
})
result := F.Pipe1(
Left[int](errors.New("original")),
chainFn,
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "chained: original", err.Error())
})
// Test with Left value - function returns Right (error recovery)
t.Run("Left value recovers to Right", func(t *testing.T) {
chainFn := ChainLeft(func(e error) IOResult[int] {
if e.Error() == "network error" {
return Right(0) // default value
}
return Left[int](e)
})
result := F.Pipe1(
Left[int](errors.New("network error")),
chainFn,
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 0, val)
})
// Test with Right value - should pass through unchanged
t.Run("Right value passes through", func(t *testing.T) {
chainFn := ChainLeft(func(e error) IOResult[int] {
return Left[int](errors.New("should not be called"))
})
result := F.Pipe1(
Right(42),
chainFn,
)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
// Test composition with other operations
t.Run("Composition with Map", func(t *testing.T) {
result := F.Pipe2(
Left[int](errors.New("error")),
ChainLeft(func(e error) IOResult[int] {
return Left[int](errors.New("handled: " + e.Error()))
}),
Map(utils.Double),
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "handled: error", err.Error())
})
}
func TestMonadChainFirstLeft(t *testing.T) {
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
sideEffectCalled := false
result := MonadChainFirstLeft(
Left[int](errors.New("original error")),
func(e error) IOResult[int] {
sideEffectCalled = true
return Left[int](errors.New("new error")) // This error is ignored, original is returned
},
)
_, err := result()
assert.True(t, sideEffectCalled)
assert.Error(t, err)
assert.Equal(t, "original error", err.Error())
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var capturedError string
result := MonadChainFirstLeft(
Left[int](errors.New("validation failed")),
func(e error) IOResult[int] {
capturedError = e.Error()
return Right(999) // This Right value is ignored, original Left is returned
},
)
_, err := result()
assert.Equal(t, "validation failed", capturedError)
assert.Error(t, err)
assert.Equal(t, "validation failed", err.Error())
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
sideEffectCalled := false
result := MonadChainFirstLeft(
Right(42),
func(e error) IOResult[int] {
sideEffectCalled = true
return Left[int](errors.New("should not be called"))
},
)
assert.False(t, sideEffectCalled)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
// Test that side effects are executed but original error is always preserved
t.Run("Side effects executed but original error preserved", func(t *testing.T) {
effectCount := 0
result := MonadChainFirstLeft(
Left[int](errors.New("original error")),
func(e error) IOResult[int] {
effectCount++
// Try to return Right, but original Left should still be returned
return Right(999)
},
)
_, err := result()
assert.Equal(t, 1, effectCount)
assert.Error(t, err)
assert.Equal(t, "original error", err.Error())
})
}
func TestChainFirstLeft(t *testing.T) {
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
var captured string
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
captured = e.Error()
return Left[int](errors.New("ignored error"))
})
result := F.Pipe1(
Left[int](errors.New("test error")),
chainFn,
)
_, err := result()
assert.Equal(t, "test error", captured)
assert.Error(t, err)
assert.Equal(t, "test error", err.Error())
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var captured string
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
captured = e.Error()
return Right(42) // This Right is ignored, original Left is returned
})
result := F.Pipe1(
Left[int](errors.New("test error")),
chainFn,
)
_, err := result()
assert.Equal(t, "test error", captured)
assert.Error(t, err)
assert.Equal(t, "test error", err.Error())
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
called := false
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
called = true
return Right(0)
})
result := F.Pipe1(
Right(100),
chainFn,
)
assert.False(t, called)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 100, val)
})
// Test that original error is always preserved regardless of what f returns
t.Run("Original error always preserved", func(t *testing.T) {
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
// Try to return Right, but original Left should still be returned
return Right(999)
})
result := F.Pipe1(
Left[int](errors.New("original")),
chainFn,
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "original", err.Error())
})
// Test with IO side effects - original Left is always preserved
t.Run("IO side effects with Left preservation", func(t *testing.T) {
effectCount := 0
chainFn := ChainFirstLeft[int](func(e error) IOResult[int] {
return FromIO(func() int {
effectCount++
return 0
})
})
// Even though FromIO wraps in Right, the original Left is preserved
result := F.Pipe1(
Left[int](errors.New("error")),
chainFn,
)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "error", err.Error())
assert.Equal(t, 1, effectCount)
})
// Test logging with Left preservation
t.Run("Logging with Left preservation", func(t *testing.T) {
errorLog := []string{}
logError := ChainFirstLeft[string](func(e error) IOResult[string] {
errorLog = append(errorLog, "Logged: "+e.Error())
return Left[string](errors.New("log entry")) // This is ignored, original is preserved
})
result := F.Pipe2(
Left[string](errors.New("step1")),
logError,
ChainLeft(func(e error) IOResult[string] {
return Left[string](errors.New("step2"))
}),
)
_, err := result()
assert.Equal(t, []string{"Logged: step1"}, errorLog)
assert.Error(t, err)
assert.Equal(t, "step2", err.Error())
})
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"encoding/json"
"log"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
// LogJSON converts the argument to pretty printed JSON and then logs it via the format string
// Can be used with [ChainFirst]
func LogJSON[A any](prefix string) Kleisli[A, any] {
return func(a A) IOResult[any] {
// convert to a string
b, jsonerr := json.MarshalIndent(a, "", " ")
// log this
return func() (any, error) {
if jsonerr != nil {
return result.Left[any](jsonerr)
}
log.Printf(prefix, string(b))
return result.Of[any](b)
}
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestLogging(t *testing.T) {
type SomeData struct {
Key string `json:"key"`
Value string `json:"value"`
}
src := &SomeData{Key: "key", Value: "value"}
res := F.Pipe1(
Of(src),
ChainFirst(LogJSON[*SomeData]("Data: \n%s")),
)
dst, err := res()
assert.NoError(t, err)
assert.Equal(t, src, dst)
}

View File

@@ -0,0 +1,132 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/monad"
"github.com/IBM/fp-go/v2/internal/pointed"
)
type (
ioEitherPointed[A any] struct {
fof Kleisli[A, A]
}
ioEitherFunctor[A, B any] struct {
fmap func(func(A) B) Operator[A, B]
}
ioEitherApply[A, B any] struct {
ioEitherFunctor[A, B]
fap func(IOResult[A]) Operator[func(A) B, B]
}
ioEitherChainable[A, B any] struct {
ioEitherApply[A, B]
fchain func(Kleisli[A, B]) Operator[A, B]
}
ioEitherMonad[A, B any] struct {
ioEitherPointed[A]
ioEitherChainable[A, B]
}
)
// Of implements the Pointed interface for IOResult.
func (o *ioEitherPointed[A]) Of(a A) IOResult[A] {
return o.fof(a)
}
// Map implements the Monad interface's Map operation.
func (o *ioEitherFunctor[A, B]) Map(f func(A) B) Operator[A, B] {
return o.fmap(f)
}
// Chain implements the Monad interface's Chain operation.
func (o *ioEitherChainable[A, B]) Chain(f Kleisli[A, B]) Operator[A, B] {
return o.fchain(f)
}
// Ap implements the Monad interface's Ap operation.
func (o *ioEitherApply[A, B]) Ap(fa IOResult[A]) Operator[func(A) B, B] {
return o.fap(fa)
}
// Pointed implements the pointed operations for [IOEither]
// Pointed returns a Pointed instance for IOResult.
// Pointed provides the ability to lift pure values into the IOResult context.
func Pointed[A any]() pointed.Pointed[A, IOResult[A]] {
return &ioEitherPointed[A]{
Of[A],
}
}
// Functor implements the monadic operations for [IOEither]
// Functor returns a Functor instance for IOResult.
// Functor provides the Map operation for transforming values.
func Functor[A, B any]() functor.Functor[A, B, IOResult[A], IOResult[B]] {
return &ioEitherFunctor[A, B]{
Map[A, B],
}
}
// Monad implements the monadic operations for [IOEither]
// Monad returns a Monad instance for IOResult.
// Monad provides the full monadic interface including Map, Chain, and Ap.
func Monad[A, B any]() monad.Monad[A, B, IOResult[A], IOResult[B], IOResult[func(A) B]] {
return MonadPar[A, B]()
}
// Monad implements the monadic operations for [IOEither]
// Monad returns a Monad instance for IOResult.
// Monad provides the full monadic interface including Map, Chain, and Ap.
func MonadPar[A, B any]() monad.Monad[A, B, IOResult[A], IOResult[B], IOResult[func(A) B]] {
return &ioEitherMonad[A, B]{
ioEitherPointed[A]{
Of[A],
},
ioEitherChainable[A, B]{
ioEitherApply[A, B]{
ioEitherFunctor[A, B]{
Map[A, B],
},
ApPar[B, A],
},
Chain[A, B],
},
}
}
// Monad implements the monadic operations for [IOEither]
// Monad returns a Monad instance for IOResult.
// Monad provides the full monadic interface including Map, Chain, and Ap.
func MonadSeq[A, B any]() monad.Monad[A, B, IOResult[A], IOResult[B], IOResult[func(A) B]] {
return &ioEitherMonad[A, B]{
ioEitherPointed[A]{
Of[A],
},
ioEitherChainable[A, B]{
ioEitherApply[A, B]{
ioEitherFunctor[A, B]{
Map[A, B],
},
ApSeq[B, A],
},
Chain[A, B],
},
}
}

View File

@@ -0,0 +1,597 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// Helper function to compare IOResult values
func ioResultEqual[T comparable](a, b IOResult[T]) bool {
valA, errA := a()
valB, errB := b()
if errA != nil && errB != nil {
return errA.Error() == errB.Error()
}
if errA != nil || errB != nil {
return false
}
return valA == valB
}
// TestPointedOf tests that Pointed().Of creates a successful IOResult
func TestPointedOf(t *testing.T) {
t.Run("Creates successful IOResult with integer", func(t *testing.T) {
pointed := Pointed[int]()
result := pointed.Of(42)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
t.Run("Creates successful IOResult with string", func(t *testing.T) {
pointed := Pointed[string]()
result := pointed.Of("hello")
val, err := result()
assert.NoError(t, err)
assert.Equal(t, "hello", val)
})
t.Run("Creates successful IOResult with struct", func(t *testing.T) {
type User struct {
Name string
Age int
}
pointed := Pointed[User]()
user := User{Name: "Alice", Age: 30}
result := pointed.Of(user)
val, err := result()
assert.NoError(t, err)
assert.Equal(t, user, val)
})
}
// TestFunctorMap tests that Functor().Map correctly transforms values
func TestFunctorMap(t *testing.T) {
t.Run("Maps over successful value", func(t *testing.T) {
functor := Functor[int, int]()
io := Of(5)
mapped := functor.Map(func(x int) int { return x * 2 })(io)
val, err := mapped()
assert.NoError(t, err)
assert.Equal(t, 10, val)
})
t.Run("Maps over error preserves error", func(t *testing.T) {
functor := Functor[int, int]()
io := Left[int](errors.New("test error"))
mapped := functor.Map(func(x int) int { return x * 2 })(io)
_, err := mapped()
assert.Error(t, err)
assert.Equal(t, "test error", err.Error())
})
t.Run("Maps with type transformation", func(t *testing.T) {
functor := Functor[int, string]()
io := Of(42)
mapped := functor.Map(func(x int) string { return fmt.Sprintf("value: %d", x) })(io)
val, err := mapped()
assert.NoError(t, err)
assert.Equal(t, "value: 42", val)
})
}
// TestMonadChain tests that Monad().Chain correctly chains computations
func TestMonadChain(t *testing.T) {
t.Run("Chains successful computations", func(t *testing.T) {
monad := Monad[int, int]()
io := monad.Of(5)
chained := monad.Chain(func(x int) IOResult[int] {
return Of(x * 2)
})(io)
val, err := chained()
assert.NoError(t, err)
assert.Equal(t, 10, val)
})
t.Run("Chains with error in first computation", func(t *testing.T) {
monad := Monad[int, int]()
io := Left[int](errors.New("initial error"))
chained := monad.Chain(func(x int) IOResult[int] {
return Of(x * 2)
})(io)
_, err := chained()
assert.Error(t, err)
assert.Equal(t, "initial error", err.Error())
})
t.Run("Chains with error in second computation", func(t *testing.T) {
monad := Monad[int, int]()
io := monad.Of(5)
chained := monad.Chain(func(x int) IOResult[int] {
return Left[int](errors.New("chain error"))
})(io)
_, err := chained()
assert.Error(t, err)
assert.Equal(t, "chain error", err.Error())
})
t.Run("Chains with type transformation", func(t *testing.T) {
monad := Monad[int, string]()
io := Of(42)
chained := monad.Chain(func(x int) IOResult[string] {
return Of(fmt.Sprintf("value: %d", x))
})(io)
val, err := chained()
assert.NoError(t, err)
assert.Equal(t, "value: 42", val)
})
}
// TestMonadAp tests the applicative functionality
func TestMonadAp(t *testing.T) {
t.Run("Applies function to value", func(t *testing.T) {
monad := Monad[int, int]()
fn := Of(func(x int) int { return x * 2 })
val := monad.Of(5)
result := monad.Ap(val)(fn)
res, err := result()
assert.NoError(t, err)
assert.Equal(t, 10, res)
})
t.Run("Error in function", func(t *testing.T) {
monad := Monad[int, int]()
fn := Left[func(int) int](errors.New("function error"))
val := monad.Of(5)
result := monad.Ap(val)(fn)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "function error", err.Error())
})
t.Run("Error in value", func(t *testing.T) {
monad := Monad[int, int]()
fn := Of(func(x int) int { return x * 2 })
val := Left[int](errors.New("value error"))
result := monad.Ap(val)(fn)
_, err := result()
assert.Error(t, err)
assert.Equal(t, "value error", err.Error())
})
}
// Monad Law Tests
// TestMonadLeftIdentity verifies: Chain(Of(a), f) == f(a)
// The left identity law states that wrapping a value with Of and then chaining
// with a function f should be the same as just applying f to the value.
func TestMonadLeftIdentity(t *testing.T) {
t.Run("Left identity with successful function", func(t *testing.T) {
monad := Monad[int, string]()
a := 42
f := func(x int) IOResult[string] {
return Of(fmt.Sprintf("value: %d", x))
}
// Chain(Of(a), f)
left := monad.Chain(f)(monad.Of(a))
// f(a)
right := f(a)
// Both should produce the same result
leftVal, leftErr := left()
rightVal, rightErr := right()
assert.Equal(t, rightErr, leftErr)
assert.Equal(t, rightVal, leftVal)
})
t.Run("Left identity with error-returning function", func(t *testing.T) {
monad := Monad[int, string]()
a := -1
f := func(x int) IOResult[string] {
if x < 0 {
return Left[string](errors.New("negative value"))
}
return Of(fmt.Sprintf("value: %d", x))
}
left := monad.Chain(f)(monad.Of(a))
right := f(a)
leftVal, leftErr := left()
rightVal, rightErr := right()
assert.Equal(t, rightErr, leftErr)
assert.Equal(t, rightVal, leftVal)
})
t.Run("Left identity with multiple values", func(t *testing.T) {
testCases := []int{0, 1, 42, 100, -5}
monad := Monad[int, int]()
f := func(x int) IOResult[int] {
return Of(x * 2)
}
for _, a := range testCases {
left := monad.Chain(f)(monad.Of(a))
right := f(a)
leftVal, leftErr := left()
rightVal, rightErr := right()
assert.Equal(t, rightErr, leftErr, "Errors should match for value %d", a)
assert.Equal(t, rightVal, leftVal, "Values should match for value %d", a)
}
})
}
// TestMonadRightIdentity verifies: Chain(m, Of) == m
// The right identity law states that chaining an IOResult with Of should
// return the original IOResult unchanged.
func TestMonadRightIdentity(t *testing.T) {
t.Run("Right identity with successful value", func(t *testing.T) {
monad := Monad[int, int]()
m := Of(42)
// Chain(m, Of)
chained := monad.Chain(func(x int) IOResult[int] {
return monad.Of(x)
})(m)
// Should be equivalent to m
mVal, mErr := m()
chainedVal, chainedErr := chained()
assert.Equal(t, mErr, chainedErr)
assert.Equal(t, mVal, chainedVal)
})
t.Run("Right identity with error", func(t *testing.T) {
monad := Monad[int, int]()
m := Left[int](errors.New("test error"))
chained := monad.Chain(func(x int) IOResult[int] {
return monad.Of(x)
})(m)
mVal, mErr := m()
chainedVal, chainedErr := chained()
assert.Equal(t, mErr, chainedErr)
assert.Equal(t, mVal, chainedVal)
})
t.Run("Right identity with different types", func(t *testing.T) {
monadStr := Monad[string, string]()
m := Of("hello")
chained := monadStr.Chain(func(x string) IOResult[string] {
return monadStr.Of(x)
})(m)
mVal, mErr := m()
chainedVal, chainedErr := chained()
assert.Equal(t, mErr, chainedErr)
assert.Equal(t, mVal, chainedVal)
})
}
// TestMonadAssociativity verifies: Chain(Chain(m, f), g) == Chain(m, x => Chain(f(x), g))
// The associativity law states that the order of nesting chains doesn't matter.
func TestMonadAssociativity(t *testing.T) {
t.Run("Associativity with successful computations", func(t *testing.T) {
monadIntInt := Monad[int, int]()
monadIntStr := Monad[int, string]()
m := Of(5)
f := func(x int) IOResult[int] {
return Of(x * 2)
}
g := func(y int) IOResult[string] {
return Of(fmt.Sprintf("result: %d", y))
}
// Chain(Chain(m, f), g)
left := monadIntStr.Chain(g)(monadIntInt.Chain(f)(m))
// Chain(m, x => Chain(f(x), g))
right := monadIntStr.Chain(func(x int) IOResult[string] {
return monadIntStr.Chain(g)(f(x))
})(m)
leftVal, leftErr := left()
rightVal, rightErr := right()
assert.Equal(t, rightErr, leftErr)
assert.Equal(t, rightVal, leftVal)
})
t.Run("Associativity with error in first function", func(t *testing.T) {
monadIntInt := Monad[int, int]()
monadIntStr := Monad[int, string]()
m := Of(5)
f := func(x int) IOResult[int] {
return Left[int](errors.New("error in f"))
}
g := func(y int) IOResult[string] {
return Of(fmt.Sprintf("result: %d", y))
}
left := monadIntStr.Chain(g)(monadIntInt.Chain(f)(m))
right := monadIntStr.Chain(func(x int) IOResult[string] {
return monadIntStr.Chain(g)(f(x))
})(m)
leftVal, leftErr := left()
rightVal, rightErr := right()
assert.Equal(t, rightErr, leftErr)
assert.Equal(t, rightVal, leftVal)
})
t.Run("Associativity with error in second function", func(t *testing.T) {
monadIntInt := Monad[int, int]()
monadIntStr := Monad[int, string]()
m := Of(5)
f := func(x int) IOResult[int] {
return Of(x * 2)
}
g := func(y int) IOResult[string] {
return Left[string](errors.New("error in g"))
}
left := monadIntStr.Chain(g)(monadIntInt.Chain(f)(m))
right := monadIntStr.Chain(func(x int) IOResult[string] {
return monadIntStr.Chain(g)(f(x))
})(m)
leftVal, leftErr := left()
rightVal, rightErr := right()
assert.Equal(t, rightErr, leftErr)
assert.Equal(t, rightVal, leftVal)
})
t.Run("Associativity with complex chain", func(t *testing.T) {
monad1 := Monad[int, int]()
monad2 := Monad[int, int]()
m := Of(2)
f := func(x int) IOResult[int] { return Of(x + 3) }
g := func(y int) IOResult[int] { return Of(y * 4) }
// (2 + 3) * 4 = 20
left := monad2.Chain(g)(monad1.Chain(f)(m))
right := monad1.Chain(func(x int) IOResult[int] {
return monad2.Chain(g)(f(x))
})(m)
leftVal, leftErr := left()
rightVal, rightErr := right()
assert.NoError(t, leftErr)
assert.NoError(t, rightErr)
assert.Equal(t, 20, leftVal)
assert.Equal(t, 20, rightVal)
})
}
// TestFunctorComposition verifies: Map(f . g) == Map(f) . Map(g)
// The functor composition law states that mapping a composition of functions
// should be the same as composing the maps of those functions.
func TestFunctorComposition(t *testing.T) {
t.Run("Functor composition law", func(t *testing.T) {
functor1 := Functor[int, int]()
functor2 := Functor[int, string]()
m := Of(5)
f := func(x int) int { return x * 2 }
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
// Map(g . f)
composed := functor2.Map(F.Flow2(f, g))(m)
// Map(g) . Map(f)
separate := functor2.Map(g)(functor1.Map(f)(m))
composedVal, composedErr := composed()
separateVal, separateErr := separate()
assert.Equal(t, composedErr, separateErr)
assert.Equal(t, composedVal, separateVal)
})
t.Run("Functor composition with error", func(t *testing.T) {
functor1 := Functor[int, int]()
functor2 := Functor[int, string]()
m := Left[int](errors.New("test error"))
f := func(x int) int { return x * 2 }
g := func(x int) string { return fmt.Sprintf("value: %d", x) }
composed := functor2.Map(F.Flow2(f, g))(m)
separate := functor2.Map(g)(functor1.Map(f)(m))
composedVal, composedErr := composed()
separateVal, separateErr := separate()
assert.Equal(t, composedErr, separateErr)
assert.Equal(t, composedVal, separateVal)
})
}
// TestFunctorIdentity verifies: Map(id) == id
// The functor identity law states that mapping the identity function
// should return the original IOResult unchanged.
func TestFunctorIdentity(t *testing.T) {
t.Run("Functor identity with successful value", func(t *testing.T) {
functor := Functor[int, int]()
m := Of(42)
// Map(id)
mapped := functor.Map(F.Identity[int])(m)
mVal, mErr := m()
mappedVal, mappedErr := mapped()
assert.Equal(t, mErr, mappedErr)
assert.Equal(t, mVal, mappedVal)
})
t.Run("Functor identity with error", func(t *testing.T) {
functor := Functor[int, int]()
m := Left[int](errors.New("test error"))
mapped := functor.Map(F.Identity[int])(m)
mVal, mErr := m()
mappedVal, mappedErr := mapped()
assert.Equal(t, mErr, mappedErr)
assert.Equal(t, mVal, mappedVal)
})
}
// TestMonadParVsSeq tests that MonadPar and MonadSeq produce the same results
func TestMonadParVsSeq(t *testing.T) {
t.Run("Par and Seq produce same results for Map", func(t *testing.T) {
monadPar := MonadPar[int, int]()
monadSeq := MonadSeq[int, int]()
io := Of(5)
f := func(x int) int { return x * 2 }
par := monadPar.Map(f)(io)
seq := monadSeq.Map(f)(io)
parVal, parErr := par()
seqVal, seqErr := seq()
assert.Equal(t, parErr, seqErr)
assert.Equal(t, parVal, seqVal)
})
t.Run("Par and Seq produce same results for Chain", func(t *testing.T) {
monadPar := MonadPar[int, string]()
monadSeq := MonadSeq[int, string]()
io := Of(42)
f := func(x int) IOResult[string] {
return Of(fmt.Sprintf("value: %d", x))
}
par := monadPar.Chain(f)(io)
seq := monadSeq.Chain(f)(io)
parVal, parErr := par()
seqVal, seqErr := seq()
assert.Equal(t, parErr, seqErr)
assert.Equal(t, parVal, seqVal)
})
t.Run("Default Monad uses parallel execution", func(t *testing.T) {
monadDefault := Monad[int, int]()
monadPar := MonadPar[int, int]()
io := Of(5)
f := func(x int) int { return x * 2 }
def := monadDefault.Map(f)(io)
par := monadPar.Map(f)(io)
defVal, defErr := def()
parVal, parErr := par()
assert.Equal(t, parErr, defErr)
assert.Equal(t, parVal, defVal)
})
}
// TestMonadIntegration tests complete workflows using the monad interface
func TestMonadIntegration(t *testing.T) {
t.Run("Complex pipeline using monad operations", func(t *testing.T) {
monad1 := Monad[int, int]()
monad2 := Monad[int, string]()
// Build a pipeline: multiply by 2, add 3, then format
result := F.Pipe2(
monad1.Of(5),
monad1.Map(func(x int) int { return x * 2 }),
monad1.Chain(func(x int) IOResult[int] {
return Of(x + 3)
}),
)
// Continue with type change
formatted := monad2.Map(func(x int) string {
return fmt.Sprintf("Final: %d", x)
})(result)
val, err := formatted()
assert.NoError(t, err)
assert.Equal(t, "Final: 13", val) // (5 * 2) + 3 = 13
})
t.Run("Error handling in complex pipeline", func(t *testing.T) {
monad1 := Monad[int, int]()
monad2 := Monad[int, string]()
result := F.Pipe2(
monad1.Of(5),
monad1.Map(func(x int) int { return x * 2 }),
monad1.Chain(func(x int) IOResult[int] {
if x > 5 {
return Left[int](errors.New("value too large"))
}
return Of(x + 3)
}),
)
formatted := monad2.Map(func(x int) string {
return fmt.Sprintf("Final: %d", x)
})(result)
_, err := formatted()
assert.Error(t, err)
assert.Equal(t, "value too large", err.Error())
})
}

View File

@@ -0,0 +1,66 @@
// 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 ioresult
import (
"github.com/IBM/fp-go/v2/monoid"
)
type (
Monoid[A any] = monoid.Monoid[IOResult[A]]
)
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
// ApplicativeMonoid returns a Monoid that concatenates IOResult instances via their applicative.
// Uses parallel execution (default Ap behavior).
func ApplicativeMonoid[A any](
m monoid.Monoid[A],
) Monoid[A] {
return monoid.ApplicativeMonoid(
MonadOf[A],
MonadMap[A, func(A) A],
MonadAp[A, A],
m,
)
}
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
// ApplicativeMonoidSeq returns a Monoid that concatenates IOResult instances sequentially.
// Uses sequential execution (ApSeq).
func ApplicativeMonoidSeq[A any](
m monoid.Monoid[A],
) Monoid[A] {
return monoid.ApplicativeMonoid(
MonadOf[A],
MonadMap[A, func(A) A],
MonadApSeq[A, A],
m,
)
}
// ApplicativeMonoid returns a [Monoid] that concatenates [IOEither] instances via their applicative
// ApplicativeMonoidPar returns a Monoid that concatenates IOResult instances in parallel.
// Uses parallel execution (ApPar) explicitly.
func ApplicativeMonoidPar[A any](
m monoid.Monoid[A],
) Monoid[A] {
return monoid.ApplicativeMonoid(
MonadOf[A],
MonadMap[A, func(A) A],
MonadApPar[A, A],
m,
)
}

View File

@@ -0,0 +1,238 @@
// 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 ioresult
import (
"fmt"
"testing"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestApplicativeMonoid(t *testing.T) {
m := ApplicativeMonoid(S.Monoid)
// good cases
result1, err1 := m.Concat(Of("a"), Of("b"))()
assert.NoError(t, err1)
assert.Equal(t, "ab", result1)
result2, err2 := m.Concat(Of("a"), m.Empty())()
assert.NoError(t, err2)
assert.Equal(t, "a", result2)
result3, err3 := m.Concat(m.Empty(), Of("b"))()
assert.NoError(t, err3)
assert.Equal(t, "b", result3)
// bad cases
e1 := fmt.Errorf("e1")
e2 := fmt.Errorf("e2")
_, err4 := m.Concat(Left[string](e1), Of("b"))()
assert.Error(t, err4)
assert.Equal(t, e1, err4)
_, err5 := m.Concat(Left[string](e1), Left[string](e2))()
assert.Error(t, err5)
assert.Equal(t, e1, err5)
_, err6 := m.Concat(Of("a"), Left[string](e2))()
assert.Error(t, err6)
assert.Equal(t, e2, err6)
}
func TestApplicativeMonoidSeq(t *testing.T) {
m := ApplicativeMonoidSeq(S.Monoid)
t.Run("Sequential concatenation of successful values", func(t *testing.T) {
result, err := m.Concat(Of("hello"), Of(" world"))()
assert.NoError(t, err)
assert.Equal(t, "hello world", result)
})
t.Run("Empty element is identity (left)", func(t *testing.T) {
result, err := m.Concat(m.Empty(), Of("test"))()
assert.NoError(t, err)
assert.Equal(t, "test", result)
})
t.Run("Empty element is identity (right)", func(t *testing.T) {
result, err := m.Concat(Of("test"), m.Empty())()
assert.NoError(t, err)
assert.Equal(t, "test", result)
})
t.Run("First error short-circuits", func(t *testing.T) {
e1 := fmt.Errorf("error1")
_, err := m.Concat(Left[string](e1), Of("world"))()
assert.Error(t, err)
assert.Equal(t, e1, err)
})
t.Run("Second error after first succeeds", func(t *testing.T) {
e2 := fmt.Errorf("error2")
_, err := m.Concat(Of("hello"), Left[string](e2))()
assert.Error(t, err)
assert.Equal(t, e2, err)
})
t.Run("Multiple concatenations", func(t *testing.T) {
m := ApplicativeMonoidSeq(S.Monoid)
result, err := m.Concat(
m.Concat(Of("a"), Of("b")),
m.Concat(Of("c"), Of("d")),
)()
assert.NoError(t, err)
assert.Equal(t, "abcd", result)
})
}
func TestApplicativeMonoidPar(t *testing.T) {
m := ApplicativeMonoidPar(N.MonoidSum[int]())
t.Run("Parallel concatenation of successful values", func(t *testing.T) {
result, err := m.Concat(Of(10), Of(20))()
assert.NoError(t, err)
assert.Equal(t, 30, result)
})
t.Run("Empty element is identity (left)", func(t *testing.T) {
result, err := m.Concat(m.Empty(), Of(42))()
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("Empty element is identity (right)", func(t *testing.T) {
result, err := m.Concat(Of(42), m.Empty())()
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("Both empty returns empty", func(t *testing.T) {
result, err := m.Empty()()
assert.NoError(t, err)
assert.Equal(t, 0, result) // 0 is the identity for sum
})
t.Run("First error in parallel", func(t *testing.T) {
e1 := fmt.Errorf("error1")
_, err := m.Concat(Left[int](e1), Of(20))()
assert.Error(t, err)
assert.Equal(t, e1, err)
})
t.Run("Second error in parallel", func(t *testing.T) {
e2 := fmt.Errorf("error2")
_, err := m.Concat(Of(10), Left[int](e2))()
assert.Error(t, err)
assert.Equal(t, e2, err)
})
t.Run("Associativity property", func(t *testing.T) {
// (a <> b) <> c == a <> (b <> c)
a := Of(1)
b := Of(2)
c := Of(3)
left, leftErr := m.Concat(m.Concat(a, b), c)()
right, rightErr := m.Concat(a, m.Concat(b, c))()
assert.NoError(t, leftErr)
assert.NoError(t, rightErr)
assert.Equal(t, 6, left)
assert.Equal(t, 6, right)
})
t.Run("Identity property (left)", func(t *testing.T) {
// empty <> a == a
a := Of(42)
result, err := m.Concat(m.Empty(), a)()
expected, expectedErr := a()
assert.NoError(t, err)
assert.NoError(t, expectedErr)
assert.Equal(t, expected, result)
})
t.Run("Identity property (right)", func(t *testing.T) {
// a <> empty == a
a := Of(42)
result, err := m.Concat(a, m.Empty())()
expected, expectedErr := a()
assert.NoError(t, err)
assert.NoError(t, expectedErr)
assert.Equal(t, expected, result)
})
}
func TestMonoidWithProduct(t *testing.T) {
m := ApplicativeMonoid(N.MonoidProduct[int]())
t.Run("Multiply successful values", func(t *testing.T) {
result, err := m.Concat(Of(3), Of(4))()
assert.NoError(t, err)
assert.Equal(t, 12, result)
})
t.Run("Identity is 1 for product", func(t *testing.T) {
result, err := m.Empty()()
assert.NoError(t, err)
assert.Equal(t, 1, result)
})
t.Run("Multiply with identity", func(t *testing.T) {
result, err := m.Concat(Of(5), m.Empty())()
assert.NoError(t, err)
assert.Equal(t, 5, result)
})
}
func TestMonoidLaws(t *testing.T) {
// Test that all three monoid variants satisfy monoid laws
testMonoidLaws := func(name string, m Monoid[string]) {
t.Run(name, func(t *testing.T) {
a := Of("a")
b := Of("b")
c := Of("c")
t.Run("Associativity: (a <> b) <> c == a <> (b <> c)", func(t *testing.T) {
left, _ := m.Concat(m.Concat(a, b), c)()
right, _ := m.Concat(a, m.Concat(b, c))()
assert.Equal(t, left, right)
})
t.Run("Left identity: empty <> a == a", func(t *testing.T) {
result, _ := m.Concat(m.Empty(), a)()
expected, _ := a()
assert.Equal(t, expected, result)
})
t.Run("Right identity: a <> empty == a", func(t *testing.T) {
result, _ := m.Concat(a, m.Empty())()
expected, _ := a()
assert.Equal(t, expected, result)
})
})
}
testMonoidLaws("ApplicativeMonoid", ApplicativeMonoid(S.Monoid))
testMonoidLaws("ApplicativeMonoidSeq", ApplicativeMonoidSeq(S.Monoid))
testMonoidLaws("ApplicativeMonoidPar", ApplicativeMonoidPar(S.Monoid))
}

View File

@@ -0,0 +1,210 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
)
// Benchmark the closure allocations in Bind
func BenchmarkBindAllocations(b *testing.B) {
type Data struct {
Value int
}
setter := func(v int) func(Data) Data {
return func(d Data) Data {
d.Value = v
return d
}
}
b.Run("Bind", func(b *testing.B) {
io := Of(Data{Value: 0})
f := func(d Data) IOResult[int] { return Of(d.Value * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := Bind(setter, f)(io)
_, _ = result()
}
})
b.Run("DirectChainMap", func(b *testing.B) {
io := Of(Data{Value: 0})
f := func(d Data) IOResult[int] { return Of(d.Value * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Manual inlined version of Bind to see baseline
result := Chain(func(s1 Data) IOResult[Data] {
return Map(func(b int) Data {
return setter(b)(s1)
})(f(s1))
})(io)
_, _ = result()
}
})
}
// Benchmark Map with different patterns
func BenchmarkMapPatterns(b *testing.B) {
b.Run("SimpleFunction", func(b *testing.B) {
io := Of(42)
f := func(x int) int { return x * 2 }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := Map(f)(io)
_, _ = result()
}
})
b.Run("InlinedLambda", func(b *testing.B) {
io := Of(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := Map(func(x int) int { return x * 2 })(io)
_, _ = result()
}
})
b.Run("NestedMaps", func(b *testing.B) {
io := Of(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := F.Pipe3(
io,
Map(func(x int) int { return x + 1 }),
Map(func(x int) int { return x * 2 }),
Map(func(x int) int { return x - 3 }),
)
_, _ = result()
}
})
}
// Benchmark Of patterns
func BenchmarkOfPatterns(b *testing.B) {
b.Run("IntValue", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
io := Of(42)
_, _ = io()
}
})
b.Run("StructValue", func(b *testing.B) {
type Data struct {
A int
B string
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
io := Of(Data{A: 42, B: "test"})
_, _ = io()
}
})
b.Run("PointerValue", func(b *testing.B) {
type Data struct {
A int
B string
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
io := Of(&Data{A: 42, B: "test"})
_, _ = io()
}
})
}
// Benchmark the internal chain implementation
func BenchmarkChainPatterns(b *testing.B) {
b.Run("SimpleChain", func(b *testing.B) {
io := Of(42)
f := func(x int) IOResult[int] { return Of(x * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := Chain(f)(io)
_, _ = result()
}
})
b.Run("ChainSequence", func(b *testing.B) {
f1 := func(x int) IOResult[int] { return Of(x + 1) }
f2 := func(x int) IOResult[int] { return Of(x * 2) }
f3 := func(x int) IOResult[int] { return Of(x - 3) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := F.Pipe3(
Of(42),
Chain(f1),
Chain(f2),
Chain(f3),
)
_, _ = result()
}
})
}
// Benchmark error handling paths
func BenchmarkErrorPaths(b *testing.B) {
b.Run("SuccessPath", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := F.Pipe2(
Of(42),
Map(func(x int) int { return x * 2 }),
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
)
_, _ = result()
}
})
b.Run("ErrorPath", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
result := F.Pipe2(
Left[int](errors.New("error")),
Map(func(x int) int { return x * 2 }),
Chain(func(x int) IOResult[int] { return Of(x + 1) }),
)
_, _ = result()
}
})
}

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/result"
R "github.com/IBM/fp-go/v2/retry"
)
// Retrying retries an IOResult computation according to a retry policy.
// The action receives retry status information on each attempt.
// The check function determines if the result warrants another retry.
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(A, error) bool,
) IOResult[A] {
fromResult := io.Retrying(policy,
func(rs R.RetryStatus) IO[Result[A]] {
return func() Result[A] {
return result.TryCatchError(action(rs)())
}
},
func(a Result[A]) bool {
return check(result.Unwrap(a))
},
)
return func() (A, error) {
return result.Unwrap(fromResult())
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"fmt"
"testing"
"time"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
var expLogBackoff = R.ExponentialBackoff(10 * time.Millisecond)
// our retry policy with a 1s cap
var testLogPolicy = R.CapDelay(
2*time.Second,
R.Monoid.Concat(expLogBackoff, R.LimitRetries(20)),
)
func TestRetry(t *testing.T) {
action := func(status R.RetryStatus) IOResult[string] {
if status.IterNumber < 5 {
return Left[string](fmt.Errorf("retrying %d", status.IterNumber))
}
return Of(fmt.Sprintf("Retrying %d", status.IterNumber))
}
check := func(val string, err error) bool {
return err != nil
}
r := Retrying(testLogPolicy, action, check)
result, err := r()
assert.NoError(t, err)
assert.Equal(t, "Retrying 5", result)
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"github.com/IBM/fp-go/v2/semigroup"
)
type (
Semigroup[A any] = semigroup.Semigroup[IOResult[A]]
)
// AltSemigroup is a [Semigroup] that tries the first item and then the second one using an alternative
// AltSemigroup creates a Semigroup that tries the first IOResult, then the second on failure.
// This implements the alternative operation for combining IOResults.
func AltSemigroup[A any]() Semigroup[A] {
return semigroup.AltSemigroup(
MonadAlt[A],
)
}

View File

@@ -0,0 +1,120 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAltSemigroup(t *testing.T) {
sg := AltSemigroup[int]()
t.Run("First succeeds, second not evaluated", func(t *testing.T) {
first := Of(42)
second := Of(100)
result, err := sg.Concat(first, second)()
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("First fails, second succeeds", func(t *testing.T) {
first := Left[int](errors.New("first error"))
second := Of(100)
result, err := sg.Concat(first, second)()
assert.NoError(t, err)
assert.Equal(t, 100, result)
})
t.Run("Both fail, returns second error", func(t *testing.T) {
first := Left[int](errors.New("first error"))
second := Left[int](errors.New("second error"))
_, err := sg.Concat(first, second)()
assert.Error(t, err)
assert.Equal(t, "second error", err.Error())
})
t.Run("Both succeed, returns first", func(t *testing.T) {
first := Of(42)
second := Of(100)
result, err := sg.Concat(first, second)()
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("Associativity property", func(t *testing.T) {
// (a <> b) <> c == a <> (b <> c)
a := Left[int](errors.New("a"))
b := Left[int](errors.New("b"))
c := Of(42)
// Left associative: (a <> b) <> c
left, leftErr := sg.Concat(sg.Concat(a, b), c)()
// Right associative: a <> (b <> c)
right, rightErr := sg.Concat(a, sg.Concat(b, c))()
assert.NoError(t, leftErr)
assert.NoError(t, rightErr)
assert.Equal(t, left, right)
assert.Equal(t, 42, left)
})
t.Run("Multiple alternatives", func(t *testing.T) {
sg := AltSemigroup[string]()
first := Left[string](errors.New("error1"))
second := Left[string](errors.New("error2"))
third := Left[string](errors.New("error3"))
fourth := Of("success")
result, err := sg.Concat(
sg.Concat(sg.Concat(first, second), third),
fourth,
)()
assert.NoError(t, err)
assert.Equal(t, "success", result)
})
}
func TestAltSemigroupWithStrings(t *testing.T) {
sg := AltSemigroup[string]()
t.Run("Concatenate successful string results", func(t *testing.T) {
first := Of("hello")
second := Of("world")
result, err := sg.Concat(first, second)()
assert.NoError(t, err)
assert.Equal(t, "hello", result) // First one wins
})
t.Run("Fallback to second on first failure", func(t *testing.T) {
first := Left[string](errors.New("network error"))
second := Of("fallback value")
result, err := sg.Concat(first, second)()
assert.NoError(t, err)
assert.Equal(t, "fallback value", result)
})
}

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"fmt"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
TST "github.com/IBM/fp-go/v2/internal/testing"
"testing"
)
func TestMapSeq(t *testing.T) {
var results []string
handler := func(value string) IOResult[string] {
return func() (string, error) {
results = append(results, value)
return value, nil
}
}
src := A.From("a", "b", "c")
res := F.Pipe2(
src,
TraverseArraySeq(handler),
Map(func(data []string) bool {
return assert.Equal(t, data, results)
}),
)
result, err := res()
assert.NoError(t, err)
assert.True(t, result)
}
func TestSequenceArray(t *testing.T) {
s := TST.SequenceArrayTest(
FromStrictEquals[bool](),
Pointed[string](),
Pointed[bool](),
Functor[[]string, bool](),
SequenceArray[string],
)
for i := 0; i < 10; i++ {
t.Run(fmt.Sprintf("TestSequenceArray %d", i), s(i))
}
}
func TestSequenceArrayError(t *testing.T) {
s := TST.SequenceArrayErrorTest(
FromStrictEquals[bool](),
Left[string],
Left[bool],
Pointed[string](),
Pointed[bool](),
Functor[[]string, bool](),
SequenceArray[string],
)
// run across four bits
s(4)(t)
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"context"
)
// WithLock executes the provided IO operation in the scope of a lock
// WithLock executes an IOResult within the scope of a lock.
// The lock is acquired before execution and released after (via defer).
func WithLock[A any](lock IO[context.CancelFunc]) Operator[A, A] {
return func(fa IOResult[A]) IOResult[A] {
return func() (A, error) {
defer lock()()
return fa()
}
}
}

View File

@@ -0,0 +1,76 @@
// 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 testing
import (
"testing"
"github.com/IBM/fp-go/v2/either"
EQ "github.com/IBM/fp-go/v2/eq"
L "github.com/IBM/fp-go/v2/internal/monad/testing"
"github.com/IBM/fp-go/v2/ioeither"
)
// AssertLaws asserts the apply monad laws for the `IOEither` monad
func AssertLaws[E, A, B, C any](t *testing.T,
eqe EQ.Eq[E],
eqa EQ.Eq[A],
eqb EQ.Eq[B],
eqc EQ.Eq[C],
ab func(A) B,
bc func(B) C,
) func(a A) bool {
return L.AssertLaws(t,
ioeither.Eq(either.Eq(eqe, eqa)),
ioeither.Eq(either.Eq(eqe, eqb)),
ioeither.Eq(either.Eq(eqe, eqc)),
ioeither.Of[E, A],
ioeither.Of[E, B],
ioeither.Of[E, C],
ioeither.Of[E, func(A) A],
ioeither.Of[E, func(A) B],
ioeither.Of[E, func(B) C],
ioeither.Of[E, func(func(A) B) B],
ioeither.MonadMap[E, A, A],
ioeither.MonadMap[E, A, B],
ioeither.MonadMap[E, A, C],
ioeither.MonadMap[E, B, C],
ioeither.MonadMap[E, func(B) C, func(func(A) B) func(A) C],
ioeither.MonadChain[E, A, A],
ioeither.MonadChain[E, A, B],
ioeither.MonadChain[E, A, C],
ioeither.MonadChain[E, B, C],
ioeither.MonadAp[A, E, A],
ioeither.MonadAp[B, E, A],
ioeither.MonadAp[C, E, B],
ioeither.MonadAp[C, E, A],
ioeither.MonadAp[B, E, func(A) B],
ioeither.MonadAp[func(A) C, E, func(A) B],
ab,
bc,
)
}

View File

@@ -0,0 +1,48 @@
// 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 testing
import (
"fmt"
"testing"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
)
func TestMonadLaws(t *testing.T) {
// some comparison
eqe := EQ.FromStrictEquals[string]()
eqa := EQ.FromStrictEquals[bool]()
eqb := EQ.FromStrictEquals[int]()
eqc := EQ.FromStrictEquals[string]()
ab := func(a bool) int {
if a {
return 1
}
return 0
}
bc := func(b int) string {
return fmt.Sprintf("value %d", b)
}
laws := AssertLaws(t, eqe, eqa, eqb, eqc, ab, bc)
assert.True(t, laws(true))
assert.True(t, laws(false))
}

View File

@@ -0,0 +1,234 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/internal/record"
)
// TraverseArray transforms an array
// TraverseArray transforms an array by applying an IOResult-producing function to each element.
// Uses parallel execution by default. If any element fails, the entire traversal fails.
//
//go:inline
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
Ap[[]B, B],
f,
)
}
// TraverseArrayWithIndex transforms an array
// TraverseArrayWithIndex transforms an array with access to element indices.
// Uses parallel execution by default.
//
//go:inline
func TraverseArrayWithIndex[A, B any](f func(int, A) IOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
Ap[[]B, B],
f,
)
}
// SequenceArray converts a homogeneous sequence of either into an either of sequence
// SequenceArray converts an array of IOResults into an IOResult of an array.
// Uses parallel execution by default.
func SequenceArray[A any](ma []IOResult[A]) IOResult[[]A] {
return TraverseArray(function.Identity[IOResult[A]])(ma)
}
// TraverseRecord transforms a record
// TraverseRecord transforms a map by applying an IOResult-producing function to each value.
// Uses parallel execution by default.
//
//go:inline
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return record.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
Ap[map[K]B, B],
f,
)
}
// TraverseRecordWithIndex transforms a record
// TraverseRecordWithIndex transforms a map with access to keys.
// Uses parallel execution by default.
//
//go:inline
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IOResult[B]) Kleisli[map[K]A, map[K]B] {
return record.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
Ap[map[K]B, B],
f,
)
}
// SequenceRecord converts a homogeneous sequence of either into an either of sequence
// SequenceRecord converts a map of IOResults into an IOResult of a map.
// Uses parallel execution by default.
func SequenceRecord[K comparable, A any](ma map[K]IOResult[A]) IOResult[map[K]A] {
return TraverseRecord[K](function.Identity[IOResult[A]])(ma)
}
// TraverseArraySeq transforms an array
// TraverseArraySeq transforms an array sequentially.
// Elements are processed one at a time in order.
//
//go:inline
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
ApSeq[[]B, B],
f,
)
}
// TraverseArrayWithIndexSeq transforms an array
// TraverseArrayWithIndexSeq transforms an array sequentially with indices.
//
//go:inline
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
ApSeq[[]B, B],
f,
)
}
// SequenceArraySeq converts a homogeneous sequence of either into an either of sequence
// SequenceArraySeq converts an array of IOResults sequentially.
func SequenceArraySeq[A any](ma []IOResult[A]) IOResult[[]A] {
return TraverseArraySeq(function.Identity[IOResult[A]])(ma)
}
// TraverseRecordSeq transforms a record
// TraverseRecordSeq transforms a map sequentially.
//
//go:inline
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return record.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
ApSeq[map[K]B, B],
f,
)
}
// TraverseRecordWithIndexSeq transforms a record
// TraverseRecordWithIndexSeq transforms a map sequentially with keys.
//
//go:inline
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) IOResult[B]) Kleisli[map[K]A, map[K]B] {
return record.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
ApSeq[map[K]B, B],
f,
)
}
// SequenceRecordSeq converts a homogeneous sequence of either into an either of sequence
// SequenceRecordSeq converts a map of IOResults sequentially.
func SequenceRecordSeq[K comparable, A any](ma map[K]IOResult[A]) IOResult[map[K]A] {
return TraverseRecordSeq[K](function.Identity[IOResult[A]])(ma)
}
// TraverseArrayPar transforms an array
// TraverseArrayPar transforms an array in parallel (explicit).
// This is equivalent to TraverseArray but makes parallelism explicit.
//
//go:inline
func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
ApPar[[]B, B],
f,
)
}
// TraverseArrayWithIndexPar transforms an array
// TraverseArrayWithIndexPar transforms an array in parallel with indices (explicit).
//
//go:inline
func TraverseArrayWithIndexPar[A, B any](f func(int, A) IOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
ApPar[[]B, B],
f,
)
}
// SequenceArrayPar converts a homogeneous Paruence of either into an either of Paruence
// SequenceArrayPar converts an array of IOResults in parallel (explicit).
func SequenceArrayPar[A any](ma []IOResult[A]) IOResult[[]A] {
return TraverseArrayPar(function.Identity[IOResult[A]])(ma)
}
// TraverseRecordPar transforms a record
// TraverseRecordPar transforms a map in parallel (explicit).
//
//go:inline
func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return record.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
ApPar[map[K]B, B],
f,
)
}
// TraverseRecordWithIndexPar transforms a record
// TraverseRecordWithIndexPar transforms a map in parallel with keys (explicit).
//
//go:inline
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) IOResult[B]) Kleisli[map[K]A, map[K]B] {
return record.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
ApSeq[map[K]B, B],
f,
)
}
// SequenceRecordPar converts a homogeneous Paruence of either into an either of Paruence
// SequenceRecordPar converts a map of IOResults in parallel (explicit).
func SequenceRecordPar[K comparable, A any](ma map[K]IOResult[A]) IOResult[map[K]A] {
return TraverseRecordPar[K](function.Identity[IOResult[A]])(ma)
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
"github.com/stretchr/testify/assert"
)
func TestTraverseArray(t *testing.T) {
src := A.From("A", "B")
trfrm := TraverseArrayWithIndex(func(idx int, data string) IOResult[string] {
return Of(fmt.Sprintf("idx: %d, data: %s", idx, data))
})
result, err := trfrm(src)()
assert.NoError(t, err)
assert.Equal(t, A.From("idx: 0, data: A", "idx: 1, data: B"), result)
}

View File

@@ -0,0 +1,39 @@
package ioresult
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
// IO represents a computation that performs side effects and returns a value of type A.
IO[A any] = io.IO[A]
// Lazy represents a deferred computation that produces a value of type A when evaluated.
Lazy[A any] = lazy.Lazy[A]
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
Result[A any] = result.Result[A]
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// Endomorphism represents a function from type A to type A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// IOResult represents a computation that performs IO and may fail with an error.
// It follows Go's idiomatic pattern of returning (value, error) tuples.
// A successful computation returns (value, nil), while a failed one returns (zero, error).
IOResult[A any] = func() (A, error)
// Kleisli represents a function from A to an IOResult of B.
// It is used for chaining computations that may fail.
Kleisli[A, B any] = Reader[A, IOResult[B]]
// Operator represents a transformation from IOResult[A] to IOResult[B].
// It is commonly used in function composition pipelines.
Operator[A, B any] = Kleisli[IOResult[A], B]
)

View File

@@ -232,5 +232,3 @@
package option
//go:generate go run .. option --count 10 --filename gen.go
// Made with Bob

View File

@@ -0,0 +1,94 @@
// 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 (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
)
// TraverseArray applies a ReaderResult-returning function to each element of an array,
// collecting the results. If any element fails, the entire operation fails with the first error.
//
// Example:
//
// parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
// ids := []int{1, 2, 3}
// result := readerresult.TraverseArray[DB](parseUser)(ids)
// // result(db) returns ([]User, nil) with all users or (nil, error) on first error
//
//go:inline
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
return array.Traverse[[]A](
Of[R, []B],
Map[R, []B, func(B) []B],
Ap[[]B, R, B],
f,
)
}
//go:inline
func MonadTraverseArray[R, A, B any](as []A, f Kleisli[R, A, B]) ReaderResult[R, []B] {
return array.MonadTraverse[[]A](
Of[R, []B],
Map[R, []B, func(B) []B],
Ap[[]B, R, B],
as,
f,
)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index.
// This is useful when the transformation depends on the position in the array.
//
// Example:
//
// processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
// return readerresult.Of[Config](idx + len(item))
// }
// items := []string{"a", "bb", "ccc"}
// result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)
//
//go:inline
func TraverseArrayWithIndex[R, A, B any](f func(int, A) ReaderResult[R, B]) Kleisli[R, []A, []B] {
return array.TraverseWithIndex[[]A](
Of[R, []B],
Map[R, []B, func(B) []B],
Ap[[]B, R, B],
f,
)
}
// SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array.
// If any element fails, the entire operation fails with the first error encountered.
// All computations share the same environment.
//
// Example:
//
// readers := []readerresult.ReaderResult[Config, int]{
// readerresult.Of[Config](1),
// readerresult.Of[Config](2),
// readerresult.Of[Config](3),
// }
// result := readerresult.SequenceArray(readers)
// // result(cfg) returns ([]int{1, 2, 3}, nil)
//
//go:inline
func SequenceArray[R, A any](ma []ReaderResult[R, A]) ReaderResult[R, []A] {
return MonadTraverseArray(ma, F.Identity[ReaderResult[R, A]])
}

View File

@@ -0,0 +1,42 @@
// 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"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestSequenceArray(t *testing.T) {
n := 10
readers := A.MakeBy(n, Of[context.Context, int])
exp := A.MakeBy(n, F.Identity[int])
g := F.Pipe1(
readers,
SequenceArray[context.Context, int],
)
v, err := g(context.Background())
assert.NoError(t, err)
assert.Equal(t, exp, v)
}

View File

@@ -0,0 +1,283 @@
// 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"
"testing"
F "github.com/IBM/fp-go/v2/function"
)
type BenchContext struct {
Value int
}
// Benchmark basic operations
func BenchmarkOf(b *testing.B) {
ctx := BenchContext{Value: 42}
b.ResetTimer()
for i := 0; i < b.N; i++ {
rr := Of[BenchContext](i)
_, _ = rr(ctx)
}
}
func BenchmarkLeft(b *testing.B) {
ctx := BenchContext{Value: 42}
err := testError
b.ResetTimer()
for i := 0; i < b.N; i++ {
rr := Left[BenchContext, int](err)
_, _ = rr(ctx)
}
}
func BenchmarkMap(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](10)
double := func(x int) int { return x * 2 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
mapped := F.Pipe1(rr, Map[BenchContext](double))
_, _ = mapped(ctx)
}
}
func BenchmarkMapChain(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](1)
double := func(x int) int { return x * 2 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := F.Pipe3(
rr,
Map[BenchContext](double),
Map[BenchContext](double),
Map[BenchContext](double),
)
_, _ = result(ctx)
}
}
func BenchmarkChain(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](10)
addOne := func(x int) ReaderResult[BenchContext, int] {
return Of[BenchContext](x + 1)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
chained := F.Pipe1(rr, Chain(addOne))
_, _ = chained(ctx)
}
}
func BenchmarkChainDeep(b *testing.B) {
ctx := BenchContext{Value: 42}
rr := Of[BenchContext](1)
addOne := func(x int) ReaderResult[BenchContext, int] {
return Of[BenchContext](x + 1)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := F.Pipe5(
rr,
Chain(addOne),
Chain(addOne),
Chain(addOne),
Chain(addOne),
Chain(addOne),
)
_, _ = result(ctx)
}
}
func BenchmarkAp(b *testing.B) {
ctx := BenchContext{Value: 42}
fab := Of[BenchContext](func(x int) int { return x * 2 })
fa := Of[BenchContext](21)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := MonadAp(fab, fa)
_, _ = result(ctx)
}
}
func BenchmarkSequenceT2(b *testing.B) {
ctx := BenchContext{Value: 42}
rr1 := Of[BenchContext](10)
rr2 := Of[BenchContext](20)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := SequenceT2(rr1, rr2)
_, _ = result(ctx)
}
}
func BenchmarkSequenceT4(b *testing.B) {
ctx := BenchContext{Value: 42}
rr1 := Of[BenchContext](10)
rr2 := Of[BenchContext](20)
rr3 := Of[BenchContext](30)
rr4 := Of[BenchContext](40)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := SequenceT4(rr1, rr2, rr3, rr4)
_, _ = result(ctx)
}
}
func BenchmarkDoNotation(b *testing.B) {
ctx := context.Background()
type State struct {
A int
B int
C int
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := F.Pipe3(
Do[context.Context](State{}),
Bind(
func(a int) func(State) State {
return func(s State) State { s.A = a; return s }
},
func(s State) ReaderResult[context.Context, int] {
return Of[context.Context](10)
},
),
Bind(
func(b int) func(State) State {
return func(s State) State { s.B = b; return s }
},
func(s State) ReaderResult[context.Context, int] {
return Of[context.Context](s.A * 2)
},
),
Bind(
func(c int) func(State) State {
return func(s State) State { s.C = c; return s }
},
func(s State) ReaderResult[context.Context, int] {
return Of[context.Context](s.A + s.B)
},
),
)
_, _ = result(ctx)
}
}
func BenchmarkErrorPropagation(b *testing.B) {
ctx := BenchContext{Value: 42}
err := testError
rr := Left[BenchContext, int](err)
double := func(x int) int { return x * 2 }
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := F.Pipe5(
rr,
Map[BenchContext](double),
Map[BenchContext](double),
Map[BenchContext](double),
Map[BenchContext](double),
Map[BenchContext](double),
)
_, _ = result(ctx)
}
}
func BenchmarkTraverseArray(b *testing.B) {
ctx := BenchContext{Value: 42}
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
kleisli := func(x int) ReaderResult[BenchContext, int] {
return Of[BenchContext](x * 2)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
traversed := TraverseArray[BenchContext](kleisli)
result := traversed(arr)
_, _ = result(ctx)
}
}
func BenchmarkSequenceArray(b *testing.B) {
ctx := BenchContext{Value: 42}
arr := []ReaderResult[BenchContext, int]{
Of[BenchContext](1),
Of[BenchContext](2),
Of[BenchContext](3),
Of[BenchContext](4),
Of[BenchContext](5),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := SequenceArray(arr)
_, _ = result(ctx)
}
}
// Real-world scenario benchmarks
func BenchmarkRealWorldPipeline(b *testing.B) {
type Config struct {
Multiplier int
Offset int
}
ctx := Config{Multiplier: 5, Offset: 10}
type State struct {
Input int
Result int
}
getMultiplier := func(cfg Config) int { return cfg.Multiplier }
getOffset := func(cfg Config) int { return cfg.Offset }
b.ResetTimer()
for i := 0; i < b.N; i++ {
step1 := Bind(
func(m int) func(State) State {
return func(s State) State { s.Result = s.Input * m; return s }
},
func(s State) ReaderResult[Config, int] {
return Asks(getMultiplier)
},
)
step2 := Bind(
func(off int) func(State) State {
return func(s State) State { s.Result += off; return s }
},
func(s State) ReaderResult[Config, int] {
return Asks(getOffset)
},
)
result := F.Pipe3(
Do[Config](State{Input: 10}),
step1,
step2,
Map[Config](func(s State) int { return s.Result }),
)
_, _ = result(ctx)
}
}

View File

@@ -0,0 +1,664 @@
// 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 (
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/result"
AP "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
FE "github.com/IBM/fp-go/v2/internal/fromeither"
FR "github.com/IBM/fp-go/v2/internal/fromreader"
FC "github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
// 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.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
// result := readereither.Do[Env, error](State{})
//
//go:inline
func Do[R, S any](
empty S,
) ReaderResult[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 the shared environment.
//
// 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
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readereither.ReaderResult[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// },
// ),
// readereither.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) readereither.ReaderResult[Env, error, Config] {
// // This can access s.User from the previous step
// return readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfigForUser(s.User.ID)
// })
// },
// ),
// )
//
//go:inline
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[R, S1, T],
) Operator[R, S1, S2] {
return C.Bind(
Chain[R, S1, S2],
Map[R, T, S2],
setter,
f,
)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
//
//go:inline
func Let[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[R, S1, S2] {
return FC.Let(
Map[R, S1, S2],
setter,
f,
)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
//
//go:inline
func LetTo[R, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(ReaderResult[R, S1]) ReaderResult[R, S2] {
return FC.LetTo(
Map[R, S1, S2],
setter,
b,
)
}
// BindTo initializes a new state [S1] from a value [T]
//
//go:inline
func BindTo[R, S1, T any](
setter func(T) S1,
) Operator[R, T, S1] {
return C.BindTo(
Map[R, T, S1],
setter,
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other.
//
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
// and can conceptually run in parallel.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// // These operations are independent and can be combined with ApS
// getUser := readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfig()
// })
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.ApS(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// readereither.ApS(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// )
//
//go:inline
func ApS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderResult[R, T],
) Operator[R, S1, S2] {
return AP.ApS(
Ap[S2, R, T],
Map[R, S1, func(T) S2],
setter,
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfig()
// })
// result := F.Pipe2(
// readereither.Of[Env, error](State{}),
// readereither.ApSL(configLens, getConfig),
// )
//
//go:inline
func ApSL[R, S, T any](
lens L.Lens[S, T],
fa ReaderResult[R, T],
) Operator[R, S, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a ReaderEither computation that produces an updated value.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.BindL(userLens, func(user User) readereither.ReaderResult[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// }),
// )
//
//go:inline
func BindL[R, S, T any](
lens L.Lens[S, T],
f Kleisli[R, T, T],
) Operator[R, S, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a ReaderEither).
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// result := F.Pipe2(
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
// readereither.LetL(configLens, func(cfg Config) Config {
// cfg.Port = 8080
// return cfg
// }),
// )
//
//go:inline
func LetL[R, S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[R, S, S] {
return Let[R](lens.Set, F.Flow2(lens.Get, f))
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// newConfig := Config{Host: "localhost", Port: 8080}
// result := F.Pipe2(
// readereither.Do[any, error](State{}),
// readereither.LetToL(configLens, newConfig),
// )
//
//go:inline
func LetToL[R, S, T any](
lens L.Lens[S, T],
b T,
) Operator[R, S, S] {
return LetTo[R](lens.Set, b)
}
// BindReaderK lifts a Reader Kleisli arrow into a ReaderResult context and binds it to the state.
// This allows you to integrate pure Reader computations (that don't have error handling)
// into a ReaderResult computation chain.
//
// The function f takes the current state S1 and returns a Reader[R, T] computation.
// The result T is then attached to the state using the setter to produce state S2.
//
// Example:
//
// type Env struct {
// ConfigPath string
// }
// type State struct {
// Config string
// }
//
// // A pure Reader computation that reads from environment
// getConfigPath := func(s State) reader.Reader[Env, string] {
// return func(env Env) string {
// return env.ConfigPath
// }
// }
//
// result := F.Pipe2(
// readerresult.Do[Env](State{}),
// readerresult.BindReaderK(
// func(path string) func(State) State {
// return func(s State) State { s.Config = path; return s }
// },
// getConfigPath,
// ),
// )
//
//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 FR.BindReaderK(
Chain[R, S1, S2],
Map[R, T, S2],
FromReader[R, T],
setter,
f,
)
}
//go:inline
func BindEitherK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f RES.Kleisli[S1, T],
) Operator[R, S1, S2] {
return FE.BindEitherK(
Chain[R, S1, S2],
Map[R, T, S2],
FromEither[R, T],
setter,
f,
)
}
// BindResultK lifts a Result Kleisli arrow into a ReaderResult context and binds it to the state.
// This allows you to integrate Result computations (that may fail with an error but don't need
// environment access) into a ReaderResult computation chain.
//
// The function f takes the current state S1 and returns a Result[T] computation.
// If the Result is successful, the value T is attached to the state using the setter to produce state S2.
// If the Result is an error, the entire computation short-circuits with that error.
//
// Example:
//
// type State struct {
// Value int
// ParsedValue int
// }
//
// // A Result computation that may fail
// parseValue := func(s State) result.Result[int] {
// if s.Value < 0 {
// return result.Error[int](errors.New("negative value"))
// }
// return result.Of(s.Value * 2)
// }
//
// result := F.Pipe2(
// readerresult.Do[any](State{Value: 5}),
// readerresult.BindResultK(
// func(parsed int) func(State) State {
// return func(s State) State { s.ParsedValue = parsed; return s }
// },
// parseValue,
// ),
// )
//
//go:inline
func BindResultK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f result.Kleisli[S1, T],
) Operator[R, S1, S2] {
return C.Bind(
Chain[R, S1, S2],
Map[R, T, S2],
setter,
func(s1 S1) ReaderResult[R, T] {
return FromResult[R](f(s1))
},
)
}
// BindToReader initializes a new state S1 from a Reader[R, T] computation.
// This is used to start a ReaderResult computation chain from a pure Reader value.
//
// The setter function takes the result T from the Reader and initializes the state S1.
// This is useful when you want to begin a do-notation chain with a Reader computation
// that doesn't involve error handling.
//
// Example:
//
// type Env struct {
// ConfigPath string
// }
// type State struct {
// Config string
// }
//
// // A Reader that just reads from the environment
// getConfigPath := func(env Env) string {
// return env.ConfigPath
// }
//
// result := F.Pipe1(
// reader.Of[Env](getConfigPath),
// readerresult.BindToReader(func(path string) State {
// return State{Config: path}
// }),
// )
//
//go:inline
func BindToReader[
R, S1, T any](
setter func(T) S1,
) func(Reader[R, T]) ReaderResult[R, S1] {
return function.Flow2(
FromReader[R],
BindTo[R](setter),
)
}
//go:inline
func BindToEither[
R, S1, T any](
setter func(T) S1,
) func(Result[T]) ReaderResult[R, S1] {
return function.Flow2(
FromEither[R],
BindTo[R](setter),
)
}
// BindToResult initializes a new state S1 from a Result[T] value.
// This is used to start a ReaderResult computation chain from a Result that may contain an error.
//
// The setter function takes the successful result T and initializes the state S1.
// If the Result is an error, the entire computation will carry that error forward.
// This is useful when you want to begin a do-notation chain with a Result computation
// that doesn't need environment access.
//
// Example:
//
// type State struct {
// Value int
// }
//
// // A Result that might contain an error
// parseResult := result.TryCatch(func() int {
// // some parsing logic that might fail
// return 42
// })
//
// computation := F.Pipe1(
// parseResult,
// readerresult.BindToResult[any](func(value int) State {
// return State{Value: value}
// }),
// )
//
//go:inline
func BindToResult[
R, S1, T any](
setter func(T) S1,
) func(T, error) ReaderResult[R, S1] {
bt := BindTo[R](setter)
return func(t T, err error) ReaderResult[R, S1] {
return bt(FromResult[R](t, err))
}
}
// ApReaderS attaches a value from a pure Reader computation to a context [S1] to produce a context [S2]
// using Applicative semantics (independent, non-sequential composition).
//
// Unlike BindReaderK which uses monadic bind (sequential), ApReaderS uses applicative apply,
// meaning the Reader computation fa is independent of the current state and can conceptually
// execute in parallel.
//
// This is useful when you want to combine a Reader computation with your ReaderResult state
// without creating a dependency between them.
//
// Example:
//
// type Env struct {
// DefaultPort int
// DefaultHost string
// }
// type State struct {
// Port int
// Host string
// }
//
// getDefaultPort := func(env Env) int { return env.DefaultPort }
// getDefaultHost := func(env Env) string { return env.DefaultHost }
//
// result := F.Pipe2(
// readerresult.Do[Env](State{}),
// readerresult.ApReaderS(
// func(port int) func(State) State {
// return func(s State) State { s.Port = port; return s }
// },
// getDefaultPort,
// ),
// readerresult.ApReaderS(
// func(host string) func(State) State {
// return func(s State) State { s.Host = host; return s }
// },
// getDefaultHost,
// ),
// )
//
//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[R](fa),
)
}
//go:inline
func ApResultS[
R, S1, S2, T any](
setter func(T) func(S1) S2,
) func(T, error) Operator[R, S1, S2] {
return func(t T, err error) Operator[R, S1, S2] {
return ApS(
setter,
FromResult[R](t, err),
)
}
}
// ApResultS attaches a value from a Result to a context [S1] to produce a context [S2]
// using Applicative semantics (independent, non-sequential composition).
//
// Unlike BindResultK which uses monadic bind (sequential), ApResultS uses applicative apply,
// meaning the Result computation fa is independent of the current state and can conceptually
// execute in parallel.
//
// If the Result fa contains an error, the entire computation short-circuits with that error.
// This is useful when you want to combine a Result value with your ReaderResult state
// without creating a dependency between them.
//
// Example:
//
// type State struct {
// Value1 int
// Value2 int
// }
//
// // Independent Result computations
// parseValue1 := result.TryCatch(func() int { return 42 })
// parseValue2 := result.TryCatch(func() int { return 100 })
//
// computation := F.Pipe2(
// readerresult.Do[any](State{}),
// readerresult.ApResultS(
// func(v int) func(State) State {
// return func(s State) State { s.Value1 = v; return s }
// },
// parseValue1,
// ),
// readerresult.ApResultS(
// func(v int) func(State) State {
// return func(s State) State { s.Value2 = v; return s }
// },
// parseValue2,
// ),
// )
//
//go:inline
func ApEitherS[
R, S1, S2, T any](
setter func(T) func(S1) S2,
fa Result[T],
) Operator[R, S1, S2] {
return ApS(
setter,
FromEither[R](fa),
)
}

View File

@@ -0,0 +1,338 @@
// 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"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/stretchr/testify/assert"
)
func getLastName(s utils.Initial) ReaderResult[context.Context, string] {
return Of[context.Context]("Doe")
}
func getGivenName(s utils.WithLastName) ReaderResult[context.Context, string] {
return Of[context.Context]("John")
}
func TestBind(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map[context.Context](utils.GetFullName),
)
v, err := res(context.Background())
assert.NoError(t, err)
assert.Equal(t, "John Doe", v)
}
func TestApS(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
ApS(utils.SetLastName, Of[context.Context]("Doe")),
ApS(utils.SetGivenName, Of[context.Context]("John")),
Map[context.Context](utils.GetFullName),
)
v, err := res(context.Background())
assert.NoError(t, err)
assert.Equal(t, "John Doe", v)
}
func TestBindReaderK(t *testing.T) {
type Env struct {
ConfigPath string
}
type State struct {
Config string
}
// A pure Reader computation
getConfigPath := func(s State) func(Env) string {
return func(env Env) string {
return env.ConfigPath
}
}
res := F.Pipe2(
Do[Env](State{}),
BindReaderK(
func(path string) func(State) State {
return func(s State) State {
s.Config = path
return s
}
},
getConfigPath,
),
Map[Env](func(s State) string { return s.Config }),
)
env := Env{ConfigPath: "/etc/config"}
v, err := res(env)
assert.NoError(t, err)
assert.Equal(t, "/etc/config", v)
}
func TestBindResultK(t *testing.T) {
type State struct {
Value int
ParsedValue int
}
// A Result computation that may fail
parseValue := func(s State) (int, error) {
if s.Value < 0 {
return 0, assert.AnError
}
return s.Value * 2, nil
}
t.Run("success case", func(t *testing.T) {
res := F.Pipe2(
Do[context.Context](State{Value: 5}),
BindResultK[context.Context](
func(parsed int) func(State) State {
return func(s State) State {
s.ParsedValue = parsed
return s
}
},
parseValue,
),
Map[context.Context](func(s State) int { return s.ParsedValue }),
)
v, err := res(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, v)
})
t.Run("error case", func(t *testing.T) {
res := F.Pipe2(
Do[context.Context](State{Value: -5}),
BindResultK[context.Context](
func(parsed int) func(State) State {
return func(s State) State {
s.ParsedValue = parsed
return s
}
},
parseValue,
),
Map[context.Context](func(s State) int { return s.ParsedValue }),
)
_, err := res(context.Background())
assert.Error(t, err)
})
}
func TestBindToReader(t *testing.T) {
type Env struct {
ConfigPath string
}
type State struct {
Config string
}
// A Reader that just reads from the environment
getConfigPath := func(env Env) string {
return env.ConfigPath
}
res := F.Pipe2(
getConfigPath,
BindToReader[Env](func(path string) State {
return State{Config: path}
}),
Map[Env](func(s State) string { return s.Config }),
)
env := Env{ConfigPath: "/etc/config"}
v, err := res(env)
assert.NoError(t, err)
assert.Equal(t, "/etc/config", v)
}
func TestBindToResult(t *testing.T) {
type State struct {
Value int
}
t.Run("success case", func(t *testing.T) {
computation := F.Pipe1(
BindToResult[context.Context](func(value int) State {
return State{Value: value}
})(42, nil),
Map[context.Context](func(s State) int { return s.Value }),
)
v, err := computation(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, v)
})
t.Run("error case", func(t *testing.T) {
computation := F.Pipe1(
BindToResult[context.Context](func(value int) State {
return State{Value: value}
})(0, assert.AnError),
Map[context.Context](func(s State) int { return s.Value }),
)
_, err := computation(context.Background())
assert.Error(t, err)
})
}
func TestApReaderS(t *testing.T) {
type Env struct {
DefaultPort int
DefaultHost string
}
type State struct {
Port int
Host string
}
getDefaultPort := func(env Env) int { return env.DefaultPort }
getDefaultHost := func(env Env) string { return env.DefaultHost }
res := F.Pipe3(
Do[Env](State{}),
ApReaderS(
func(port int) func(State) State {
return func(s State) State {
s.Port = port
return s
}
},
getDefaultPort,
),
ApReaderS(
func(host string) func(State) State {
return func(s State) State {
s.Host = host
return s
}
},
getDefaultHost,
),
Map[Env](func(s State) State { return s }),
)
env := Env{DefaultPort: 8080, DefaultHost: "localhost"}
state, err := res(env)
assert.NoError(t, err)
assert.Equal(t, 8080, state.Port)
assert.Equal(t, "localhost", state.Host)
}
func TestApResultS(t *testing.T) {
type State struct {
Value1 int
Value2 int
}
t.Run("success case", func(t *testing.T) {
computation := F.Pipe3(
Do[context.Context](State{}),
ApResultS[context.Context](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
)(42, nil),
ApResultS[context.Context](
func(v int) func(State) State {
return func(s State) State {
s.Value2 = v
return s
}
},
)(100, nil),
Map[context.Context](func(s State) State { return s }),
)
state, err := computation(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value1)
assert.Equal(t, 100, state.Value2)
})
t.Run("error in first value", func(t *testing.T) {
computation := F.Pipe3(
Do[context.Context](State{}),
ApResultS[context.Context](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
)(0, assert.AnError),
ApResultS[context.Context](
func(v int) func(State) State {
return func(s State) State {
s.Value2 = v
return s
}
},
)(100, nil),
Map[context.Context](func(s State) State { return s }),
)
_, err := computation(context.Background())
assert.Error(t, err)
})
t.Run("error in second value", func(t *testing.T) {
computation := F.Pipe3(
Do[context.Context](State{}),
ApResultS[context.Context](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
)(42, nil),
ApResultS[context.Context](
func(v int) func(State) State {
return func(s State) State {
s.Value2 = v
return s
}
},
)(0, assert.AnError),
Map[context.Context](func(s State) State { return s }),
)
_, err := computation(context.Background())
assert.Error(t, err)
})
}

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