1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-09 23:11:40 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
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
Dr. Carsten Leue
8a2e9539b1 fix: add result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-17 20:36:06 +01:00
Dr. Carsten Leue
03d9720a29 fix: optimize performance for option
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-17 12:19:24 +01:00
110 changed files with 11685 additions and 424 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

@@ -0,0 +1,11 @@
{
"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:*)"
],
"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

View File

@@ -0,0 +1,344 @@
# Deep Chaining Performance Analysis
## Executive Summary
The **only remaining performance gap** between `v2/option` and `idiomatic/option` is in **deep chaining operations** (multiple sequential transformations). This document demonstrates the problem, explains the root cause, and provides recommendations.
## Benchmark Results
### v2/option (Struct-based)
```
BenchmarkChain_3Steps 8.17 ns/op 0 allocs
BenchmarkChain_5Steps 16.57 ns/op 0 allocs
BenchmarkChain_10Steps 47.01 ns/op 0 allocs
BenchmarkMap_5Steps 0.28 ns/op 0 allocs ⚡
```
### idiomatic/option (Tuple-based)
```
BenchmarkChain_3Steps 0.22 ns/op 0 allocs ⚡
BenchmarkChain_5Steps 0.22 ns/op 0 allocs ⚡
BenchmarkChain_10Steps 0.21 ns/op 0 allocs ⚡
BenchmarkMap_5Steps 0.22 ns/op 0 allocs ⚡
```
### Performance Comparison
| Steps | v2/option | idiomatic/option | Slowdown |
|-------|-----------|------------------|----------|
| 3 | 8.17 ns | 0.22 ns | **37x slower** |
| 5 | 16.57 ns | 0.22 ns | **75x slower** |
| 10 | 47.01 ns | 0.21 ns | **224x slower** |
**Key Finding**: The performance gap **increases linearly** with chain depth!
---
## Visual Example: The Problem
### Scenario: Processing User Input
```go
// Process user input through multiple validation steps
input := "42"
// v2/option - Nested MonadChain
result := MonadChain(
MonadChain(
MonadChain(
Some(input),
validateNotEmpty, // Step 1
),
parseToInt, // Step 2
),
validateRange, // Step 3
)
```
### What Happens Under the Hood
#### v2/option (Struct Construction Overhead)
```go
// Step 0: Initial value
Some(input)
// Creates: Option[string]{value: "42", isSome: true}
// Memory: HEAP allocation
// Step 1: Validate not empty
MonadChain(opt, validateNotEmpty)
// Input: Option[string]{value: "42", isSome: true} ← Read from heap
// Output: Option[string]{value: "42", isSome: true} ← NEW heap allocation
// Memory: 2 heap allocations
// Step 2: Parse to int
MonadChain(opt, parseToInt)
// Input: Option[string]{value: "42", isSome: true} ← Read from heap
// Output: Option[int]{value: 42, isSome: true} ← NEW heap allocation
// Memory: 3 heap allocations
// Step 3: Validate range
MonadChain(opt, validateRange)
// Input: Option[int]{value: 42, isSome: true} ← Read from heap
// Output: Option[int]{value: 42, isSome: true} ← NEW heap allocation
// Memory: 4 heap allocations TOTAL
// Each step:
// 1. Reads Option struct from memory
// 2. Checks isSome field
// 3. Calls function
// 4. Creates NEW Option struct
// 5. Writes to memory
```
#### idiomatic/option (Zero Allocation)
```go
// Step 0: Initial value
s, ok := Some(input)
// Creates: ("42", true)
// Memory: STACK only (registers)
// Step 1: Validate not empty
v1, ok1 := Chain(validateNotEmpty)(s, ok)
// Input: ("42", true) ← Values in registers
// Output: ("42", true) ← Values in registers
// Memory: ZERO allocations
// Step 2: Parse to int
v2, ok2 := Chain(parseToInt)(v1, ok1)
// Input: ("42", true) ← Values in registers
// Output: (42, true) ← Values in registers
// Memory: ZERO allocations
// Step 3: Validate range
v3, ok3 := Chain(validateRange)(v2, ok2)
// Input: (42, true) ← Values in registers
// Output: (42, true) ← Values in registers
// Memory: ZERO allocations TOTAL
// Each step:
// 1. Reads values from registers (no memory access!)
// 2. Checks bool flag
// 3. Calls function
// 4. Returns new tuple (stays in registers)
// 5. Compiler optimizes everything away!
```
---
## Assembly-Level Difference
### v2/option - Struct Overhead
```asm
; Every chain step does:
MOV RAX, [heap_ptr] ; Load struct from heap
TEST BYTE [RAX+8], 1 ; Check isSome field
JZ none_case ; Branch if None
MOV RDI, [RAX] ; Load value from struct
CALL transform_func ; Call the function
CALL malloc ; Allocate new struct ⚠️
MOV [new_ptr], result ; Store result
MOV [new_ptr+8], 1 ; Set isSome = true
```
### idiomatic/option - Optimized Away
```asm
; All steps compiled to:
MOV EAX, 42 ; The final result!
; Everything else optimized away! ⚡
```
**Compiler insight**: With tuples, the Go compiler can:
1. **Inline everything** - No function call overhead
2. **Eliminate branches** - Constant propagation removes `if ok` checks
3. **Use registers only** - Values never touch memory
4. **Dead code elimination** - Removes unnecessary operations
---
## Real-World Example with Timings
### Example: User Registration Validation Chain
```go
// Validate: email → trim → lowercase → check format → check uniqueness
```
#### v2/option Performance
```go
func ValidateEmail_v2(email string) Option[string] {
return MonadChain(
MonadChain(
MonadChain(
MonadChain(
Some(email),
trimWhitespace, // ~2 ns
),
toLowerCase, // ~2 ns
),
validateFormat, // ~2 ns
),
checkUniqueness, // ~2 ns
)
}
// Total: ~8-16 ns (matches our 5-step benchmark: 16.57 ns)
```
#### idiomatic/option Performance
```go
func ValidateEmail_idiomatic(email string) (string, bool) {
v1, ok1 := Chain(trimWhitespace)(email, true)
v2, ok2 := Chain(toLowerCase)(v1, ok1)
v3, ok3 := Chain(validateFormat)(v2, ok2)
return Chain(checkUniqueness)(v3, ok3)
}
// Total: ~0.22 ns (entire chain optimized to single operation!)
```
**Impact**: For 1 million validations:
- v2/option: 16.57 ms
- idiomatic/option: 0.22 ms
- **Difference: 75x faster = saved 16.35 ms**
---
## Why Map is Fast in v2/option
Interestingly, `Map` (pure transformations) is **much faster** than `Chain`:
```
v2/option:
- BenchmarkChain_5Steps: 16.57 ns
- BenchmarkMap_5Steps: 0.28 ns ← 59x FASTER!
```
**Reason**: Map transformations can be **inlined and fused** by the compiler:
```go
// This:
Map(f5)(Map(f4)(Map(f3)(Map(f2)(Map(f1)(opt)))))
// Becomes (after compiler optimization):
Some(f5(f4(f3(f2(f1(value)))))) // Single struct construction!
// While Chain cannot be optimized the same way:
MonadChain(MonadChain(...)) // Must construct at each step
```
---
## When Does This Matter?
### ⚠️ **Rarely Critical** (99% of use cases)
Even 10-step chains only cost **47 nanoseconds**. For context:
- Database query: **~1,000,000 ns** (1 ms)
- HTTP request: **~10,000,000 ns** (10 ms)
- File I/O: **~100,000 ns** (0.1 ms)
**The 47 ns overhead is negligible compared to real I/O operations.**
### ⚡ **Can Matter** (High-throughput scenarios)
1. **In-memory data processing pipelines**
```go
// Processing 10 million records with 5-step validation
v2/option: 165 ms
idiomatic/option: 2 ms
Difference: 163 ms saved ⚡
```
2. **Real-time stream processing**
- Processing 100k events/second with chained transformations
- 16.57 ns × 100,000 = 1.66 ms vs 0.22 ns × 100,000 = 0.022 ms
- Can affect throughput for high-frequency trading, gaming, etc.
3. **Tight inner loops with chained logic**
```go
for i := 0; i < 1_000_000; i++ {
result := Chain(f1).Chain(f2).Chain(f3).Chain(f4)(data[i])
}
// v2/option: 16 ms
// idiomatic: 0.22 ms
```
---
## Root Cause Summary
| Aspect | v2/option | idiomatic/option | Why? |
|--------|-----------|------------------|------|
| **Intermediate values** | `Option[T]` struct | `(T, bool)` tuple | Struct requires memory, tuple can use registers |
| **Memory allocation** | 1 per step | 0 total | Heap vs stack |
| **Compiler optimization** | Limited | Aggressive | Structs block inlining |
| **Cache impact** | Heap reads | Register-only | Memory bandwidth saved |
| **Branch prediction** | Struct checks | Optimized away | Compiler removes branches |
---
## Recommendations
### ✅ **Use v2/option When:**
- I/O-bound operations (database, network, files)
- User-facing applications (latency dominated by I/O)
- Need JSON marshaling, TryCatch, SequenceArray
- Chain depth < 5 steps (overhead < 20 ns - negligible)
- Code clarity > microsecond performance
### ✅ **Use idiomatic/option When:**
- CPU-bound data processing
- High-throughput stream processing
- Tight inner loops with chaining
- In-memory analytics
- Performance-critical paths
- Chain depth > 5 steps
### ✅ **Mitigation for v2/option:**
If you need v2/option but want better chain performance:
1. **Use Map instead of Chain** when possible:
```go
// Bad (16.57 ns):
MonadChain(MonadChain(MonadChain(opt, f1), f2), f3)
// Good (0.28 ns):
Map(f3)(Map(f2)(Map(f1)(opt)))
```
2. **Batch operations**:
```go
// Instead of chaining many steps:
validate := func(x T) Option[T] {
// Combine multiple checks in one function
if check1(x) && check2(x) && check3(x) {
return Some(transform(x))
}
return None[T]()
}
```
3. **Profile first**:
- Only optimize hot paths
- 47 ns is often acceptable
- Don't premature optimize
---
## Conclusion
**The deep chaining performance gap is:**
- ✅ **Real and measurable** (37-224x slower)
- ✅ **Well understood** (struct construction overhead)
- ⚠️ **Rarely critical** (nanosecond differences usually don't matter)
- ✅ **Easy to work around** (use Map, batch operations)
- ✅ **Worth it for the API benefits** (JSON, methods, helpers)
**For 99% of applications, v2/option's performance is excellent.** The gap only matters in specialized high-throughput scenarios where you should probably use idiomatic/option anyway.
The optimizations already applied (`//go:inline`, direct field access) brought v2/option to **competitive parity** for all practical purposes. The remaining gap is a **fundamental design trade-off**, not a fixable bug.

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.

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

@@ -20,6 +20,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
@@ -203,7 +204,7 @@ func TestLetL(t *testing.T) {
)
t.Run("LetL with pure transformation", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
result := F.Pipe1(
Right[error](Counter{Value: 21}),
@@ -215,7 +216,7 @@ func TestLetL(t *testing.T) {
})
t.Run("LetL with Left input", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
result := F.Pipe1(
Left[Counter](assert.AnError),
@@ -227,8 +228,8 @@ func TestLetL(t *testing.T) {
})
t.Run("LetL with multiple transformations", func(t *testing.T) {
double := func(v int) int { return v * 2 }
addTen := func(v int) int { return v + 10 }
double := N.Mul(2)
addTen := N.Add(10)
result := F.Pipe2(
Right[error](Counter{Value: 5}),
@@ -241,7 +242,7 @@ func TestLetL(t *testing.T) {
})
t.Run("LetL with identity transformation", func(t *testing.T) {
identity := func(v int) int { return v }
identity := F.Identity[int]
result := F.Pipe1(
Right[error](Counter{Value: 42}),
@@ -315,7 +316,7 @@ func TestLensOperationsCombined(t *testing.T) {
)
t.Run("Combine LetToL and LetL", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
result := F.Pipe2(
Right[error](Counter{Value: 100}),
@@ -328,7 +329,7 @@ func TestLensOperationsCombined(t *testing.T) {
})
t.Run("Combine LetL and BindL", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
validate := func(v int) Either[error, int] {
if v > 100 {
return Left[int](assert.AnError)

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,21 @@ 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[E, R, A, 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

@@ -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, E](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
}

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

@@ -0,0 +1,355 @@
// 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 two 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
// }
//
// # 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]
//
// # 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)
//
// 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
//
// # 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),
// )
//
// ## 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
// }
//
// # 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
//
// 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
package idiomatic

View File

@@ -30,13 +30,13 @@ package option
// result := TraverseArrayG[[]string, []int](parse)([]string{"1", "x", "3"}) // None
func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
return func(g GA) (GB, bool) {
var bs GB
for _, a := range g {
bs := make(GB, len(g))
for i, a := range g {
b, bok := f(a)
if !bok {
return bs, false
}
bs = append(bs, b)
bs[i] = b
}
return bs, true
}
@@ -69,13 +69,13 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
// result := TraverseArrayWithIndexG[[]string, []string](f)([]string{"a", "b"}) // Some(["0:a", "1:b"])
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) (B, bool)) Kleisli[GA, GB] {
return func(g GA) (GB, bool) {
var bs GB
bs := make(GB, len(g))
for i, a := range g {
b, bok := f(i, a)
if !bok {
return bs, false
}
bs = append(bs, b)
bs[i] = b
}
return bs, true
}

View File

@@ -0,0 +1,181 @@
// 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 option
import (
"testing"
)
// Benchmark basic construction
func BenchmarkSome(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = Some(42)
}
}
func BenchmarkNone(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = None[int]()
}
}
// Benchmark basic operations
func BenchmarkIsSome(b *testing.B) {
v, ok := Some(42)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = IsSome(v, ok)
}
}
func BenchmarkMap(b *testing.B) {
v, ok := Some(21)
mapper := Map(func(x int) int { return x * 2 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = mapper(v, ok)
}
}
func BenchmarkChain(b *testing.B) {
v, ok := Some(21)
chainer := Chain(func(x int) (int, bool) {
if x > 0 {
return x * 2, true
}
return 0, false
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = chainer(v, ok)
}
}
func BenchmarkFilter(b *testing.B) {
v, ok := Some(42)
filter := Filter(func(x int) bool { return x > 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = filter(v, ok)
}
}
func BenchmarkGetOrElse(b *testing.B) {
v, ok := Some(42)
getter := GetOrElse(func() int { return 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = getter(v, ok)
}
}
// Benchmark collection operations
func BenchmarkTraverseArray_Small(b *testing.B) {
data := []int{1, 2, 3, 4, 5}
traverser := TraverseArray(func(x int) (int, bool) {
return x * 2, true
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = traverser(data)
}
}
func BenchmarkTraverseArray_Large(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
traverser := TraverseArray(func(x int) (int, bool) {
return x * 2, true
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = traverser(data)
}
}
// Benchmark do-notation
func BenchmarkDoBind(b *testing.B) {
type State struct {
x int
y int
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
s1, ok1 := Do(State{})
s2, ok2 := Bind(
func(x int) func(State) State {
return func(s State) State {
s.x = x
return s
}
},
func(s State) (int, bool) { return 10, true },
)(s1, ok1)
_, _ = Bind(
func(y int) func(State) State {
return func(s State) State {
s.y = y
return s
}
},
func(s State) (int, bool) { return 20, true },
)(s2, ok2)
}
}
// Benchmark conversions
func BenchmarkFromPredicate(b *testing.B) {
pred := FromPredicate(func(x int) bool { return x > 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = pred(42)
}
}
func BenchmarkFromNillable(b *testing.B) {
val := 42
ptr := &val
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FromNillable(ptr)
}
}
// Benchmark complex chains
func BenchmarkComplexChain(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Some(1)
v2, ok2 := Chain(func(x int) (int, bool) { return x + 1, true })(v1, ok1)
v3, ok3 := Chain(func(x int) (int, bool) { return x * 2, true })(v2, ok2)
_, _ = Chain(func(x int) (int, bool) { return x - 5, true })(v3, ok3)
}
}

View File

@@ -0,0 +1,123 @@
// 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 option
import (
"testing"
)
// Benchmark shallow chain (1 step)
func BenchmarkChain_1Step(b *testing.B) {
v, ok := Some(1)
chainer := Chain(func(x int) (int, bool) { return x + 1, true })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = chainer(v, ok)
}
}
// Benchmark moderate chain (3 steps)
func BenchmarkChain_3Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Chain(func(x int) (int, bool) { return x + 1, true })(v, ok)
v2, ok2 := Chain(func(x int) (int, bool) { return x * 2, true })(v1, ok1)
_, _ = Chain(func(x int) (int, bool) { return x - 5, true })(v2, ok2)
}
}
// Benchmark deep chain (5 steps)
func BenchmarkChain_5Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Chain(func(x int) (int, bool) { return x + 1, true })(v, ok)
v2, ok2 := Chain(func(x int) (int, bool) { return x * 2, true })(v1, ok1)
v3, ok3 := Chain(func(x int) (int, bool) { return x - 5, true })(v2, ok2)
v4, ok4 := Chain(func(x int) (int, bool) { return x * 10, true })(v3, ok3)
_, _ = Chain(func(x int) (int, bool) { return x + 100, true })(v4, ok4)
}
}
// Benchmark very deep chain (10 steps)
func BenchmarkChain_10Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Chain(func(x int) (int, bool) { return x + 1, true })(v, ok)
v2, ok2 := Chain(func(x int) (int, bool) { return x * 2, true })(v1, ok1)
v3, ok3 := Chain(func(x int) (int, bool) { return x - 5, true })(v2, ok2)
v4, ok4 := Chain(func(x int) (int, bool) { return x * 10, true })(v3, ok3)
v5, ok5 := Chain(func(x int) (int, bool) { return x + 100, true })(v4, ok4)
v6, ok6 := Chain(func(x int) (int, bool) { return x - 50, true })(v5, ok5)
v7, ok7 := Chain(func(x int) (int, bool) { return x * 3, true })(v6, ok6)
v8, ok8 := Chain(func(x int) (int, bool) { return x + 20, true })(v7, ok7)
v9, ok9 := Chain(func(x int) (int, bool) { return x / 2, true })(v8, ok8)
_, _ = Chain(func(x int) (int, bool) { return x - 10, true })(v9, ok9)
}
}
// Benchmark Map-based chain (should be faster due to inlining)
func BenchmarkMap_5Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Map(func(x int) int { return x + 1 })(v, ok)
v2, ok2 := Map(func(x int) int { return x * 3 })(v1, ok1)
v3, ok3 := Map(func(x int) int { return x + 20 })(v2, ok2)
v4, ok4 := Map(func(x int) int { return x / 2 })(v3, ok3)
_, _ = Map(func(x int) int { return x - 10 })(v4, ok4)
}
}
// Real-world example: parsing and validating user input
func BenchmarkChain_RealWorld_Validation(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
s, sok := Some("42")
// Step 1: Validate not empty
v1, ok1 := Chain(func(s string) (string, bool) {
if len(s) > 0 {
return s, true
}
return "", false
})(s, sok)
// Step 2: Parse to int (simulated)
v2, ok2 := Chain(func(s string) (int, bool) {
if s == "42" {
return 42, true
}
return 0, false
})(v1, ok1)
// Step 3: Validate range
_, _ = Chain(func(n int) (int, bool) {
if n > 0 && n < 100 {
return n, true
}
return 0, false
})(v2, ok2)
}
}

View File

@@ -0,0 +1,257 @@
mode: set
github.com/IBM/fp-go/v2/idiomatic/option/array.go:31.82,32.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:32.31,34.23 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:34.23,36.12 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:36.12,38.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:39.4,39.22 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:41.3,41.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:56.65,58.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:70.100,71.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:71.31,73.23 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:73.23,75.12 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:75.12,77.5 1 0
github.com/IBM/fp-go/v2/idiomatic/option/array.go:78.4,78.22 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:80.3,80.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:94.83,96.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:38.13,40.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:61.20,62.51 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:62.51,63.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:63.11,65.11 2 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:65.11,67.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:69.3,69.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:92.20,93.51 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:93.51,94.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:94.11,96.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:97.3,97.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:119.20,121.51 2 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:121.51,122.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:122.11,124.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:125.3,125.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:144.19,145.48 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:145.48,146.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:146.10,148.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:149.3,149.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:172.34,173.46 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:173.46,174.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:174.10,176.53 2 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:176.53,177.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:177.13,179.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:180.5,180.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:183.3,183.48 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:183.48,185.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:229.32,231.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:274.18,275.44 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:275.44,277.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:316.18,318.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:354.18,356.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:39.40,41.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:57.40,59.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:72.37,74.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:87.35,89.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:99.36,101.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:108.44,109.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:109.9,111.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:112.2,112.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:49.62,50.50 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:50.50,51.37 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:51.37,52.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:52.12,53.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:53.13,55.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:56.5,56.17 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:58.4,58.16 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:82.72,84.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:7.74,9.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:15.88,17.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:23.116,25.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:31.130,32.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:32.43,34.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:41.158,43.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:49.172,50.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:50.43,52.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:59.200,61.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:67.214,68.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:68.43,70.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:77.242,79.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:85.256,86.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:86.43,88.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/functor.go:26.62,28.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/functor.go:39.44,41.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:8.81,9.38 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:9.38,10.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:10.27,12.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:13.3,13.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:18.125,19.52 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:19.52,20.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:20.27,21.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:21.28,23.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:25.3,25.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:30.169,31.66 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:31.66,32.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:32.27,33.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:33.28,34.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:34.29,36.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:39.3,39.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:44.213,45.80 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:45.80,46.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:46.27,47.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:47.28,48.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:48.29,49.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:49.30,51.7 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:55.3,55.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:60.257,61.94 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:61.94,62.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:62.27,63.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:63.28,64.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:64.29,65.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:65.30,66.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:66.31,68.8 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:73.3,73.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:78.301,79.108 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:79.108,80.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:80.27,81.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:81.28,82.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:82.29,83.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:83.30,84.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:84.31,85.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:85.32,87.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:93.3,93.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:98.345,99.122 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:99.122,100.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:100.27,101.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:101.28,102.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:102.29,103.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:103.30,104.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:104.31,105.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:105.32,106.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:106.33,108.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:115.3,115.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:120.389,121.136 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:121.136,122.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:122.27,123.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:123.28,124.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:124.29,125.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:125.30,126.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:126.31,127.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:127.32,128.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:128.33,129.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:129.34,131.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:139.3,139.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:144.433,145.150 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:145.150,146.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:146.27,147.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:147.28,148.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:148.29,149.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:149.30,150.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:150.31,151.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:151.32,152.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:152.33,153.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:153.34,154.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:154.35,156.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:165.3,165.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:170.487,171.168 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:171.168,172.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:172.27,173.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:173.28,174.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:174.29,175.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:175.30,176.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:176.31,177.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:177.32,178.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:178.33,179.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:179.34,180.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:180.35,181.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:181.39,183.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:193.3,193.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:57.70,58.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:58.39,60.20 2 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:60.20,62.12 2 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:62.12,64.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:65.4,65.22 1 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:67.3,67.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:24.103,25.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:25.39,26.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:26.10,28.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:28.9,30.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:31.3,31.16 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:56.72,58.44 2 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:58.44,60.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:53.60,54.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:54.29,56.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:60.45,62.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:65.48,67.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:70.57,72.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:86.43,88.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:103.59,104.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:104.10,105.58 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:105.58,106.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:106.13,108.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:109.4,109.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:112.2,112.51 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:112.51,114.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:128.50,129.47 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:129.47,130.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:130.11,132.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:133.3,133.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:146.42,147.40 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:147.40,149.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:167.72,168.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:168.31,169.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:169.10,171.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:172.3,172.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:186.56,187.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:187.31,188.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:188.10,190.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:191.3,191.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:208.54,209.45 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:209.45,210.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:210.10,212.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:213.3,213.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:227.54,228.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:228.39,230.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:245.59,246.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:246.39,247.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:247.10,250.4 2 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:251.3,251.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:265.55,266.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:266.39,267.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:267.10,269.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:270.3,270.16 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:286.66,287.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:287.31,288.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:288.10,290.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:291.3,291.17 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:306.54,307.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:307.39,309.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:322.49,323.55 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:323.55,324.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:324.12,326.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:327.3,327.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:37.63,38.47 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:38.47,39.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:39.10,40.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:40.35,41.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:41.12,43.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:44.5,44.14 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:47.3,47.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:47.34,48.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:48.11,50.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:51.4,51.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:64.71,66.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/pointed.go:26.45,28.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/pointed.go:37.38,39.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:30.105,31.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:31.32,33.24 2 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:33.24,34.25 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:34.25,36.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:36.10,38.5 1 0
github.com/IBM/fp-go/v2/idiomatic/option/record.go:41.3,41.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:56.88,58.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:71.121,72.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:72.32,74.24 2 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:74.24,75.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:75.28,77.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:77.10,79.5 1 0
github.com/IBM/fp-go/v2/idiomatic/option/record.go:82.3,82.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:97.104,99.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/type.go:18.37,21.2 2 1
github.com/IBM/fp-go/v2/idiomatic/option/type.go:35.39,37.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/type.go:48.38,50.2 1 1

View File

@@ -79,7 +79,7 @@
//
// # Working with Collections
//
// Transform arrays:
// Transform arrays using TraverseArray:
//
// doublePositive := func(x int) (int, bool) {
// if x > 0 { return x * 2, true }
@@ -88,18 +88,19 @@
// result := TraverseArray(doublePositive)([]int{1, 2, 3}) // ([2, 4, 6], true)
// result := TraverseArray(doublePositive)([]int{1, -2, 3}) // ([], false)
//
// Sequence arrays of Options:
// Transform with indexes:
//
// opts := []Option[int]{Some(1), Some(2), Some(3)}
// result := SequenceArray(opts) // ([1, 2, 3], true)
// f := func(i int, x int) (int, bool) {
// if x > i { return x, true }
// return 0, false
// }
// result := TraverseArrayWithIndex(f)([]int{1, 2, 3}) // ([1, 2, 3], true)
//
// opts := []Option[int]{Some(1), None[int](), Some(3)}
// result := SequenceArray(opts) // ([], false)
// Transform records (maps):
//
// Compact arrays (remove None values):
//
// opts := []Option[int]{Some(1), None[int](), Some(3)}
// result := CompactArray(opts) // [1, 3]
// double := func(x int) (int, bool) { return x * 2, true }
// result := TraverseRecord(double)(map[string]int{"a": 1, "b": 2})
// // (map[string]int{"a": 2, "b": 4}, true)
//
// # Algebraic Operations
//
@@ -122,40 +123,35 @@
// result := withDefault(Some(42)) // (42, true)
// result := withDefault(None[int]()) // (100, true)
//
// # Error Handling
// # Conversion Functions
//
// Convert error-returning functions:
//
// result := TryCatch(func() (int, error) {
// return strconv.Atoi("42")
// }) // (42, true)
//
// result := TryCatch(func() (int, error) {
// return strconv.Atoi("invalid")
// }) // (0, false)
//
// Convert validation functions:
//
// parse := FromValidation(func(s string) (int, bool) {
// n, err := strconv.Atoi(s)
// return n, err == nil
// })
// result := parse("42") // (42, true)
// result := parse("invalid") // (0, false)
//
// Convert predicates:
// Convert predicates to Options:
//
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
// result := isPositive(5) // (5, true)
// result := isPositive(-1) // (-1, false)
// result := isPositive(-1) // (0, false)
//
// Convert nullable pointers:
// Convert nullable pointers to Options:
//
// var ptr *int = nil
// result := FromNillable(ptr) // (nil, false)
// val := 42
// result := FromNillable(&val) // (&val, true)
//
// Convert zero/non-zero values to Options:
//
// result := FromZero[int]()(0) // (0, true)
// result := FromZero[int]()(5) // (0, false)
// result := FromNonZero[int]()(5) // (5, true)
// result := FromNonZero[int]()(0) // (0, false)
//
// Use equality-based conversion:
//
// import "github.com/IBM/fp-go/v2/eq"
// equals42 := FromEq(eq.FromStrictEquals[int]())(42)
// result := equals42(42) // (42, true)
// result := equals42(10) // (0, false)
//
// # Do-Notation Style
//
// Build complex computations using do-notation:
@@ -232,10 +228,7 @@
//
// # Subpackages
//
// - option/number: Number conversion utilities (Atoi, Itoa)
// - option/testing: Testing utilities for verifying monad laws
// - option/number: Number conversion utilities for working with Options
package option
//go:generate go run .. option --count 10 --filename gen.go
// Made with Bob

View File

@@ -0,0 +1,435 @@
// 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 option
import (
"testing"
"github.com/IBM/fp-go/v2/eq"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
// Test Alt function
func TestAlt(t *testing.T) {
t.Run("Some value - returns original", func(t *testing.T) {
withDefault := Alt(func() (int, bool) { return 100, true })
AssertEq(Some(42))(withDefault(Some(42)))(t)
})
t.Run("None value - returns alternative Some", func(t *testing.T) {
withDefault := Alt(func() (int, bool) { return 100, true })
AssertEq(Some(100))(withDefault(None[int]()))(t)
})
t.Run("None value - alternative is also None", func(t *testing.T) {
withDefault := Alt(func() (int, bool) { return None[int]() })
AssertEq(None[int]())(withDefault(None[int]()))(t)
})
}
// Test Reduce function
func TestReduce(t *testing.T) {
t.Run("Some value - applies reducer", func(t *testing.T) {
sum := Reduce(func(acc, val int) int { return acc + val }, 10)
result := sum(Some(5))
assert.Equal(t, 15, result)
})
t.Run("None value - returns initial", func(t *testing.T) {
sum := Reduce(func(acc, val int) int { return acc + val }, 10)
result := sum(None[int]())
assert.Equal(t, 10, result)
})
t.Run("string concatenation", func(t *testing.T) {
concat := Reduce(func(acc, val string) string { return acc + val }, "prefix:")
result := concat(Some("test"))
assert.Equal(t, "prefix:test", result)
})
}
// Test FromZero function
func TestFromZero(t *testing.T) {
t.Run("zero value - returns Some", func(t *testing.T) {
AssertEq(Some(0))(FromZero[int]()(0))(t)
})
t.Run("non-zero value - returns None", func(t *testing.T) {
AssertEq(None[int]())(FromZero[int]()(5))(t)
})
t.Run("empty string - returns Some", func(t *testing.T) {
AssertEq(Some(""))(FromZero[string]()(""))(t)
})
t.Run("non-empty string - returns None", func(t *testing.T) {
AssertEq(None[string]())(FromZero[string]()("hello"))(t)
})
}
// Test FromNonZero function
func TestFromNonZero(t *testing.T) {
t.Run("non-zero value - returns Some", func(t *testing.T) {
AssertEq(Some(5))(FromNonZero[int]()(5))(t)
})
t.Run("zero value - returns None", func(t *testing.T) {
AssertEq(None[int]())(FromNonZero[int]()(0))(t)
})
t.Run("non-empty string - returns Some", func(t *testing.T) {
AssertEq(Some("hello"))(FromNonZero[string]()("hello"))(t)
})
t.Run("empty string - returns None", func(t *testing.T) {
AssertEq(None[string]())(FromNonZero[string]()(""))(t)
})
}
// Test FromEq function
func TestFromEq(t *testing.T) {
t.Run("matching value - returns Some", func(t *testing.T) {
equals42 := FromEq(eq.FromStrictEquals[int]())(42)
AssertEq(Some(42))(equals42(42))(t)
})
t.Run("non-matching value - returns None", func(t *testing.T) {
equals42 := FromEq(eq.FromStrictEquals[int]())(42)
AssertEq(None[int]())(equals42(10))(t)
})
t.Run("string equality", func(t *testing.T) {
equalsHello := FromEq(eq.FromStrictEquals[string]())("hello")
assert.True(t, IsSome(equalsHello("hello")))
assert.True(t, IsNone(equalsHello("world")))
})
}
// Test Pipe and Flow functions
func TestPipe1(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
AssertEq(Some(10))(Pipe1(5, double))(t)
}
func TestFlow1(t *testing.T) {
double := func(x int, ok bool) (int, bool) { return x * 2, ok }
flow := Flow1(double)
AssertEq(Some(10))(flow(Some(5)))(t)
}
func TestFlow2(t *testing.T) {
double := func(x int, ok bool) (int, bool) { return x * 2, ok }
add10 := func(x int, ok bool) (int, bool) {
if ok {
return x + 10, true
}
return 0, false
}
flow := Flow2(double, add10)
AssertEq(Some(20))(flow(Some(5)))(t)
}
func TestPipe3(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
add10 := func(x int, ok bool) (int, bool) {
if ok {
return x + 10, true
}
return 0, false
}
mul3 := func(x int, ok bool) (int, bool) {
if ok {
return x * 3, true
}
return 0, false
}
AssertEq(Some(60))(Pipe3(5, double, add10, mul3))(t) // (5 * 2 + 10) * 3 = 60
}
func TestPipe4(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
add10 := func(x int, ok bool) (int, bool) {
if ok {
return x + 10, true
}
return 0, false
}
mul3 := func(x int, ok bool) (int, bool) {
if ok {
return x * 3, true
}
return 0, false
}
sub5 := func(x int, ok bool) (int, bool) {
if ok {
return x - 5, true
}
return 0, false
}
AssertEq(Some(55))(Pipe4(5, double, add10, mul3, sub5))(t) // ((5 * 2 + 10) * 3) - 5 = 55
}
func TestFlow4(t *testing.T) {
f1 := func(x int, ok bool) (int, bool) { return x + 1, ok }
f2 := func(x int, ok bool) (int, bool) { return x * 2, ok }
f3 := func(x int, ok bool) (int, bool) { return x - 5, ok }
f4 := func(x int, ok bool) (int, bool) { return x * 10, ok }
flow := Flow4(f1, f2, f3, f4)
AssertEq(Some(70))(flow(Some(5)))(t) // ((5 + 1) * 2 - 5) * 10 = 70
}
func TestFlow5(t *testing.T) {
f1 := func(x int, ok bool) (int, bool) { return x + 1, ok }
f2 := func(x int, ok bool) (int, bool) { return x * 2, ok }
f3 := func(x int, ok bool) (int, bool) { return x - 5, ok }
f4 := func(x int, ok bool) (int, bool) { return x * 10, ok }
f5 := func(x int, ok bool) (int, bool) { return x + 100, ok }
flow := Flow5(f1, f2, f3, f4, f5)
AssertEq(Some(170))(flow(Some(5)))(t) // (((5 + 1) * 2 - 5) * 10) + 100 = 170
}
// Test Functor and Pointed
func TestMakeFunctor(t *testing.T) {
t.Run("Map with functor", func(t *testing.T) {
f := MakeFunctor[int, int]()
double := f.Map(func(x int) int { return x * 2 })
AssertEq(Some(42))(double(Some(21)))(t)
})
t.Run("Map with None", func(t *testing.T) {
f := MakeFunctor[int, int]()
double := f.Map(func(x int) int { return x * 2 })
AssertEq(None[int]())(double(None[int]()))(t)
})
}
func TestMakePointed(t *testing.T) {
t.Run("Of with value", func(t *testing.T) {
p := MakePointed[int]()
AssertEq(Some(42))(p.Of(42))(t)
})
t.Run("Of with string", func(t *testing.T) {
p := MakePointed[string]()
AssertEq(Some("hello"))(p.Of("hello"))(t)
})
}
// Test lens-based operations
type TestStruct struct {
Value int
Name string
}
func TestApSL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("Some struct, Some value", func(t *testing.T) {
applyValue := ApSL(valueLens)
v, ok := applyValue(Some(42))(Some(TestStruct{Value: 0, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 42, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("Some struct, None value", func(t *testing.T) {
applyValue := ApSL(valueLens)
AssertEq(None[TestStruct]())(applyValue(None[int]())(Some(TestStruct{Value: 10, Name: "test"})))(t)
})
t.Run("None struct, Some value", func(t *testing.T) {
applyValue := ApSL(valueLens)
AssertEq(None[TestStruct]())(applyValue(Some(42))(None[TestStruct]()))(t)
})
}
func TestBindL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("increment value with validation", func(t *testing.T) {
increment := func(v int) (int, bool) {
if v < 100 {
return v + 1, true
}
return 0, false
}
bindIncrement := BindL(valueLens, increment)
v, ok := bindIncrement(Some(TestStruct{Value: 42, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 43, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("validation fails", func(t *testing.T) {
increment := func(v int) (int, bool) {
if v < 100 {
return v + 1, true
}
return 0, false
}
bindIncrement := BindL(valueLens, increment)
AssertEq(None[TestStruct]())(bindIncrement(Some(TestStruct{Value: 100, Name: "test"})))(t)
})
t.Run("None input", func(t *testing.T) {
increment := func(v int) (int, bool) { return v + 1, true }
bindIncrement := BindL(valueLens, increment)
AssertEq(None[TestStruct]())(bindIncrement(None[TestStruct]()))(t)
})
}
func TestLetL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("double value", func(t *testing.T) {
double := func(v int) int { return v * 2 }
letDouble := LetL(valueLens, double)
v, ok := letDouble(Some(TestStruct{Value: 21, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 42, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("None input", func(t *testing.T) {
double := func(v int) int { return v * 2 }
letDouble := LetL(valueLens, double)
AssertEq(None[TestStruct]())(letDouble(None[TestStruct]()))(t)
})
}
func TestLetToL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("set constant value", func(t *testing.T) {
setValue := LetToL(valueLens, 100)
v, ok := setValue(Some(TestStruct{Value: 42, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 100, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("None input", func(t *testing.T) {
setValue := LetToL(valueLens, 100)
AssertEq(None[TestStruct]())(setValue(None[TestStruct]()))(t)
})
}
// Test tuple traversals
func TestTraverseTuple5(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, ok := TraverseTuple5(double, double, double, double, double)(1, 2, 3, 4, 5)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
}
func TestTraverseTuple6(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, ok := TraverseTuple6(double, double, double, double, double, double)(1, 2, 3, 4, 5, 6)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
}
func TestTraverseTuple7(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, ok := TraverseTuple7(double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
}
func TestTraverseTuple8(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, v8, ok := TraverseTuple8(double, double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7, 8)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
assert.Equal(t, 16, v8)
}
func TestTraverseTuple9(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, v8, v9, ok := TraverseTuple9(double, double, double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7, 8, 9)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
assert.Equal(t, 16, v8)
assert.Equal(t, 18, v9)
}
func TestTraverseTuple10(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, ok := TraverseTuple10(double, double, double, double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
assert.Equal(t, 16, v8)
assert.Equal(t, 18, v9)
assert.Equal(t, 20, v10)
}
// Test tuple traversals with failure cases
func TestTraverseTuple5_Failure(t *testing.T) {
validate := func(x int) (int, bool) {
if x > 0 {
return x, true
}
return 0, false
}
_, _, _, _, _, ok := TraverseTuple5(validate, validate, validate, validate, validate)(1, -2, 3, 4, 5)
assert.False(t, ok)
}

View File

@@ -0,0 +1,6 @@
package result
type Applicative[A, B any] interface {
Apply[A, B]
Pointed[A]
}

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 result
import (
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
// ApplySemigroup lifts a Semigroup over the Right values of Either.
// Combines two Right values using the provided Semigroup.
// If either value is Left, returns the first Left encountered.
//
// Example:
//
// intAdd := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
// eitherSemi := either.ApplySemigroup[error](intAdd)
// result := eitherSemi.Concat(either.Right[error](2), either.Right[error](3)) // Right(5)
//
//go:inline
func ApplySemigroup[A any](s S.Semigroup[A]) S.Semigroup[Either[A]] {
return S.ApplySemigroup(MonadMap[A, func(A) A], MonadAp[A, E, A], s)
}
// ApplicativeMonoid returns a Monoid that concatenates Either instances via their applicative.
// Provides an empty Either (Right with monoid's empty value) and combines Right values using the monoid.
//
// Example:
//
// intAddMonoid := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
// eitherMon := either.ApplicativeMonoid[error](intAddMonoid)
// empty := eitherMon.Empty() // Right(0)
//
//go:inline
func ApplicativeMonoid[A any](m M.Monoid[A]) M.Monoid[Either[A]] {
return M.ApplicativeMonoid(Of[A], MonadMap[A, func(A) A], MonadAp[A, E, A], m)
}

View File

@@ -0,0 +1,6 @@
package result
type Apply[A, B any] interface {
Functor[A, B]
Ap(A, error) Operator[func(A) B, B]
}

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 result
import (
"testing"
M "github.com/IBM/fp-go/v2/monoid/testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
func TestApplySemigroup(t *testing.T) {
sg := ApplySemigroup[string](N.SemigroupSum[int]())
la := Left[int]("a")
lb := Left[int]("b")
r1 := Right[string](1)
r2 := Right[string](2)
r3 := Right[string](3)
assert.Equal(t, la, sg.Concat(la, lb))
assert.Equal(t, lb, sg.Concat(r1, lb))
assert.Equal(t, la, sg.Concat(la, r2))
assert.Equal(t, lb, sg.Concat(r1, lb))
assert.Equal(t, r3, sg.Concat(r1, r2))
}
func TestApplicativeMonoid(t *testing.T) {
m := ApplicativeMonoid[string](N.MonoidSum[int]())
la := Left[int]("a")
lb := Left[int]("b")
r1 := Right[string](1)
r2 := Right[string](2)
r3 := Right[string](3)
assert.Equal(t, la, m.Concat(la, m.Empty()))
assert.Equal(t, lb, m.Concat(m.Empty(), lb))
assert.Equal(t, r1, m.Concat(r1, m.Empty()))
assert.Equal(t, r2, m.Concat(m.Empty(), r2))
assert.Equal(t, r3, m.Concat(r1, r2))
}
func TestApplicativeMonoidLaws(t *testing.T) {
m := ApplicativeMonoid[string](N.MonoidSum[int]())
M.AssertLaws(t, m)([]Either[string, int]{Left[int]("a"), Right[string](1)})
}

View File

@@ -0,0 +1,184 @@
// 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 result
// TraverseArrayG transforms an array by applying a function that returns a Result (value, error) to each element.
// It processes elements from left to right, applying the function to each.
// If any element produces an error, the entire operation short-circuits and returns that error.
// Otherwise, it returns a successful result containing the array of all transformed values.
//
// The G suffix indicates support for generic slice types (e.g., custom slice types based on []T).
//
// Type Parameters:
// - GA: Source slice type (must be based on []A)
// - GB: Target slice type (must be based on []B)
// - A: Source element type
// - B: Target element type
//
// Parameters:
// - f: A Kleisli arrow (A) -> (B, error) that transforms each element
//
// Returns:
// - A Kleisli arrow (GA) -> (GB, error) that transforms the entire array
//
// Behavior:
// - Short-circuits on the first error encountered
// - Preserves the order of elements
// - Returns an empty slice for empty input
//
// Example - Parse strings to integers:
//
// parse := func(s string) (int, error) {
// return strconv.Atoi(s)
// }
// result := result.TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"})
// // result is ([]int{1, 2, 3}, nil)
//
// Example - Short-circuit on error:
//
// result := result.TraverseArrayG[[]string, []int](parse)([]string{"1", "bad", "3"})
// // result is ([]int(nil), error) - stops at "bad"
func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
return func(ga GA) (GB, error) {
bs := make(GB, len(ga))
for i, a := range ga {
b, err := f(a)
if err != nil {
return Left[GB](err)
}
bs[i] = b
}
return Of(bs)
}
}
// TraverseArray transforms an array by applying a function that returns a Result (value, error) to each element.
// It processes elements from left to right, applying the function to each.
// If any element produces an error, the entire operation short-circuits and returns that error.
// Otherwise, it returns a successful result containing the array of all transformed values.
//
// This is a convenience wrapper around [TraverseArrayG] for standard slice types.
//
// Type Parameters:
// - A: Source element type
// - B: Target element type
//
// Parameters:
// - f: A Kleisli arrow (A) -> (B, error) that transforms each element
//
// Returns:
// - A Kleisli arrow ([]A) -> ([]B, error) that transforms the entire array
//
// Example - Validate and transform:
//
// validate := func(s string) (int, error) {
// n, err := strconv.Atoi(s)
// if err != nil {
// return 0, err
// }
// if n < 0 {
// return 0, errors.New("negative number")
// }
// return n * 2, nil
// }
// result := result.TraverseArray(validate)([]string{"1", "2", "3"})
// // result is ([]int{2, 4, 6}, nil)
//
//go:inline
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return TraverseArrayG[[]A, []B](f)
}
// TraverseArrayWithIndexG transforms an array by applying an indexed function that returns a Result (value, error).
// The function receives both the zero-based index and the element for each iteration.
// If any element produces an error, the entire operation short-circuits and returns that error.
// Otherwise, it returns a successful result containing the array of all transformed values.
//
// The G suffix indicates support for generic slice types (e.g., custom slice types based on []T).
//
// Type Parameters:
// - GA: Source slice type (must be based on []A)
// - GB: Target slice type (must be based on []B)
// - A: Source element type
// - B: Target element type
//
// Parameters:
// - f: An indexed function (int, A) -> (B, error) that transforms each element
//
// Returns:
// - A Kleisli arrow (GA) -> (GB, error) that transforms the entire array
//
// Behavior:
// - Processes elements from left to right with their indices (0, 1, 2, ...)
// - Short-circuits on the first error encountered
// - Preserves the order of elements
//
// Example - Annotate with index:
//
// annotate := func(i int, s string) (string, error) {
// if len(s) == 0 {
// return "", fmt.Errorf("empty string at index %d", i)
// }
// return fmt.Sprintf("[%d]=%s", i, s), nil
// }
// result := result.TraverseArrayWithIndexG[[]string, []string](annotate)([]string{"a", "b", "c"})
// // result is ([]string{"[0]=a", "[1]=b", "[2]=c"}, nil)
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) (B, error)) Kleisli[GA, GB] {
return func(ga GA) (GB, error) {
bs := make(GB, len(ga))
for i, a := range ga {
b, err := f(i, a)
if err != nil {
return Left[GB](err)
}
bs[i] = b
}
return Of(bs)
}
}
// TraverseArrayWithIndex transforms an array by applying an indexed function that returns a Result (value, error).
// The function receives both the zero-based index and the element for each iteration.
// If any element produces an error, the entire operation short-circuits and returns that error.
// Otherwise, it returns a successful result containing the array of all transformed values.
//
// This is a convenience wrapper around [TraverseArrayWithIndexG] for standard slice types.
//
// Type Parameters:
// - A: Source element type
// - B: Target element type
//
// Parameters:
// - f: An indexed function (int, A) -> (B, error) that transforms each element
//
// Returns:
// - A Kleisli arrow ([]A) -> ([]B, error) that transforms the entire array
//
// Example - Validate with position info:
//
// check := func(i int, s string) (string, error) {
// if len(s) == 0 {
// return "", fmt.Errorf("empty value at position %d", i)
// }
// return strings.ToUpper(s), nil
// }
// result := result.TraverseArrayWithIndex(check)([]string{"a", "b", "c"})
// // result is ([]string{"A", "B", "C"}, nil)
//
//go:inline
func TraverseArrayWithIndex[A, B any](f func(int, A) (B, error)) Kleisli[[]A, []B] {
return TraverseArrayWithIndexG[[]A, []B](f)
}

View File

@@ -0,0 +1,419 @@
// 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 result
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestTraverseArrayG_Success tests successful traversal of an array with all valid elements
func TestTraverseArrayG_Success(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"})
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, result)
}
// TestTraverseArrayG_Error tests that traversal short-circuits on first error
func TestTraverseArrayG_Error(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "bad", "3"})
require.Error(t, err)
assert.Nil(t, result)
}
// TestTraverseArrayG_EmptyArray tests traversal of an empty array
func TestTraverseArrayG_EmptyArray(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
result, err := TraverseArrayG[[]string, []int](parse)([]string{})
require.NoError(t, err)
assert.Empty(t, result)
assert.NotNil(t, result) // Should be an empty slice, not nil
}
// TestTraverseArrayG_SingleElement tests traversal with a single element
func TestTraverseArrayG_SingleElement(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
result, err := TraverseArrayG[[]string, []int](parse)([]string{"42"})
require.NoError(t, err)
assert.Equal(t, []int{42}, result)
}
// TestTraverseArrayG_ShortCircuit tests that processing stops at first error
func TestTraverseArrayG_ShortCircuit(t *testing.T) {
callCount := 0
parse := func(s string) (int, error) {
callCount++
if s == "error" {
return 0, errors.New("parse error")
}
return len(s), nil
}
_, err := TraverseArrayG[[]string, []int](parse)([]string{"ok", "error", "should-not-process"})
require.Error(t, err)
assert.Equal(t, 2, callCount, "should stop after encountering error")
}
// TestTraverseArrayG_CustomSliceType tests with custom slice types
func TestTraverseArrayG_CustomSliceType(t *testing.T) {
type StringSlice []string
type IntSlice []int
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
input := StringSlice{"1", "2", "3"}
result, err := TraverseArrayG[StringSlice, IntSlice](parse)(input)
require.NoError(t, err)
assert.Equal(t, IntSlice{1, 2, 3}, result)
}
// TestTraverseArray_Success tests successful traversal
func TestTraverseArray_Success(t *testing.T) {
validate := func(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if n < 0 {
return 0, errors.New("negative number")
}
return n * 2, nil
}
result, err := TraverseArray(validate)([]string{"1", "2", "3"})
require.NoError(t, err)
assert.Equal(t, []int{2, 4, 6}, result)
}
// TestTraverseArray_ValidationError tests validation failure
func TestTraverseArray_ValidationError(t *testing.T) {
validate := func(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if n < 0 {
return 0, errors.New("negative number")
}
return n * 2, nil
}
result, err := TraverseArray(validate)([]string{"1", "-5", "3"})
require.Error(t, err)
assert.Contains(t, err.Error(), "negative number")
assert.Nil(t, result)
}
// TestTraverseArray_ParseError tests parse failure
func TestTraverseArray_ParseError(t *testing.T) {
validate := func(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
return n, nil
}
result, err := TraverseArray(validate)([]string{"1", "not-a-number", "3"})
require.Error(t, err)
assert.Nil(t, result)
}
// TestTraverseArray_EmptyArray tests empty array
func TestTraverseArray_EmptyArray(t *testing.T) {
identity := func(n int) (int, error) {
return n, nil
}
result, err := TraverseArray(identity)([]int{})
require.NoError(t, err)
assert.Empty(t, result)
assert.NotNil(t, result)
}
// TestTraverseArray_DifferentTypes tests transformation between different types
func TestTraverseArray_DifferentTypes(t *testing.T) {
toLength := func(s string) (int, error) {
if len(s) == 0 {
return 0, errors.New("empty string")
}
return len(s), nil
}
result, err := TraverseArray(toLength)([]string{"a", "bb", "ccc"})
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, result)
}
// TestTraverseArrayWithIndexG_Success tests successful indexed traversal
func TestTraverseArrayWithIndexG_Success(t *testing.T) {
annotate := func(i int, s string) (string, error) {
if len(s) == 0 {
return "", fmt.Errorf("empty string at index %d", i)
}
return fmt.Sprintf("[%d]=%s", i, s), nil
}
result, err := TraverseArrayWithIndexG[[]string, []string](annotate)([]string{"a", "b", "c"})
require.NoError(t, err)
assert.Equal(t, []string{"[0]=a", "[1]=b", "[2]=c"}, result)
}
// TestTraverseArrayWithIndexG_Error tests error handling with index
func TestTraverseArrayWithIndexG_Error(t *testing.T) {
annotate := func(i int, s string) (string, error) {
if len(s) == 0 {
return "", fmt.Errorf("empty string at index %d", i)
}
return fmt.Sprintf("[%d]=%s", i, s), nil
}
result, err := TraverseArrayWithIndexG[[]string, []string](annotate)([]string{"a", "", "c"})
require.Error(t, err)
assert.Contains(t, err.Error(), "index 1")
assert.Nil(t, result)
}
// TestTraverseArrayWithIndexG_EmptyArray tests empty array
func TestTraverseArrayWithIndexG_EmptyArray(t *testing.T) {
annotate := func(i int, s string) (string, error) {
return fmt.Sprintf("%d:%s", i, s), nil
}
result, err := TraverseArrayWithIndexG[[]string, []string](annotate)([]string{})
require.NoError(t, err)
assert.Empty(t, result)
assert.NotNil(t, result)
}
// TestTraverseArrayWithIndexG_IndexValidation tests that indices are correct
func TestTraverseArrayWithIndexG_IndexValidation(t *testing.T) {
indices := []int{}
collect := func(i int, s string) (string, error) {
indices = append(indices, i)
return s, nil
}
_, err := TraverseArrayWithIndexG[[]string, []string](collect)([]string{"a", "b", "c", "d"})
require.NoError(t, err)
assert.Equal(t, []int{0, 1, 2, 3}, indices)
}
// TestTraverseArrayWithIndexG_ShortCircuit tests short-circuit behavior
func TestTraverseArrayWithIndexG_ShortCircuit(t *testing.T) {
maxIndex := -1
process := func(i int, s string) (string, error) {
maxIndex = i
if i == 2 {
return "", errors.New("stop at index 2")
}
return s, nil
}
_, err := TraverseArrayWithIndexG[[]string, []string](process)([]string{"a", "b", "c", "d", "e"})
require.Error(t, err)
assert.Equal(t, 2, maxIndex, "should stop at index 2")
}
// TestTraverseArrayWithIndexG_CustomSliceType tests with custom slice types
func TestTraverseArrayWithIndexG_CustomSliceType(t *testing.T) {
type StringSlice []string
type ResultSlice []string
annotate := func(i int, s string) (string, error) {
return fmt.Sprintf("%d:%s", i, s), nil
}
input := StringSlice{"x", "y", "z"}
result, err := TraverseArrayWithIndexG[StringSlice, ResultSlice](annotate)(input)
require.NoError(t, err)
assert.Equal(t, ResultSlice{"0:x", "1:y", "2:z"}, result)
}
// TestTraverseArrayWithIndex_Success tests successful indexed traversal
func TestTraverseArrayWithIndex_Success(t *testing.T) {
check := func(i int, s string) (string, error) {
if len(s) == 0 {
return "", fmt.Errorf("empty value at position %d", i)
}
return fmt.Sprintf("%d_%s", i, s), nil
}
result, err := TraverseArrayWithIndex(check)([]string{"a", "b", "c"})
require.NoError(t, err)
assert.Equal(t, []string{"0_a", "1_b", "2_c"}, result)
}
// TestTraverseArrayWithIndex_Error tests error with position info
func TestTraverseArrayWithIndex_Error(t *testing.T) {
check := func(i int, s string) (string, error) {
if len(s) == 0 {
return "", fmt.Errorf("empty value at position %d", i)
}
return s, nil
}
result, err := TraverseArrayWithIndex(check)([]string{"ok", "ok", "", "ok"})
require.Error(t, err)
assert.Contains(t, err.Error(), "position 2")
assert.Nil(t, result)
}
// TestTraverseArrayWithIndex_TypeTransformation tests transforming types with index
func TestTraverseArrayWithIndex_TypeTransformation(t *testing.T) {
multiply := func(i int, n int) (int, error) {
return n * (i + 1), nil
}
result, err := TraverseArrayWithIndex(multiply)([]int{10, 20, 30})
require.NoError(t, err)
assert.Equal(t, []int{10, 40, 90}, result) // [10*(0+1), 20*(1+1), 30*(2+1)]
}
// TestTraverseArrayWithIndex_EmptyArray tests empty array
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
process := func(i int, s string) (int, error) {
return i, nil
}
result, err := TraverseArrayWithIndex(process)([]string{})
require.NoError(t, err)
assert.Empty(t, result)
assert.NotNil(t, result)
}
// TestTraverseArrayWithIndex_SingleElement tests single element processing
func TestTraverseArrayWithIndex_SingleElement(t *testing.T) {
annotate := func(i int, s string) (string, error) {
return fmt.Sprintf("item_%d:%s", i, s), nil
}
result, err := TraverseArrayWithIndex(annotate)([]string{"solo"})
require.NoError(t, err)
assert.Equal(t, []string{"item_0:solo"}, result)
}
// TestTraverseArrayWithIndex_ConditionalLogic tests using index for conditional logic
func TestTraverseArrayWithIndex_ConditionalLogic(t *testing.T) {
// Only accept even indices
process := func(i int, s string) (string, error) {
if i%2 != 0 {
return "", fmt.Errorf("odd index %d not allowed", i)
}
return s, nil
}
result, err := TraverseArrayWithIndex(process)([]string{"ok", "fail"})
require.Error(t, err)
assert.Contains(t, err.Error(), "odd index 1")
assert.Nil(t, result)
}
// TestTraverseArray_LargeArray tests traversal with a larger array
func TestTraverseArray_LargeArray(t *testing.T) {
// Create array with 1000 elements
input := make([]int, 1000)
for i := range input {
input[i] = i
}
double := func(n int) (int, error) {
return n * 2, nil
}
result, err := TraverseArray(double)(input)
require.NoError(t, err)
assert.Len(t, result, 1000)
assert.Equal(t, 0, result[0])
assert.Equal(t, 1998, result[999])
}
// TestTraverseArrayG_PreservesOrder tests that order is preserved
func TestTraverseArrayG_PreservesOrder(t *testing.T) {
reverse := func(s string) (string, error) {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes), nil
}
result, err := TraverseArrayG[[]string, []string](reverse)([]string{"abc", "def", "ghi"})
require.NoError(t, err)
assert.Equal(t, []string{"cba", "fed", "ihg"}, result)
}
// TestTraverseArrayWithIndex_BoundaryCheck tests boundary conditions with index
func TestTraverseArrayWithIndex_BoundaryCheck(t *testing.T) {
// Reject if index exceeds a threshold
limitedProcess := func(i int, s string) (string, error) {
if i >= 100 {
return "", errors.New("index too large")
}
return s, nil
}
// Should succeed with index < 100
result, err := TraverseArrayWithIndex(limitedProcess)([]string{"a", "b", "c"})
require.NoError(t, err)
assert.Equal(t, []string{"a", "b", "c"}, result)
}

View File

@@ -0,0 +1,18 @@
package result
import (
"testing"
"github.com/stretchr/testify/assert"
)
func AssertEq[A any](l A, lerr error) func(A, error) func(*testing.T) {
return func(r A, rerr error) func(*testing.T) {
return func(t *testing.T) {
assert.Equal(t, lerr, rerr)
if (lerr != nil) && (rerr != nil) {
assert.Equal(t, l, r)
}
}
}
}

384
v2/idiomatic/result/bind.go Normal file
View File

@@ -0,0 +1,384 @@
// 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 result
import (
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type S to be used with the Bind operation.
// This is the starting point for do-notation style computations.
//
// Example:
//
// type State struct { x, y int }
// result := either.Do[error](State{})
//
//go:inline
func Do[S any](
empty S,
) (S, error) {
return Of(empty)
}
// Bind attaches the result of a computation to a context S1 to produce a context S2.
// This enables building up complex computations in a pipeline.
//
// Example:
//
// type State struct { value int }
// result := F.Pipe2(
// either.Do[error](State{}),
// either.Bind(
// func(v int) func(State) State {
// return func(s State) State { return State{value: v} }
// },
// func(s State) either.Either[error, int] {
// return either.Right[error](42)
// },
// ),
// )
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return func(s S1, err error) (S2, error) {
if err != nil {
return Left[S2](err)
}
t, err := f(s)
if err != nil {
return Left[S2](err)
}
return Of(setter(t)(s))
}
}
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Similar to Bind but for pure (non-Either) computations.
//
// Example:
//
// type State struct { value int }
// result := F.Pipe2(
// either.Right[error](State{value: 10}),
// either.Let(
// func(v int) func(State) State {
// return func(s State) State { return State{value: s.value + v} }
// },
// func(s State) int { return 32 },
// ),
// ) // Right(State{value: 42})
func Let[S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
) Operator[S1, S2] {
return func(s S1, err error) (S2, error) {
if err != nil {
return Left[S2](err)
}
return Of(key(f(s))(s))
}
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
//
// Example:
//
// type State struct { name string }
// result := F.Pipe2(
// either.Right[error](State{}),
// either.LetTo(
// func(n string) func(State) State {
// return func(s State) State { return State{name: n} }
// },
// "Alice",
// ),
// ) // Right(State{name: "Alice"})
func LetTo[S1, S2, T any](
key func(T) func(S1) S2,
t T,
) Operator[S1, S2] {
return func(s S1, err error) (S2, error) {
if err != nil {
return Left[S2](err)
}
return Of(key(t)(s))
}
}
// BindTo initializes a new state S1 from a value T.
// This is typically used to start a bind chain.
//
// Example:
//
// type State struct { value int }
// result := F.Pipe2(
// either.Right[error](42),
// either.BindTo(func(v int) State { return State{value: v} }),
// ) // Right(State{value: 42})
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return func(t T, err error) (S1, error) {
if err != nil {
return Left[S1](err)
}
return Of(setter(t))
}
}
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
// Uses applicative semantics rather than monadic sequencing.
//
// Example:
//
// type State struct { x, y int }
// result := F.Pipe2(
// either.Right[error](State{x: 10}),
// either.ApS(
// func(y int) func(State) State {
// return func(s State) State { return State{x: s.x, y: y} }
// },
// either.Right[error](32),
// ),
// ) // Right(State{x: 10, y: 32})
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
) func(T, error) Operator[S1, S2] {
return func(t T, terr error) Operator[S1, S2] {
return func(s S1, serr error) (S2, error) {
if terr != nil {
return Left[S2](terr)
}
if serr != nil {
return Left[S2](serr)
}
return Of(setter(t)(s))
}
}
}
// 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 and enables working with
// nested fields in a type-safe manner.
//
// Unlike BindL, ApSL uses applicative semantics, meaning the computation fa is independent
// of the current state and can be evaluated concurrently.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - fa: An Either[T] computation that produces the value to set
//
// Returns:
// - An endomorphism that updates the focused field in the Either context
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, a int) Person { p.Age = a; return p },
// )
//
// result := F.Pipe2(
// either.Right[error](Person{Name: "Alice", Age: 25}),
// either.ApSL(ageLens, either.Right[error](30)),
// ) // Right(Person{Name: "Alice", Age: 30})
//
//go:inline
func ApSL[S, T any](
lens Lens[S, T],
) func(T, error) Operator[S, S] {
return ApS(lens.Set)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an Either that produces the new value.
//
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
// the current value of the focused field.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - f: A function that takes the current field value and returns an Either[T]
//
// Returns:
// - An endomorphism that updates the focused field based on its current value
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but fail if it would exceed 100
// increment := func(v int) either.Either[error, int] {
// if v >= 100 {
// return either.Left[int](errors.New("counter overflow"))
// }
// return either.Right[error](v + 1)
// }
//
// result := F.Pipe1(
// either.Right[error](Counter{Value: 42}),
// either.BindL(valueLens, increment),
// ) // Right(Counter{Value: 43})
func BindL[S, T any](
lens Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return func(s S, serr error) (S, error) {
t, terr := f(lens.Get(s))
if terr != nil {
return Left[S](terr)
}
if serr != nil {
return Left[S](serr)
}
return Of(lens.Set(t)(s))
}
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in Either).
//
// This is useful for pure transformations that cannot fail, such as mathematical operations,
// string manipulations, or other deterministic updates.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - f: An endomorphism (T → T) that transforms the current field value
//
// Returns:
// - An endomorphism that updates the focused field with the transformed value
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// either.Right[error](Counter{Value: 21}),
// either.LetL(valueLens, double),
// ) // Right(Counter{Value: 42})
func LetL[S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
mod := L.Modify[S](f)(lens)
return func(s S, err error) (S, error) {
if err != nil {
return Left[S](err)
}
return Of(mod(s))
}
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// This is useful for resetting fields, initializing values, or setting fields to
// predetermined constants.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - b: The constant value to set the field to
//
// Returns:
// - An endomorphism that sets the focused field to the constant value
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// either.Right[error](Config{Debug: true, Timeout: 30}),
// either.LetToL(debugLens, false),
// ) // Right(Config{Debug: false, Timeout: 30})
//
//go:inline
func LetToL[S, T any](
lens Lens[S, T],
b T,
) Operator[S, S] {
return LetTo(lens.Set, b)
}

View File

@@ -0,0 +1,363 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package result
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
func getLastName(s utils.Initial) (string, error) {
return Of("Doe")
}
func getGivenName(s utils.WithLastName) (string, error) {
return Of("John")
}
func TestBind(t *testing.T) {
res, err := Pipe4(
utils.Empty,
Do,
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map(utils.GetFullName),
)
AssertEq(Of("John Doe"))(res, err)(t)
}
func TestApS(t *testing.T) {
res, err := Pipe4(
utils.Empty,
Do,
ApS(utils.SetLastName)(Of("Doe")),
ApS(utils.SetGivenName)(Of("John")),
Map(utils.GetFullName),
)
AssertEq(Of("John Doe"))(res, err)(t)
}
// Test types for lens-based operations
type Counter struct {
Value int
}
type Person struct {
Name string
Age int
}
type Config struct {
Debug bool
Timeout int
}
func TestApSL(t *testing.T) {
// Create a lens for the Age field
ageLens := L.MakeLens(
func(p Person) int { return p.Age },
func(p Person, a int) Person { p.Age = a; return p },
)
t.Run("ApSL with Right value", func(t *testing.T) {
result, err := Pipe2(
Person{Name: "Alice", Age: 25},
Right,
ApSL(ageLens)(Right(30)),
)
AssertEq(Right(Person{Name: "Alice", Age: 30}))(result, err)(t)
})
t.Run("ApSL with Left in context", func(t *testing.T) {
result, err := Pipe2(
assert.AnError,
Left[Person],
ApSL(ageLens)(Right(30)),
)
AssertEq(Left[Person](assert.AnError))(result, err)(t)
})
t.Run("ApSL with Left in value", func(t *testing.T) {
result, err := Pipe2(
Person{Name: "Alice", Age: 25},
Right,
ApSL(ageLens)(Left[int](assert.AnError)),
)
AssertEq(Left[Person](assert.AnError))(result, err)(t)
})
t.Run("ApSL with both Left", func(t *testing.T) {
result, err := Pipe2(
assert.AnError,
Left[Person],
ApSL(ageLens)(Left[int](assert.AnError)),
)
AssertEq(Left[Person](assert.AnError))(result, err)(t)
})
}
func TestBindL(t *testing.T) {
// Create a lens for the Value field
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("BindL with successful transformation", func(t *testing.T) {
// Increment the counter, but fail if it would exceed 100
increment := func(v int) (int, error) {
if v >= 100 {
return Left[int](assert.AnError)
}
return Right(v + 1)
}
result, err := Pipe2(
Counter{Value: 42},
Right,
BindL(valueLens, increment),
)
AssertEq(Right(Counter{Value: 43}))(result, err)(t)
})
t.Run("BindL with failing transformation", func(t *testing.T) {
increment := func(v int) (int, error) {
if v >= 100 {
return Left[int](assert.AnError)
}
return Right(v + 1)
}
result, err := Pipe2(
Counter{Value: 100},
Right,
BindL(valueLens, increment),
)
AssertEq(Left[Counter](assert.AnError))(result, err)(t)
})
t.Run("BindL with Left input", func(t *testing.T) {
increment := func(v int) (int, error) {
return Right(v + 1)
}
result, err := Pipe2(
assert.AnError,
Left[Counter],
BindL(valueLens, increment),
)
AssertEq(Left[Counter](assert.AnError))(result, err)(t)
})
t.Run("BindL with multiple operations", func(t *testing.T) {
double := func(v int) (int, error) {
return Right(v * 2)
}
addTen := func(v int) (int, error) {
return Right(v + 10)
}
result, err := Pipe3(
Counter{Value: 5},
Right,
BindL(valueLens, double),
BindL(valueLens, addTen),
)
AssertEq(Right(Counter{Value: 20}))(result, err)(t)
})
}
func TestLetL(t *testing.T) {
// Create a lens for the Value field
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("LetL with pure transformation", func(t *testing.T) {
double := N.Mul(2)
result, err := Pipe2(
Counter{Value: 21},
Right,
LetL(valueLens, double),
)
AssertEq(Right(Counter{Value: 42}))(result, err)(t)
})
t.Run("LetL with Left input", func(t *testing.T) {
double := N.Mul(2)
result, err := Pipe2(
assert.AnError,
Left[Counter],
LetL(valueLens, double),
)
AssertEq(Left[Counter](assert.AnError))(result, err)(t)
})
t.Run("LetL with multiple transformations", func(t *testing.T) {
double := N.Mul(2)
addTen := N.Add(10)
result, err := Pipe3(
Counter{Value: 5},
Right,
LetL(valueLens, double),
LetL(valueLens, addTen),
)
AssertEq(Right(Counter{Value: 20}))(result, err)(t)
})
t.Run("LetL with identity transformation", func(t *testing.T) {
identity := F.Identity[int]
result, err := Pipe2(
Counter{Value: 42},
Right,
LetL(valueLens, identity),
)
AssertEq(Right(Counter{Value: 42}))(result, err)(t)
})
}
func TestLetToL(t *testing.T) {
// Create a lens for the Debug field
debugLens := L.MakeLens(
func(c Config) bool { return c.Debug },
func(c Config, d bool) Config { c.Debug = d; return c },
)
t.Run("LetToL with constant value", func(t *testing.T) {
result, err := Pipe2(
Config{Debug: true, Timeout: 30},
Right,
LetToL(debugLens, false),
)
AssertEq(Right(Config{Debug: false, Timeout: 30}))(result, err)(t)
})
t.Run("LetToL with Left input", func(t *testing.T) {
result, err := Pipe2(
assert.AnError,
Left[Config],
LetToL(debugLens, false),
)
AssertEq(Left[Config](assert.AnError))(result, err)(t)
})
t.Run("LetToL with multiple fields", func(t *testing.T) {
timeoutLens := L.MakeLens(
func(c Config) int { return c.Timeout },
func(c Config, t int) Config { c.Timeout = t; return c },
)
result, err := Pipe3(
Config{Debug: true, Timeout: 30},
Right,
LetToL(debugLens, false),
LetToL(timeoutLens, 60),
)
AssertEq(Right(Config{Debug: false, Timeout: 60}))(result, err)(t)
})
t.Run("LetToL setting same value", func(t *testing.T) {
result, err := Pipe2(
Config{Debug: false, Timeout: 30},
Right,
LetToL(debugLens, false),
)
AssertEq(Right(Config{Debug: false, Timeout: 30}))(result, err)(t)
})
}
func TestLensOperationsCombined(t *testing.T) {
// Test combining different lens operations
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("Combine LetToL and LetL", func(t *testing.T) {
double := N.Mul(2)
result, err := Pipe3(
Counter{Value: 100},
Right,
LetToL(valueLens, 10),
LetL(valueLens, double),
)
AssertEq(Right(Counter{Value: 20}))(result, err)(t)
})
t.Run("Combine LetL and BindL", func(t *testing.T) {
double := N.Mul(2)
validate := func(v int) (int, error) {
if v > 100 {
return Left[int](assert.AnError)
}
return Right(v)
}
result, err := Pipe3(
Counter{Value: 25},
Right,
LetL(valueLens, double),
BindL(valueLens, validate),
)
AssertEq(Right(Counter{Value: 50}))(result, err)(t)
})
t.Run("Combine ApSL and LetL", func(t *testing.T) {
addFive := func(v int) int { return v + 5 }
result, err := Pipe3(
Counter{Value: 10},
Right,
ApSL(valueLens)(Right(20)),
LetL(valueLens, addFive),
)
AssertEq(Right(Counter{Value: 25}))(result, err)(t)
})
}

View File

@@ -0,0 +1,6 @@
package result
type Chainable[A, B any] interface {
Apply[A, B]
Chain(Kleisli[A, B]) Operator[A, B]
}

View File

@@ -0,0 +1,80 @@
// 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 result
import (
"fmt"
)
// String prints some debug info for the object
func ToString[A any](a A, err error) string {
if err != nil {
return fmt.Sprintf("Left(%v)", err)
}
return fmt.Sprintf("Right[%T](%v)", a, a)
}
// IsLeft tests if the Either is a Left value.
// Rather use [Fold] or [MonadFold] if you need to access the values.
// Inverse is [IsRight].
//
// Example:
//
// either.IsLeft(either.Left[int](errors.New("err"))) // true
// either.IsLeft(either.Right[error](42)) // false
//
//go:inline
func IsLeft[A any](_ A, err error) bool {
return err != nil
}
// IsRight tests if the Either is a Right value.
// Rather use [Fold] or [MonadFold] if you need to access the values.
// Inverse is [IsLeft].
//
// Example:
//
// either.IsRight(either.Right[error](42)) // true
// either.IsRight(either.Left[int](errors.New("err"))) // false
//
//go:inline
func IsRight[A any](_ A, err error) bool {
return err == nil
}
// Left creates a new Either representing a Left (error/failure) value.
// By convention, Left represents the error case.
//
// Example:
//
// result := either.Left[int](errors.New("something went wrong"))
//
//go:inline
func Left[A any](err error) (A, error) {
return *new(A), err
}
// Right creates a new Either representing a Right (success) value.
// By convention, Right represents the success case.
//
// Example:
//
// result := either.Right[error](42)
//
//go:inline
func Right[A any](a A) (A, error) {
return a, nil
}

View File

@@ -0,0 +1,154 @@
// 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.
//go:build either_any
package result
import (
"fmt"
)
type (
either struct {
value any
isRight bool
}
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
Either[A any] either
)
// String prints some debug info for the object
//
//go:noinline
func eitherString(s *either) string {
if s.isRight {
return fmt.Sprintf("Right[%T](%v)", s.value, s.value)
}
return fmt.Sprintf("Left[%T](%v)", s.value, s.value)
}
// Format prints some debug info for the object
//
//go:noinline
func eitherFormat(e *either, f fmt.State, c rune) {
switch c {
case 's':
fmt.Fprint(f, eitherString(e))
default:
fmt.Fprint(f, eitherString(e))
}
}
// String prints some debug info for the object
func (s Either[A]) String() string {
return eitherString((*either)(&s))
}
// Format prints some debug info for the object
func (s Either[A]) Format(f fmt.State, c rune) {
eitherFormat((*either)(&s), f, c)
}
// IsLeft tests if the Either is a Left value.
// Rather use [Fold] or [MonadFold] if you need to access the values.
// Inverse is [IsRight].
//
// Example:
//
// either.IsLeft(either.Left[int](errors.New("err"))) // true
// either.IsLeft(either.Right[error](42)) // false
//
//go:inline
func IsLeft[A any](val Either[A]) bool {
return !val.isRight
}
// IsRight tests if the Either is a Right value.
// Rather use [Fold] or [MonadFold] if you need to access the values.
// Inverse is [IsLeft].
//
// Example:
//
// either.IsRight(either.Right[error](42)) // true
// either.IsRight(either.Left[int](errors.New("err"))) // false
//
//go:inline
func IsRight[A any](val Either[A]) bool {
return val.isRight
}
// Left creates a new Either representing a Left (error/failure) value.
// By convention, Left represents the error case.
//
// Example:
//
// result := either.Left[int](errors.New("something went wrong"))
//
//go:inline
func Left[A, E any](value E) Either[A] {
return Either[A]{value, false}
}
// Right creates a new Either representing a Right (success) value.
// By convention, Right represents the success case.
//
// Example:
//
// result := either.Right[error](42)
//
//go:inline
func Right[A any](value A) Either[A] {
return Either[A]{value, true}
}
// MonadFold extracts the value from an Either by providing handlers for both cases.
// This is the fundamental pattern matching operation for Either.
//
// Example:
//
// result := either.MonadFold(
// either.Right[error](42),
// func(err error) string { return "Error: " + err.Error() },
// func(n int) string { return fmt.Sprintf("Value: %d", n) },
// ) // "Value: 42"
//
//go:inline
func MonadFold[A, B any](ma Either[A], onLeft func(e E) B, onRight func(a A) B) B {
if ma.isRight {
return onRight(ma.value.(A))
}
return onLeft(ma.value.(E))
}
// Unwrap converts an Either into the idiomatic Go tuple (value, error).
// For Right values, returns (value, zero-error).
// For Left values, returns (zero-value, error).
//
// Example:
//
// val, err := either.Unwrap(either.Right[error](42)) // 42, nil
// val, err := either.Unwrap(either.Left[int](errors.New("fail"))) // 0, error
//
//go:inline
func Unwrap[A any](ma Either[A]) (A, E) {
if ma.isRight {
var e E
return ma.value.(A), e
} else {
var a A
return a, ma.value.(E)
}
}

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.
//go:build either_pointers
package result
import "fmt"
type Either[A any] struct {
left *E
right *A
}
// String prints some debug info for the object
//
//go:noinline
func eitherString[A any](s *Either[A]) string {
if s.right != nil {
return fmt.Sprintf("Right[%T](%v)", *s.right, *s.right)
}
return fmt.Sprintf("Left[%T](%v)", *s.left, *s.left)
}
// Format prints some debug info for the object
//
//go:noinline
func eitherFormat[A any](e *Either[A], f fmt.State, c rune) {
switch c {
case 's':
fmt.Fprint(f, eitherString(e))
default:
fmt.Fprint(f, eitherString(e))
}
}
// String prints some debug info for the object
func (s Either[A]) String() string {
return eitherString(&s)
}
// Format prints some debug info for the object
func (s Either[A]) Format(f fmt.State, c rune) {
eitherFormat(&s, f, c)
}
//go:inline
func Left[A, E any](value E) Either[A] {
return Either[A]{left: &value}
}
//go:inline
func Right[A any](value A) Either[A] {
return Either[A]{right: &value}
}
//go:inline
func IsLeft[A any](e Either[A]) bool {
return e.left != nil
}
//go:inline
func IsRight[A any](e Either[A]) bool {
return e.right != nil
}
//go:inline
func MonadFold[A, B any](ma Either[A], onLeft func(E) B, onRight func(A) B) B {
if ma.right != nil {
return onRight(*ma.right)
}
return onLeft(*ma.left)
}
//go:inline
func Unwrap[A any](ma Either[A]) (A, E) {
if ma.right != nil {
var e E
return *ma.right, e
}
var a A
return a, *ma.left
}

View File

@@ -0,0 +1,142 @@
mode: set
github.com/IBM/fp-go/v2/idiomatic/result/array.go:54.82,55.33 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:55.33,57.24 2 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:57.24,59.18 2 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:59.18,61.5 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:62.4,62.13 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:64.3,64.16 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:101.65,103.2 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:139.101,140.33 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:140.33,142.24 2 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:142.24,144.18 2 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:144.18,146.5 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:147.4,147.13 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:149.3,149.16 1 1
github.com/IBM/fp-go/v2/idiomatic/result/array.go:182.84,184.2 1 1
github.com/IBM/fp-go/v2/idiomatic/result/core.go:23.45,24.16 1 0
github.com/IBM/fp-go/v2/idiomatic/result/core.go:24.16,26.3 1 0
github.com/IBM/fp-go/v2/idiomatic/result/core.go:27.2,27.43 1 0
github.com/IBM/fp-go/v2/idiomatic/result/core.go:40.41,42.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/core.go:54.42,56.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/core.go:66.40,68.2 1 1
github.com/IBM/fp-go/v2/idiomatic/result/core.go:78.35,80.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:36.32,38.2 1 1
github.com/IBM/fp-go/v2/idiomatic/result/either.go:42.61,43.54 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:43.54,44.20 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:44.20,46.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:47.3,47.19 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:47.19,49.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:50.3,50.21 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:56.75,57.41 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:57.41,58.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:58.17,60.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:61.3,61.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:66.42,67.41 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:67.41,69.3 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:74.48,75.41 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:75.41,76.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:76.17,78.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:79.3,79.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:85.59,86.41 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:86.41,87.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:87.17,89.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:90.3,90.15 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:95.92,96.50 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:96.50,97.42 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:97.42,98.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:98.18,100.5 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:101.4,101.25 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:101.25,103.5 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:104.4,104.28 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:110.56,111.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:111.17,112.40 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:112.40,114.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:116.2,116.42 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:116.42,117.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:117.18,119.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:120.3,120.15 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:126.54,127.42 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:127.42,128.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:128.18,130.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:131.3,131.14 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:136.59,137.42 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:137.42,138.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:138.18,140.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:141.3,142.17 2 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:152.70,153.40 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:153.40,154.11 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:154.11,156.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:157.3,157.15 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:169.49,171.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:183.56,184.30 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:184.30,186.3 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:195.43,197.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:208.79,209.33 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:209.33,210.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:210.18,212.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:213.3,213.20 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:228.83,229.30 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:229.30,230.14 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:230.14,232.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:233.3,233.29 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:245.51,246.32 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:246.32,247.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:247.17,249.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:250.3,250.15 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:260.62,261.32 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:261.32,262.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:262.17,264.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:265.3,265.11 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:271.67,272.32 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:272.32,273.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:273.17,275.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:276.3,276.23 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:288.56,289.41 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:289.41,290.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:290.17,292.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:293.3,293.15 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:305.61,306.41 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:306.41,307.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:307.17,309.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:310.3,310.15 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:323.61,324.32 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:324.32,325.25 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:325.25,327.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:328.3,328.29 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:333.48,335.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:338.49,339.54 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:339.54,340.20 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:340.20,342.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/either.go:343.3,343.20 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:7.76,9.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:15.92,17.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:23.120,25.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:31.136,32.45 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:32.45,34.3 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:41.164,43.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:49.180,50.45 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:50.45,52.3 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:59.208,61.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:67.224,68.45 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:68.45,70.3 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:77.252,79.2 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:85.268,86.45 1 0
github.com/IBM/fp-go/v2/idiomatic/result/function.go:86.45,88.3 1 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:35.43,36.51 1 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:36.51,38.37 2 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:38.37,39.18 1 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:39.18,41.5 1 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:42.4,42.22 1 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:64.36,66.42 2 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:66.42,67.17 1 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:67.17,69.4 1 0
github.com/IBM/fp-go/v2/idiomatic/result/traverse.go:70.3,70.21 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:84.81,85.54 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:85.54,86.55 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:86.55,88.19 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:88.19,89.22 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:89.22,91.6 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:93.5,93.25 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:96.4,96.21 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:96.21,98.5 1 0
github.com/IBM/fp-go/v2/idiomatic/result/validation.go:100.4,100.21 1 0

View File

@@ -0,0 +1,127 @@
// 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 result
// Curry0 converts a Go function that returns (R, error) into a curried version that returns (R, error).
//
// Example:
//
// getConfig := func() (string, error) { return "config", nil }
// curried := either.Curry0(getConfig)
// result := curried() // Right("config")
func Curry0[R any](f func() (R, error)) func() (R, error) {
return f
}
// Curry1 converts a Go function that returns (R, error) into a curried version that returns (R, error).
//
// Example:
//
// parse := func(s string) (int, error) { return strconv.Atoi(s) }
// curried := either.Curry1(parse)
// result := curried("42") // Right(42)
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) (R, error) {
return f
}
// Curry2 converts a 2-argument Go function that returns (R, error) into a curried version.
//
// Example:
//
// divide := func(a, b int) (int, error) {
// if b == 0 { return 0, errors.New("div by zero") }
// return a / b, nil
// }
// curried := either.Curry2(divide)
// result := curried(10)(2) // Right(5)
func Curry2[T1, T2, R any](f func(T1, T2) (R, error)) func(T1) func(T2) (R, error) {
return func(t1 T1) func(T2) (R, error) {
return func(t2 T2) (R, error) {
return f(t1, t2)
}
}
}
// Curry3 converts a 3-argument Go function that returns (R, error) into a curried version.
func Curry3[T1, T2, T3, R any](f func(T1, T2, T3) (R, error)) func(T1) func(T2) func(T3) (R, error) {
return func(t1 T1) func(T2) func(T3) (R, error) {
return func(t2 T2) func(T3) (R, error) {
return func(t3 T3) (R, error) {
return f(t1, t2, t3)
}
}
}
}
// Curry4 converts a 4-argument Go function that returns (R, error) into a curried version.
func Curry4[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) (R, error)) func(T1) func(T2) func(T3) func(T4) (R, error) {
return func(t1 T1) func(T2) func(T3) func(T4) (R, error) {
return func(t2 T2) func(T3) func(T4) (R, error) {
return func(t3 T3) func(T4) (R, error) {
return func(t4 T4) (R, error) {
return f(t1, t2, t3, t4)
}
}
}
}
}
// Uncurry0 converts a function returning (R, error) back to Go's (R, error) style.
//
// Example:
//
// curried := func() either.Either[error, string] { return either.Right[error]("value") }
// uncurried := either.Uncurry0(curried)
// result, err := uncurried() // "value", nil
func Uncurry0[R any](f func() (R, error)) func() (R, error) {
return func() (R, error) {
return f()
}
}
// Uncurry1 converts a function returning (R, error) back to Go's (R, error) style.
//
// Example:
//
// curried := func(x int) either.Either[error, string] { return either.Right[error](strconv.Itoa(x)) }
// uncurried := either.Uncurry1(curried)
// result, err := uncurried(42) // "42", nil
func Uncurry1[T1, R any](f func(T1) (R, error)) func(T1) (R, error) {
return func(t1 T1) (R, error) {
return f(t1)
}
}
// Uncurry2 converts a curried function returning (R, error) back to Go's (R, error) style.
func Uncurry2[T1, T2, R any](f func(T1) func(T2) (R, error)) func(T1, T2) (R, error) {
return func(t1 T1, t2 T2) (R, error) {
return f(t1)(t2)
}
}
// Uncurry3 converts a curried function returning (R, error) back to Go's (R, error) style.
func Uncurry3[T1, T2, T3, R any](f func(T1) func(T2) func(T3) (R, error)) func(T1, T2, T3) (R, error) {
return func(t1 T1, t2 T2, t3 T3) (R, error) {
return f(t1)(t2)(t3)
}
}
// Uncurry4 converts a curried function returning (R, error) back to Go's (R, error) style.
func Uncurry4[T1, T2, T3, T4, R any](f func(T1) func(T2) func(T3) func(T4) (R, error)) func(T1, T2, T3, T4) (R, error) {
return func(t1 T1, t2 T2, t3 T3, t4 T4) (R, error) {
return f(t1)(t2)(t3)(t4)
}
}

View File

@@ -0,0 +1,66 @@
// 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 result provides an idiomatic Go approach to error handling using the (value, error) tuple pattern.
//
// This package represents the Result/Either monad idiomatically in Go, leveraging the standard
// (T, error) return pattern that Go developers are familiar with. By convention:
// - (value, nil) represents a success case (Right)
// - (zero, error) represents a failure case (Left)
//
// # Core Concepts
//
// The Result pattern is a functional approach to error handling that makes error flows explicit
// and composable. Instead of checking errors manually at each step, you can chain operations
// that automatically short-circuit on the first error.
//
// # Basic Usage
//
// // Creating Result values
// success := result.Right[error](42) // (42, nil)
// failure := result.Left[int](errors.New("oops")) // (0, error)
//
// // Pattern matching with Fold
// output := result.Fold(
// func(err error) string { return "Error: " + err.Error() },
// func(n int) string { return fmt.Sprintf("Success: %d", n) },
// )(success)
//
// // Chaining operations (short-circuits on Left/error)
// output := result.Chain(func(n int) (int, error) {
// return result.Right[error](n * 2)
// })(success)
//
// # Monadic Operations
//
// Result implements the Monad interface, providing:
// - Map: Transform the Right value
// - Chain (FlatMap): Chain computations that may fail
// - Ap: Apply a function wrapped in Result
//
// # Error Handling
//
// Result provides utilities for working with Go's error type:
// - FromError: Create Result from error-returning functions
// - FromPredicate: Create Result based on a predicate
// - ToError: Extract the error from a Result
//
// # Subpackages
//
// - result/exec: Execute system commands returning Result
// - result/http: HTTP request builders returning Result
package result
//go:generate go run .. either --count 15 --filename gen.go

View File

@@ -0,0 +1,341 @@
// 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 result
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/option"
)
// Of constructs a Right value containing the given value.
// This is the monadic return/pure operation for Either.
// Equivalent to [Right].
//
// Example:
//
// result := either.Of[error](42) // Right(42)
//
//go:inline
func Of[A any](a A) (A, error) {
return a, nil
}
// Ap is the curried version of [MonadAp].
// Returns a function that applies a wrapped function to the given wrapped value.
func Ap[B, A any](fa A, faerr error) Operator[func(A) B, B] {
return func(fab func(A) B, faberr error) (B, error) {
if faberr != nil {
return Left[B](faberr)
}
if faerr != nil {
return Left[B](faerr)
}
return Of(fab(fa))
}
}
// BiMap is the curried version of [MonadBiMap].
// Maps a pair of functions over the two type arguments of the bifunctor.
func BiMap[A, B any](f Endomorphism[error], g func(a A) B) Operator[A, B] {
return func(a A, err error) (B, error) {
if err != nil {
return Left[B](f(err))
}
return Of(g(a))
}
}
// MapTo is the curried version of [MonadMapTo].
func MapTo[A, B any](b B) Operator[A, B] {
return func(_ A, err error) (B, error) {
return b, err
}
}
// Map is the curried version of [MonadMap].
// Transforms the Right value using the provided function.
func Map[A, B any](f func(A) B) Operator[A, B] {
return func(a A, err error) (B, error) {
if err != nil {
return Left[B](err)
}
return Of(f(a))
}
}
// MapLeft is the curried version of [MonadMapLeft].
// Applies a mapping function to the Left (error) channel.
func MapLeft[A any](f Endomorphism[error]) Operator[A, A] {
return func(a A, err error) (A, error) {
if err != nil {
return Left[A](f(err))
}
return Of(a)
}
}
// ChainOptionK is the curried version of [MonadChainOptionK].
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
return func(f func(A) (B, bool)) Operator[A, B] {
return func(a A, err error) (B, error) {
if err != nil {
return Left[B](err)
}
if b, ok := f(a); ok {
return Of(b)
}
return Left[B](onNone())
}
}
}
// ChainTo is the curried version of [MonadChainTo].
func ChainTo[A, B any](b B, berr error) Operator[A, B] {
if berr != nil {
return func(_ A, _ error) (B, error) {
return Left[B](berr)
}
}
return func(a A, aerr error) (B, error) {
if aerr != nil {
return Left[B](aerr)
}
return Of(b)
}
}
// Chain is the curried version of [MonadChain].
// Sequences two computations where the second depends on the first.
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return func(a A, aerr error) (B, error) {
if aerr != nil {
return Left[B](aerr)
}
return f(a)
}
}
// ChainFirst is the curried version of [MonadChainFirst].
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return func(a A, aerr error) (A, error) {
if aerr != nil {
return Left[A](aerr)
}
_, berr := f(a)
return a, berr
}
}
// FromOption converts an Option to an Either, using the provided function to generate a Left value for None.
//
// Example:
//
// opt := option.Some(42)
// result := either.FromOption[int](func() error { return errors.New("none") })(opt) // Right(42)
func FromOption[A any](onNone func() error) func(A, bool) (A, error) {
return func(a A, aok bool) (A, error) {
if !aok {
return Left[A](onNone())
}
return Of(a)
}
}
// ToOption converts an Either to an Option, discarding the Left value.
//
// Example:
//
// result := either.ToOption(either.Right[error](42)) // Some(42)
// result := either.ToOption(either.Left[int](errors.New("err"))) // None
//
//go:inline
func ToOption[A any](a A, aerr error) (A, bool) {
return a, aerr == nil
}
// FromError creates an Either from a function that may return an error.
//
// Example:
//
// validate := func(x int) error {
// if x < 0 { return errors.New("negative") }
// return nil
// }
// toEither := either.FromError(validate)
// result := toEither(42) // Right(42)
func FromError[A any](f func(a A) error) Kleisli[A, A] {
return func(a A) (A, error) {
return a, f(a)
}
}
// ToError converts an Either[error, A] to an error, returning nil for Right values.
//
// Example:
//
// err := either.ToError(either.Left[int](errors.New("fail"))) // error
// err := either.ToError(either.Right[error](42)) // nil
func ToError[A any](_ A, err error) error {
return err
}
// Fold is the curried version of [MonadFold].
// Extracts the value from an Either by providing handlers for both cases.
//
// Example:
//
// result := either.Fold(
// func(err error) string { return "Error: " + err.Error() },
// func(n int) string { return fmt.Sprintf("Value: %d", n) },
// )(either.Right[error](42)) // "Value: 42"
func Fold[A, B any](onLeft func(error) B, onRight func(A) B) func(A, error) B {
return func(a A, aerr error) B {
if aerr != nil {
return onLeft(aerr)
}
return onRight(a)
}
}
// FromPredicate creates an Either based on a predicate.
// If the predicate returns true, creates a Right; otherwise creates a Left using onFalse.
//
// Example:
//
// isPositive := either.FromPredicate(
// func(x int) bool { return x > 0 },
// func(x int) error { return errors.New("not positive") },
// )
// result := isPositive(42) // Right(42)
// result := isPositive(-1) // Left(error)
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
return func(a A) (A, error) {
if pred(a) {
return Right(a)
}
return Left[A](onFalse(a))
}
}
// FromNillable creates an Either from a pointer, using the provided error for nil pointers.
//
// Example:
//
// var ptr *int = nil
// result := either.FromNillable[int](errors.New("nil"))(ptr) // Left(error)
// val := 42
// result := either.FromNillable[int](errors.New("nil"))(&val) // Right(&42)
func FromNillable[A any](e error) Kleisli[*A, *A] {
return func(a *A) (*A, error) {
if F.IsNil(a) {
return Left[*A](e)
}
return Of(a)
}
}
// GetOrElse extracts the Right value or computes a default from the Left value.
//
// Example:
//
// result := either.GetOrElse(func(err error) int { return 0 })(either.Right[error](42)) // 42
// result := either.GetOrElse(func(err error) int { return 0 })(either.Left[int](err)) // 0
func GetOrElse[A any](onLeft func(error) A) func(A, error) A {
return func(a A, err error) A {
if err != nil {
return onLeft(err)
}
return 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[A, B any](f func(B, A) B, initial B) func(A, error) B {
return func(a A, err error) B {
if err != nil {
return initial
}
return f(initial, a)
}
}
// Alt provides an alternative Either if the first is Left.
//
// Example:
//
// alternative := either.Alt[error](func() either.Either[error, int] {
// return either.Right[error](99)
// })
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
func Alt[A any](that func() (A, error)) Operator[A, A] {
return func(a A, err error) (A, error) {
if err != nil {
return that()
}
return Of(a)
}
}
// OrElse recovers from a Left by providing an alternative computation.
//
// Example:
//
// recover := either.OrElse(func(err error) either.Either[error, int] {
// return either.Right[error](0) // default value
// })
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
return func(a A, err error) (A, error) {
if err != nil {
return onLeft(err)
}
return Of(a)
}
}
// ToType attempts to convert an any value to a specific type, returning Either.
//
// Example:
//
// convert := either.ToType[int](func(v any) error {
// return fmt.Errorf("cannot convert %v to int", v)
// })
// result := convert(42) // Right(42)
// result := convert("string") // Left(error)
func ToType[A any](onError func(any) error) Kleisli[any, A] {
return func(x any) (A, error) {
if a, ok := x.(A); ok {
return Of(a)
}
return Left[A](onError(x))
}
}
// Memoize returns the Either unchanged (Either values are already memoized).
func Memoize[A any](a A, err error) (A, error) {
return a, err
}
// Flap is the curried version of [MonadFlap].
func Flap[B, A any](a A) Operator[func(A) B, B] {
return func(fab func(A) B, faberr error) (B, error) {
if faberr != nil {
return Left[B](faberr)
}
return Of(fab(a))
}
}

View File

@@ -0,0 +1,566 @@
// 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 result
import (
"errors"
"testing"
N "github.com/IBM/fp-go/v2/number"
)
var (
errBench = errors.New("benchmark error")
benchResultInt int
benchResultErr error
benchBool bool
benchInt int
benchString string
)
// Benchmark core constructors
func BenchmarkLeft(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Left[int](errBench)
}
}
func BenchmarkRight(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Right(42)
}
}
func BenchmarkOf(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Of(42)
}
}
// Benchmark predicates
func BenchmarkIsLeft(b *testing.B) {
val, err := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchBool = IsLeft(val, err)
}
}
func BenchmarkIsRight(b *testing.B) {
val, err := Right(42)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchBool = IsRight(val, err)
}
}
// Benchmark fold operations
func BenchmarkFold_Right(b *testing.B) {
val, err := Right(42)
folder := Fold(
func(e error) int { return 0 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = folder(val, err)
}
}
func BenchmarkFold_Left(b *testing.B) {
val, err := Left[int](errBench)
folder := Fold(
func(e error) int { return 0 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = folder(val, err)
}
}
// Benchmark functor operations
func BenchmarkMap_Right(b *testing.B) {
val, err := Right(42)
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
func BenchmarkMap_Left(b *testing.B) {
val, err := Left[int](errBench)
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
// Benchmark monad operations
func BenchmarkChain_Right(b *testing.B) {
val, err := Right(42)
chainer := Chain(func(a int) (int, error) { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = chainer(val, err)
}
}
func BenchmarkChain_Left(b *testing.B) {
val, err := Left[int](errBench)
chainer := Chain(func(a int) (int, error) { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = chainer(val, err)
}
}
func BenchmarkChainFirst_Right(b *testing.B) {
val, err := Right(42)
chainer := ChainFirst(func(a int) (string, error) { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = chainer(val, err)
}
}
func BenchmarkChainFirst_Left(b *testing.B) {
val, err := Left[int](errBench)
chainer := ChainFirst(func(a int) (string, error) { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = chainer(val, err)
}
}
// Benchmark alternative operations
func BenchmarkAlt_RightRight(b *testing.B) {
val, err := Right(42)
alternative := Alt(func() (int, error) { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = alternative(val, err)
}
}
func BenchmarkAlt_LeftRight(b *testing.B) {
val, err := Left[int](errBench)
alternative := Alt(func() (int, error) { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = alternative(val, err)
}
}
func BenchmarkOrElse_Right(b *testing.B) {
val, err := Right(42)
recover := OrElse(func(e error) (int, error) { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = recover(val, err)
}
}
func BenchmarkOrElse_Left(b *testing.B) {
val, err := Left[int](errBench)
recover := OrElse(func(e error) (int, error) { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = recover(val, err)
}
}
// Benchmark GetOrElse
func BenchmarkGetOrElse_Right(b *testing.B) {
val, err := Right(42)
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = getter(val, err)
}
}
func BenchmarkGetOrElse_Left(b *testing.B) {
val, err := Left[int](errBench)
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = getter(val, err)
}
}
// Benchmark pipeline operations
func BenchmarkPipeline_Map_Right(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Pipe2(
21,
Right[int],
Map(N.Mul(2)),
)
}
}
func BenchmarkPipeline_Map_Left(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Pipe2(
0,
func(int) (int, error) { return Left[int](errBench) },
Map(N.Mul(2)),
)
}
}
func BenchmarkPipeline_Chain_Right(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Pipe2(
21,
Right[int],
Chain(func(x int) (int, error) { return Right(x * 2) }),
)
}
}
func BenchmarkPipeline_Chain_Left(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Pipe2(
0,
func(int) (int, error) { return Left[int](errBench) },
Chain(func(x int) (int, error) { return Right(x * 2) }),
)
}
}
func BenchmarkPipeline_Complex_Right(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Pipe4(
10,
Right[int],
Map(N.Mul(2)),
Chain(func(x int) (int, error) { return Right(x + 1) }),
Map(N.Mul(2)),
)
}
}
func BenchmarkPipeline_Complex_Left(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = Pipe4(
0,
func(int) (int, error) { return Left[int](errBench) },
Map(N.Mul(2)),
Chain(func(x int) (int, error) { return Right(x + 1) }),
Map(N.Mul(2)),
)
}
}
// Benchmark string formatting
func BenchmarkToString_Right(b *testing.B) {
val, err := Right(42)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchString = ToString(val, err)
}
}
func BenchmarkToString_Left(b *testing.B) {
val, err := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchString = ToString(val, err)
}
}
// Benchmark BiMap
func BenchmarkBiMap_Right(b *testing.B) {
val, err := Right(42)
wrapErr := func(e error) error { return e }
mapper := BiMap[int, int](wrapErr, N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
func BenchmarkBiMap_Left(b *testing.B) {
val, err := Left[int](errBench)
wrapErr := func(e error) error { return e }
mapper := BiMap[int, int](wrapErr, N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
// Benchmark MapTo
func BenchmarkMapTo_Right(b *testing.B) {
val, err := Right(42)
mapper := MapTo[int, int](99)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
func BenchmarkMapTo_Left(b *testing.B) {
val, err := Left[int](errBench)
mapper := MapTo[int, int](99)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
// Benchmark MapLeft
func BenchmarkMapLeft_Right(b *testing.B) {
val, err := Right(42)
mapper := MapLeft[int](func(e error) error { return e })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
func BenchmarkMapLeft_Left(b *testing.B) {
val, err := Left[int](errBench)
mapper := MapLeft[int](func(e error) error { return e })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = mapper(val, err)
}
}
// Benchmark ChainTo
func BenchmarkChainTo_Right(b *testing.B) {
val, err := Right(42)
chainer := ChainTo[int, int](99, nil)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = chainer(val, err)
}
}
func BenchmarkChainTo_Left(b *testing.B) {
val, err := Left[int](errBench)
chainer := ChainTo[int, int](99, nil)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = chainer(val, err)
}
}
// Benchmark Reduce
func BenchmarkReduce_Right(b *testing.B) {
val, err := Right(42)
reducer := Reduce(func(acc, v int) int { return acc + v }, 10)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = reducer(val, err)
}
}
func BenchmarkReduce_Left(b *testing.B) {
val, err := Left[int](errBench)
reducer := Reduce(func(acc, v int) int { return acc + v }, 10)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = reducer(val, err)
}
}
// Benchmark FromPredicate
func BenchmarkFromPredicate_Pass(b *testing.B) {
pred := FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return errBench },
)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = pred(42)
}
}
func BenchmarkFromPredicate_Fail(b *testing.B) {
pred := FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return errBench },
)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = pred(-1)
}
}
// Benchmark Flap
func BenchmarkFlap_Right(b *testing.B) {
fn, ferr := Right(N.Mul(2))
flapper := Flap[int, int](21)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = flapper(fn, ferr)
}
}
func BenchmarkFlap_Left(b *testing.B) {
fn, ferr := Left[func(int) int](errBench)
flapper := Flap[int, int](21)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = flapper(fn, ferr)
}
}
// Benchmark ToOption
func BenchmarkToOption_Right(b *testing.B) {
val, err := Right(42)
var resVal int
var resOk bool
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
resVal, resOk = ToOption(val, err)
benchInt = resVal
benchBool = resOk
}
}
func BenchmarkToOption_Left(b *testing.B) {
val, err := Left[int](errBench)
var resVal int
var resOk bool
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
resVal, resOk = ToOption(val, err)
benchInt = resVal
benchBool = resOk
}
}
// Benchmark FromOption
func BenchmarkFromOption_Some(b *testing.B) {
converter := FromOption[int](func() error { return errBench })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = converter(42, true)
}
}
func BenchmarkFromOption_None(b *testing.B) {
converter := FromOption[int](func() error { return errBench })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt, benchResultErr = converter(0, false)
}
}
// Benchmark ToError
func BenchmarkToError_Right(b *testing.B) {
val, err := Right(42)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultErr = ToError(val, err)
}
}
func BenchmarkToError_Left(b *testing.B) {
val, err := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultErr = ToError(val, err)
}
}
// Benchmark TraverseArray
func BenchmarkTraverseArray_Success(b *testing.B) {
input := []int{1, 2, 3, 4, 5}
traverse := TraverseArray(func(x int) (int, error) {
return Right(x * 2)
})
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
_, benchResultErr = traverse(input)
}
}
func BenchmarkTraverseArray_Error(b *testing.B) {
input := []int{1, 2, 3, 4, 5}
traverse := TraverseArray(func(x int) (int, error) {
if x == 3 {
return Left[int](errBench)
}
return Right(x * 2)
})
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
_, benchResultErr = traverse(input)
}
}

View File

@@ -0,0 +1,112 @@
// 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 result
import (
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/option"
"github.com/IBM/fp-go/v2/internal/utils"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestIsLeft(t *testing.T) {
err := errors.New("Some error")
withError, e1 := Left[string](err)
assert.True(t, IsLeft(withError, e1))
assert.False(t, IsRight(withError, e1))
}
func TestIsRight(t *testing.T) {
noError, err := Right("Carsten")
assert.True(t, IsRight(noError, err))
assert.False(t, IsLeft(noError, err))
}
func TestMapEither(t *testing.T) {
AssertEq(Pipe2("abc", Of, Map(utils.StringLen)))(Right(3))(t)
e := errors.New("s")
AssertEq(Left[int](e))(Pipe2(e, Left[string], Map(utils.StringLen)))(t)
}
func TestAp(t *testing.T) {
f := S.Size
maError := errors.New("maError")
mabError := errors.New("mabError")
AssertEq(Right(3))(Pipe2(f, Right, Ap[int](Right("abc"))))(t)
AssertEq(Left[int](maError))(Pipe2(f, Right, Ap[int](Left[string](maError))))(t)
AssertEq(Left[int](mabError))(Pipe2(mabError, Left[func(string) int], Ap[int](Left[string](maError))))(t)
AssertEq(Left[int](mabError))(Pipe2(mabError, Left[func(string) int], Ap[int](Right("abc"))))(t)
}
func TestAlt(t *testing.T) {
a := errors.New("a")
b := errors.New("b")
AssertEq(Right(1))(Pipe2(1, Right, Alt(func() (int, error) { return Right(2) })))(t)
AssertEq(Right(1))(Pipe2(1, Right, Alt(func() (int, error) { return Left[int](a) })))(t)
AssertEq(Right(2))(Pipe2(b, Left[int], Alt(func() (int, error) { return Right(2) })))(t)
AssertEq(Left[int](b))(Pipe2(a, Left[int], Alt(func() (int, error) { return Left[int](b) })))(t)
}
func TestChainFirst(t *testing.T) {
f := func(s string) (int, error) {
return Of(S.Size((s)))
}
maError := errors.New("maError")
AssertEq(Right("abc"))(Pipe2("abc", Right, ChainFirst(f)))(t)
AssertEq(Left[string](maError))(Pipe2(maError, Left[string], ChainFirst(f)))(t)
}
func TestChainOptionK(t *testing.T) {
a := errors.New("a")
b := errors.New("b")
f := ChainOptionK[int, int](F.Constant(a))(func(n int) (int, bool) {
if n > 0 {
return option.Some(n)
}
return option.None[int]()
})
AssertEq(Right(1))(f(Right(1)))(t)
AssertEq(Left[int](a))(f(Right(-1)))(t)
AssertEq(Left[int](b))(f(Left[int](b)))(t)
}
func TestFromOption(t *testing.T) {
none := errors.New("none")
AssertEq(Left[int](none))(FromOption[int](F.Constant(none))(option.None[int]()))(t)
AssertEq(Right(1))(FromOption[int](F.Constant(none))(option.Some(1)))(t)
}
func TestStringer(t *testing.T) {
e := ToString(Of("foo"))
exp := "Right[string](foo)"
assert.Equal(t, exp, e)
}

63
v2/idiomatic/result/eq.go Normal file
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 result
import (
EQ "github.com/IBM/fp-go/v2/eq"
)
// Eq constructs an equality predicate for Result values (A, error).
// Two Result values are equal if they are both Left (error) with equal error values,
// or both Right (success) with equal values according to the provided equality predicate.
//
// Parameters:
// - eq: Equality predicate for the Right (success) type A
//
// Returns a curried comparison function that takes two Result values and returns true if equal.
//
// Example:
//
// eq := result.Eq(eq.FromStrictEquals[int]())
// result1 := eq(42, nil)(42, nil) // true
// result2 := eq(42, nil)(43, nil) // false
func Eq[A any](eq EQ.Eq[A]) func(A, error) func(A, error) bool {
return func(a A, aerr error) func(A, error) bool {
return func(b A, berr error) bool {
if aerr != nil {
if berr != nil {
return aerr == berr
}
return false
}
if berr != nil {
return false
}
return eq.Equals(a, b)
}
}
}
// FromStrictEquals constructs an equality predicate using Go's == operator.
// The Right type must be comparable.
//
// Example:
//
// eq := result.FromStrictEquals[int]()
// result1 := eq(42, nil)(42, nil) // true
// result2 := eq(42, nil)(43, nil) // false
func FromStrictEquals[A comparable]() func(A, error) func(A, error) bool {
return Eq(EQ.FromStrictEquals[A]())
}

View File

@@ -0,0 +1,54 @@
// 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 result
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEq(t *testing.T) {
r1v, r1e := Of(1)
r2v, r2e := Of(2)
e1v, e1e := Left[int](errorString("a"))
e2v, e2e := Left[int](errorString("a"))
e3v, e3e := Left[int](errorString("b"))
eq := FromStrictEquals[int]()
// Right values
assert.True(t, eq(r1v, r1e)(r1v, r1e))
assert.False(t, eq(r1v, r1e)(r2v, r2e))
// Left values (errors)
assert.True(t, eq(e1v, e1e)(e1v, e1e))
assert.True(t, eq(e1v, e1e)(e2v, e2e))
assert.False(t, eq(e1v, e1e)(e3v, e3e))
// Mixed Left and Right
assert.False(t, eq(r1v, r1e)(e1v, e1e))
assert.False(t, eq(e1v, e1e)(r1v, r1e))
}
// errorString is a simple error type for testing
type errorString string
func (e errorString) Error() string {
return string(e)
}

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 result
import (
"fmt"
"github.com/IBM/fp-go/v2/errors"
)
func Example_creation() {
// Build an Either
leftValue, leftErr := Left[string](fmt.Errorf("some error"))
rightValue, rightErr := Right("value")
// Build from a value
fromNillable := FromNillable[string](fmt.Errorf("value was nil"))
leftFromNil, nilErr := fromNillable(nil)
value := "value"
rightFromPointer, ptrErr := fromNillable(&value)
// some predicate
isEven := func(num int) bool {
return num%2 == 0
}
fromEven := FromPredicate(isEven, errors.OnSome[int]("%d is an odd number"))
leftFromPred, predErrOdd := fromEven(3)
rightFromPred, predErrEven := fromEven(4)
fmt.Println(ToString(leftValue, leftErr))
fmt.Println(ToString(rightValue, rightErr))
fmt.Println(ToString(leftFromNil, nilErr))
fmt.Println(IsRight(rightFromPointer, ptrErr))
fmt.Println(ToString(leftFromPred, predErrOdd))
fmt.Println(ToString(rightFromPred, predErrEven))
// Output:
// Left(some error)
// Right[string](value)
// Left(value was nil)
// true
// Left(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 result
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
func Example_extraction() {
leftValue, leftErr := Left[int](fmt.Errorf("Division by Zero!"))
rightValue, rightErr := Right(10)
// Convert Either[A] to A with a default value
leftWithDefault := GetOrElse(F.Constant1[error](0))(leftValue, leftErr) // 0
rightWithDefault := GetOrElse(F.Constant1[error](0))(rightValue, rightErr) // 10
// Apply a different function on Left(...)/Right(...)
doubleOrZero := Fold(F.Constant1[error](0), N.Mul(2)) // func(int, error) int
doubleFromLeft := doubleOrZero(leftValue, leftErr) // 0
doubleFromRight := doubleOrZero(rightValue, rightErr) // 20
// You can also chain operations using Map
tripled, tripledErr := Pipe2(
rightValue,
Right[int],
Map(N.Mul(3)),
)
tripledResult := GetOrElse(F.Constant1[error](0))(tripled, tripledErr) // 30
fmt.Println(ToString(leftValue, leftErr))
fmt.Println(ToString(rightValue, rightErr))
fmt.Println(leftWithDefault)
fmt.Println(rightWithDefault)
fmt.Println(doubleFromLeft)
fmt.Println(doubleFromRight)
fmt.Println(tripledResult)
// Output:
// Left(Division by Zero!)
// Right[int](10)
// 0
// 10
// 0
// 20
// 30
}

View File

@@ -0,0 +1,49 @@
// 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 Either-based error handling.
package exec
import (
"context"
"github.com/IBM/fp-go/v2/exec"
"github.com/IBM/fp-go/v2/idiomatic/result"
GE "github.com/IBM/fp-go/v2/internal/exec"
)
var (
// Command executes a system command and returns the result as an Either.
// Use this version if the command does not produce any side effects,
// i.e., if the output is uniquely determined by the input.
// For commands with side effects, typically you'd use the IOEither version instead.
//
// Parameters (curried):
// - name: The command name/path
// - args: Command arguments
// - in: Input bytes to send to the command's stdin
//
// Returns Either[error, CommandOutput] containing the command's output or an error.
//
// Example:
//
// result := exec.Command("echo")( []string{"hello"})([]byte{})
// // result is Right(CommandOutput{Stdout: "hello\n", ...})
Command = result.Curry3(command)
)
func command(name string, args []string, in []byte) (exec.CommandOutput, error) {
return GE.Exec(context.Background(), name, args, in)
}

View File

@@ -0,0 +1,89 @@
package result
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe1[F1 ~func(T0) (T1, error), T0, T1 any](t0 T0, f1 F1) (T1, error) {
return f1(t0)
}
// Flow1 creates a function that takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow1[F1 ~func(T0, error) (T1, error), T0, T1 any](f1 F1) func(T0, error) (T1, error) {
return f1
}
// Pipe2 takes an initial value t0 and successively applies 2 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe2[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), T0, T1, T2 any](t0 T0, f1 F1, f2 F2) (T2, error) {
return f2(f1(t0))
}
// Flow2 creates a function that takes an initial value t0 and successively applies 2 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow2[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), T0, T1, T2 any](f1 F1, f2 F2) func(T0, error) (T2, error) {
return func(t0 T0, t0ok error) (T2, error) {
return f2(f1(t0, t0ok))
}
}
// Pipe3 takes an initial value t0 and successively applies 3 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe3[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), T0, T1, T2, T3 any](t0 T0, f1 F1, f2 F2, f3 F3) (T3, error) {
return f3(f2(f1(t0)))
}
// Flow3 creates a function that takes an initial value t0 and successively applies 3 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow3[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), T0, T1, T2, T3 any](f1 F1, f2 F2, f3 F3) func(T0, error) (T3, error) {
return func(t0 T0, t0ok error) (T3, error) {
return f3(f2(f1(t0, t0ok)))
}
}
// Pipe4 takes an initial value t0 and successively applies 4 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe4[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), T0, T1, T2, T3, T4 any](t0 T0, f1 F1, f2 F2, f3 F3, f4 F4) (T4, error) {
return f4(f3(f2(f1(t0))))
}
// Flow4 creates a function that takes an initial value t0 and successively applies 4 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow4[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), T0, T1, T2, T3, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(T0, error) (T4, error) {
return func(t0 T0, t0ok error) (T4, error) {
return f4(f3(f2(f1(t0, t0ok))))
}
}
// Pipe5 takes an initial value t0 and successively applies 5 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe5[F1 ~func(T0) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), F5 ~func(T4, error) (T5, error), T0, T1, T2, T3, T4, T5 any](t0 T0, f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) (T5, error) {
return f5(f4(f3(f2(f1(t0)))))
}
// Flow5 creates a function that takes an initial value t0 and successively applies 5 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow5[F1 ~func(T0, error) (T1, error), F2 ~func(T1, error) (T2, error), F3 ~func(T2, error) (T3, error), F4 ~func(T3, error) (T4, error), F5 ~func(T4, error) (T5, error), T0, T1, T2, T3, T4, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(T0, error) (T5, error) {
return func(t0 T0, t0ok error) (T5, error) {
return f5(f4(f3(f2(f1(t0, t0ok)))))
}
}

View File

@@ -0,0 +1,454 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package result
import (
"errors"
"fmt"
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
// TestBiMap tests mapping over both error and value channels
func TestBiMap(t *testing.T) {
wrapError := func(e error) error {
return fmt.Errorf("wrapped: %w", e)
}
double := N.Mul(2)
t.Run("BiMap on Right", func(t *testing.T) {
val, err := BiMap[int, int](wrapError, double)(Right(21))
AssertEq(Right(42))(val, err)(t)
})
t.Run("BiMap on Left", func(t *testing.T) {
originalErr := errors.New("original")
val, err := BiMap[int, int](wrapError, double)(Left[int](originalErr))
assert.Error(t, err)
assert.Contains(t, err.Error(), "wrapped")
assert.Contains(t, err.Error(), "original")
assert.Equal(t, 0, val)
})
}
// TestMapTo tests mapping to a constant value
func TestMapTo(t *testing.T) {
t.Run("MapTo on Right", func(t *testing.T) {
val, err := MapTo[int, string]("constant")(Right(42))
AssertEq(Right("constant"))(val, err)(t)
})
t.Run("MapTo on Left", func(t *testing.T) {
originalErr := errors.New("error")
val, err := MapTo[int, string]("constant")(Left[int](originalErr))
assert.Error(t, err)
assert.Equal(t, originalErr, err)
// MapTo still applies the constant value even for Left
assert.Equal(t, "constant", val)
})
}
// TestChainTo tests chaining to a constant value
func TestChainTo(t *testing.T) {
t.Run("ChainTo Right to Right", func(t *testing.T) {
val, err := ChainTo[int, string]("success", nil)(Right(42))
AssertEq(Right("success"))(val, err)(t)
})
t.Run("ChainTo Right to Left", func(t *testing.T) {
targetErr := errors.New("target error")
val, err := ChainTo[int, string]("", targetErr)(Right(42))
assert.Error(t, err)
assert.Equal(t, targetErr, err)
assert.Equal(t, "", val)
})
t.Run("ChainTo Left", func(t *testing.T) {
sourceErr := errors.New("source error")
val, err := ChainTo[int, string]("success", nil)(Left[int](sourceErr))
assert.Error(t, err)
assert.Equal(t, sourceErr, err)
assert.Equal(t, "", val)
})
}
// TestReduce tests the reduce/fold operation
func TestReduce(t *testing.T) {
sum := func(acc, val int) int {
return acc + val
}
t.Run("Reduce on Right", func(t *testing.T) {
result := Reduce(sum, 10)(Right(32))
assert.Equal(t, 42, result)
})
t.Run("Reduce on Left", func(t *testing.T) {
result := Reduce(sum, 10)(Left[int](errors.New("error")))
assert.Equal(t, 10, result) // Returns initial value
})
}
// TestFromPredicate tests creating Result from a predicate
func TestFromPredicate(t *testing.T) {
isPositive := FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return fmt.Errorf("%d is not positive", x) },
)
t.Run("Predicate passes", func(t *testing.T) {
val, err := isPositive(42)
AssertEq(Right(42))(val, err)(t)
})
t.Run("Predicate fails", func(t *testing.T) {
val, err := isPositive(-5)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not positive")
// FromPredicate returns zero value on Left
assert.Equal(t, 0, val)
})
t.Run("Predicate with zero", func(t *testing.T) {
val, err := isPositive(0)
assert.Error(t, err)
assert.Equal(t, 0, val)
})
}
// TestFromNillable tests creating Result from nullable pointers
func TestFromNillable(t *testing.T) {
nilErr := errors.New("value is nil")
fromPtr := FromNillable[int](nilErr)
t.Run("Non-nil pointer", func(t *testing.T) {
value := 42
val, err := fromPtr(&value)
assert.NoError(t, err)
assert.NotNil(t, val)
assert.Equal(t, 42, *val)
})
t.Run("Nil pointer", func(t *testing.T) {
val, err := fromPtr(nil)
assert.Error(t, err)
assert.Equal(t, nilErr, err)
assert.Nil(t, val)
})
}
// TestToType tests type conversion
func TestToType(t *testing.T) {
toInt := ToType[int](func(v any) error {
return fmt.Errorf("cannot convert %T to int", v)
})
t.Run("Correct type", func(t *testing.T) {
val, err := toInt(42)
AssertEq(Right(42))(val, err)(t)
})
t.Run("Wrong type", func(t *testing.T) {
val, err := toInt("string")
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot convert")
assert.Equal(t, 0, val)
})
t.Run("Nil value", func(t *testing.T) {
val, err := toInt(nil)
assert.Error(t, err)
assert.Equal(t, 0, val)
})
}
// TestMemoize tests that Memoize returns the value unchanged
func TestMemoize(t *testing.T) {
t.Run("Memoize Right", func(t *testing.T) {
val, err := Memoize(Right(42))
AssertEq(Right(42))(val, err)(t)
})
t.Run("Memoize Left", func(t *testing.T) {
originalErr := errors.New("error")
val, err := Memoize(Left[int](originalErr))
assert.Error(t, err)
assert.Equal(t, originalErr, err)
assert.Equal(t, 0, val)
})
}
// TestFlap tests reverse application
func TestFlap(t *testing.T) {
t.Run("Flap with Right function", func(t *testing.T) {
double := N.Mul(2)
val, err := Flap[int, int](21)(Right(double))
AssertEq(Right(42))(val, err)(t)
})
t.Run("Flap with Left function", func(t *testing.T) {
fnErr := errors.New("function error")
val, err := Flap[int, int](21)(Left[func(int) int](fnErr))
assert.Error(t, err)
assert.Equal(t, fnErr, err)
assert.Equal(t, 0, val)
})
}
// TestToError tests extracting error from Result
func TestToError(t *testing.T) {
t.Run("ToError from Right", func(t *testing.T) {
err := ToError(Right(42))
assert.NoError(t, err)
})
t.Run("ToError from Left", func(t *testing.T) {
originalErr := errors.New("error")
err := ToError(Left[int](originalErr))
assert.Error(t, err)
assert.Equal(t, originalErr, err)
})
}
// TestLet tests the Let operation for do-notation
func TestLet(t *testing.T) {
type State struct {
value int
}
t.Run("Let with Right", func(t *testing.T) {
val, err := Pipe2(
State{value: 10},
Right,
Let(
func(v int) func(State) State {
return func(s State) State { return State{value: s.value + v} }
},
func(s State) int { return 32 },
),
)
assert.NoError(t, err)
assert.Equal(t, 42, val.value)
})
t.Run("Let with Left", func(t *testing.T) {
originalErr := errors.New("error")
val, err := Pipe2(
originalErr,
Left[State],
Let(
func(v int) func(State) State {
return func(s State) State { return State{value: v} }
},
func(s State) int { return 42 },
),
)
assert.Error(t, err)
assert.Equal(t, originalErr, err)
assert.Equal(t, State{}, val)
})
}
// TestLetTo tests the LetTo operation
func TestLetTo(t *testing.T) {
type State struct {
name string
}
t.Run("LetTo with Right", func(t *testing.T) {
val, err := Pipe2(
State{},
Right,
LetTo(
func(n string) func(State) State {
return func(s State) State { return State{name: n} }
},
"Alice",
),
)
assert.NoError(t, err)
assert.Equal(t, "Alice", val.name)
})
t.Run("LetTo with Left", func(t *testing.T) {
originalErr := errors.New("error")
val, err := Pipe2(
originalErr,
Left[State],
LetTo(
func(n string) func(State) State {
return func(s State) State { return State{name: n} }
},
"Bob",
),
)
assert.Error(t, err)
assert.Equal(t, State{}, val)
})
}
// TestBindTo tests the BindTo operation
func TestBindTo(t *testing.T) {
type State struct {
value int
}
t.Run("BindTo with Right", func(t *testing.T) {
val, err := Pipe2(
42,
Right,
BindTo(func(v int) State { return State{value: v} }),
)
assert.NoError(t, err)
assert.Equal(t, 42, val.value)
})
t.Run("BindTo with Left", func(t *testing.T) {
originalErr := errors.New("error")
val, err := Pipe2(
originalErr,
Left[int],
BindTo(func(v int) State { return State{value: v} }),
)
assert.Error(t, err)
assert.Equal(t, State{}, val)
})
}
// TestMapLeft tests mapping over the error channel
func TestMapLeft(t *testing.T) {
wrapError := func(e error) error {
return fmt.Errorf("wrapped: %w", e)
}
t.Run("MapLeft on Right", func(t *testing.T) {
val, err := MapLeft[int](wrapError)(Right(42))
AssertEq(Right(42))(val, err)(t)
})
t.Run("MapLeft on Left", func(t *testing.T) {
originalErr := errors.New("original")
_, err := MapLeft[int](wrapError)(Left[int](originalErr))
assert.Error(t, err)
assert.Contains(t, err.Error(), "wrapped")
assert.Contains(t, err.Error(), "original")
})
}
// TestOrElse tests recovery from error
func TestOrElse(t *testing.T) {
recover := OrElse(func(e error) (int, error) {
return Right(0) // default value
})
t.Run("OrElse on Right", func(t *testing.T) {
val, err := recover(Right(42))
AssertEq(Right(42))(val, err)(t)
})
t.Run("OrElse on Left recovers", func(t *testing.T) {
val, err := recover(Left[int](errors.New("error")))
AssertEq(Right(0))(val, err)(t)
})
}
// TestDo tests the Do operation
func TestDo(t *testing.T) {
type State struct {
x int
y int
}
result, err := Do(State{})
assert.NoError(t, err)
assert.Equal(t, State{}, result)
}
// TestOf tests the Of/pure operation
func TestOf(t *testing.T) {
val, err := Of(42)
AssertEq(Right(42))(val, err)(t)
}
// TestToString tests string representation
func TestToString(t *testing.T) {
t.Run("ToString Right", func(t *testing.T) {
str := ToString(Right(42))
assert.Equal(t, "Right[int](42)", str)
})
t.Run("ToString Left", func(t *testing.T) {
str := ToString(Left[int](errors.New("error")))
assert.Contains(t, str, "Left(")
assert.Contains(t, str, "error")
})
}
// TestToOption tests conversion to Option
func TestToOption(t *testing.T) {
t.Run("ToOption from Right", func(t *testing.T) {
val, ok := ToOption(Right(42))
assert.True(t, ok)
assert.Equal(t, 42, val)
})
t.Run("ToOption from Left", func(t *testing.T) {
val, ok := ToOption(Left[int](errors.New("error")))
assert.False(t, ok)
assert.Equal(t, 0, val)
})
}
// TestFromError tests creating Result from error-returning function
func TestFromError(t *testing.T) {
validate := func(x int) error {
if x < 0 {
return errors.New("negative")
}
return nil
}
toResult := FromError(validate)
t.Run("FromError with valid value", func(t *testing.T) {
val, err := toResult(42)
AssertEq(Right(42))(val, err)(t)
})
t.Run("FromError with invalid value", func(t *testing.T) {
val, err := toResult(-5)
assert.Error(t, err)
assert.Equal(t, "negative", err.Error())
assert.Equal(t, -5, val)
})
}
// TestGetOrElse tests extracting value with default
func TestGetOrElse(t *testing.T) {
defaultValue := func(error) int { return 0 }
t.Run("GetOrElse on Right", func(t *testing.T) {
val := GetOrElse(defaultValue)(Right(42))
assert.Equal(t, 42, val)
})
t.Run("GetOrElse on Left", func(t *testing.T) {
val := GetOrElse(defaultValue)(Left[int](errors.New("error")))
assert.Equal(t, 0, val)
})
}

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 result
type (
eitherFunctor[A, B any] struct{}
Functor[A, B any] interface {
Map(f func(A) B) Operator[A, B]
}
)
func (o eitherFunctor[A, B]) Map(f func(A) B) Operator[A, B] {
return Map(f)
}
// MakeFunctor creates a Functor instance for Result operations.
// A functor provides the Map operation that transforms values inside a context
// while preserving the structure.
//
// Example:
//
// f := result.MakeFunctor[int, string]()
// val, err := f.Map(strconv.Itoa)(result.Right[error](42))
// // val is "42", err is nil
func MakeFunctor[A, B any]() Functor[A, B] {
return eitherFunctor[A, B]{}
}

303
v2/idiomatic/result/gen.go Normal file
View File

@@ -0,0 +1,303 @@
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-03-09 23:52:50.4396159 +0100 CET m=+0.001548701
//
// This file contains generated functions for converting between Either and tuple-based functions.
// It provides Eitherize/Uneitherize functions for functions with 0-15 parameters,
// as well as SequenceT/SequenceTuple/TraverseTuple functions for working with tuples of Either values.
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-03-09 23:52:50.4396159 +0100 CET m=+0.001548701
package result
// TraverseTuple1 converts a [Tuple1] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple1]].
func TraverseTuple1[F1 ~func(A1) (T1, error), E, A1, T1 any](f1 F1) func(A1) (T1, error) {
return func(a1 A1) (t1 T1, err error) {
t1, err = f1(a1)
if err != nil {
return
}
return
}
}
// TraverseTuple2 converts a [Tuple2] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple2]].
func TraverseTuple2[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), E, A1, T1, A2, T2 any](f1 F1, f2 F2) func(A1, A2) (T1, T2, error) {
return func(a1 A1, a2 A2) (t1 T1, t2 T2, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
return
}
}
// TraverseTuple3 converts a [Tuple3] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple3]].
func TraverseTuple3[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), E, A1, T1, A2, T2, A3, T3 any](f1 F1, f2 F2, f3 F3) func(A1, A2, A3) (T1, T2, T3, error) {
return func(a1 A1, a2 A2, a3 A3) (t1 T1, t2 T2, t3 T3, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
return
}
}
// TraverseTuple4 converts a [Tuple4] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple4]].
func TraverseTuple4[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), E, A1, T1, A2, T2, A3, T3, A4, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(A1, A2, A3, A4) (T1, T2, T3, T4, error) {
return func(a1 A1, a2 A2, a3 A3, a4 A4) (t1 T1, t2 T2, t3 T3, t4 T4, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
t4, err = f4(a4)
if err != nil {
return
}
return
}
}
// TraverseTuple5 converts a [Tuple5] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple5]].
func TraverseTuple5[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(A1, A2, A3, A4, A5) (T1, T2, T3, T4, T5, error) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
t4, err = f4(a4)
if err != nil {
return
}
t5, err = f5(a5)
if err != nil {
return
}
return
}
}
// TraverseTuple6 converts a [Tuple6] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple6]].
func TraverseTuple6[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6) func(A1, A2, A3, A4, A5, A6) (T1, T2, T3, T4, T5, T6, error) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
t4, err = f4(a4)
if err != nil {
return
}
t5, err = f5(a5)
if err != nil {
return
}
t6, err = f6(a6)
if err != nil {
return
}
return
}
}
// TraverseTuple7 converts a [Tuple7] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple7]].
func TraverseTuple7[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7) func(A1, A2, A3, A4, A5, A6, A7) (T1, T2, T3, T4, T5, T6, T7, error) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
t4, err = f4(a4)
if err != nil {
return
}
t5, err = f5(a5)
if err != nil {
return
}
t6, err = f6(a6)
if err != nil {
return
}
t7, err = f7(a7)
if err != nil {
return
}
return
}
}
// TraverseTuple8 converts a [Tuple8] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple8]].
func TraverseTuple8[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), F8 ~func(A8) (T8, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8) func(A1, A2, A3, A4, A5, A6, A7, A8) (T1, T2, T3, T4, T5, T6, T7, T8, error) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
t4, err = f4(a4)
if err != nil {
return
}
t5, err = f5(a5)
if err != nil {
return
}
t6, err = f6(a6)
if err != nil {
return
}
t7, err = f7(a7)
if err != nil {
return
}
t8, err = f8(a8)
if err != nil {
return
}
return
}
}
// TraverseTuple9 converts a [Tuple9] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple9]].
func TraverseTuple9[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), F8 ~func(A8) (T8, error), F9 ~func(A9) (T9, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8, A9, T9 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9) func(A1, A2, A3, A4, A5, A6, A7, A8, A9) (T1, T2, T3, T4, T5, T6, T7, T8, T9, error) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8, a9 A9) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, t9 T9, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
t4, err = f4(a4)
if err != nil {
return
}
t5, err = f5(a5)
if err != nil {
return
}
t6, err = f6(a6)
if err != nil {
return
}
t7, err = f7(a7)
if err != nil {
return
}
t8, err = f8(a8)
if err != nil {
return
}
t9, err = f9(a9)
if err != nil {
return
}
return
}
}
// TraverseTuple10 converts a [Tuple10] of [A] via transformation functions transforming [A] to [Either[A]] into a [Either[Tuple10]].
func TraverseTuple10[F1 ~func(A1) (T1, error), F2 ~func(A2) (T2, error), F3 ~func(A3) (T3, error), F4 ~func(A4) (T4, error), F5 ~func(A5) (T5, error), F6 ~func(A6) (T6, error), F7 ~func(A7) (T7, error), F8 ~func(A8) (T8, error), F9 ~func(A9) (T9, error), F10 ~func(A10) (T10, error), E, A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8, A9, T9, A10, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, error) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8, a9 A9, a10 A10) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, t9 T9, t10 T10, err error) {
t1, err = f1(a1)
if err != nil {
return
}
t2, err = f2(a2)
if err != nil {
return
}
t3, err = f3(a3)
if err != nil {
return
}
t4, err = f4(a4)
if err != nil {
return
}
t5, err = f5(a5)
if err != nil {
return
}
t6, err = f6(a6)
if err != nil {
return
}
t7, err = f7(a7)
if err != nil {
return
}
t8, err = f8(a8)
if err != nil {
return
}
t9, err = f9(a9)
if err != nil {
return
}
t10, err = f10(a10)
if err != nil {
return
}
return
}
}

View File

@@ -0,0 +1,202 @@
// 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 result
import (
"errors"
"testing"
)
// Test TraverseTuple1
func TestTraverseTuple1(t *testing.T) {
f := func(x int) (string, error) {
if x > 0 {
return Right("positive")
}
return Left[string](errors.New("negative"))
}
result, err := TraverseTuple1[func(int) (string, error), error](f)(5)
AssertEq(Right("positive"))(result, err)(t)
result, err = TraverseTuple1[func(int) (string, error), error](f)(-1)
AssertEq(Left[string](errors.New("negative")))(result, err)(t)
}
// Test TraverseTuple2
func TestTraverseTuple2(t *testing.T) {
f1 := func(x int) (int, error) {
return Right(x * 2)
}
f2 := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, err := TraverseTuple2[func(int) (int, error), func(int) (int, error), error](f1, f2)(1, 2)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
}
// Test TraverseTuple3
func TestTraverseTuple3(t *testing.T) {
f1 := func(x int) (int, error) {
return Right(x * 2)
}
f2 := func(x int) (int, error) {
return Right(x * 2)
}
f3 := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, err := TraverseTuple3[func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f1, f2, f3)(1, 2, 3)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
}
// Test TraverseTuple4
func TestTraverseTuple4(t *testing.T) {
f1 := func(x int) (int, error) {
return Right(x * 2)
}
f2 := func(x int) (int, error) {
return Right(x * 2)
}
f3 := func(x int) (int, error) {
return Right(x * 2)
}
f4 := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, r4, err := TraverseTuple4[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f1, f2, f3, f4)(1, 2, 3, 4)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
AssertEq(Right(8))(r4, err)(t)
}
// Test TraverseTuple5
func TestTraverseTuple5(t *testing.T) {
f1 := func(x int) (int, error) {
return Right(x * 2)
}
f2 := func(x int) (int, error) {
return Right(x * 2)
}
f3 := func(x int) (int, error) {
return Right(x * 2)
}
f4 := func(x int) (int, error) {
return Right(x * 2)
}
f5 := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, r4, r5, err := TraverseTuple5[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f1, f2, f3, f4, f5)(1, 2, 3, 4, 5)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
AssertEq(Right(8))(r4, err)(t)
AssertEq(Right(10))(r5, err)(t)
}
// Test TraverseTuple6
func TestTraverseTuple6(t *testing.T) {
f := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, r4, r5, r6, err := TraverseTuple6[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f)(1, 2, 3, 4, 5, 6)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
AssertEq(Right(8))(r4, err)(t)
AssertEq(Right(10))(r5, err)(t)
AssertEq(Right(12))(r6, err)(t)
}
// Test TraverseTuple7
func TestTraverseTuple7(t *testing.T) {
f := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, r4, r5, r6, r7, err := TraverseTuple7[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
AssertEq(Right(8))(r4, err)(t)
AssertEq(Right(10))(r5, err)(t)
AssertEq(Right(12))(r6, err)(t)
AssertEq(Right(14))(r7, err)(t)
}
// Test TraverseTuple8
func TestTraverseTuple8(t *testing.T) {
f := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, r4, r5, r6, r7, r8, err := TraverseTuple8[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7, 8)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
AssertEq(Right(8))(r4, err)(t)
AssertEq(Right(10))(r5, err)(t)
AssertEq(Right(12))(r6, err)(t)
AssertEq(Right(14))(r7, err)(t)
AssertEq(Right(16))(r8, err)(t)
}
// Test TraverseTuple9
func TestTraverseTuple9(t *testing.T) {
f := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, r4, r5, r6, r7, r8, r9, err := TraverseTuple9[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7, 8, 9)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
AssertEq(Right(8))(r4, err)(t)
AssertEq(Right(10))(r5, err)(t)
AssertEq(Right(12))(r6, err)(t)
AssertEq(Right(14))(r7, err)(t)
AssertEq(Right(16))(r8, err)(t)
AssertEq(Right(18))(r9, err)(t)
}
// Test TraverseTuple10
func TestTraverseTuple10(t *testing.T) {
f := func(x int) (int, error) {
return Right(x * 2)
}
r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, err := TraverseTuple10[func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), func(int) (int, error), error](f, f, f, f, f, f, f, f, f, f)(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
AssertEq(Right(2))(r1, err)(t)
AssertEq(Right(4))(r2, err)(t)
AssertEq(Right(6))(r3, err)(t)
AssertEq(Right(8))(r4, err)(t)
AssertEq(Right(10))(r5, err)(t)
AssertEq(Right(12))(r6, err)(t)
AssertEq(Right(14))(r7, err)(t)
AssertEq(Right(16))(r8, err)(t)
AssertEq(Right(18))(r9, err)(t)
AssertEq(Right(20))(r10, err)(t)
}

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 http provides utilities for creating HTTP requests with Either-based error handling.
package http
import (
"bytes"
"net/http"
)
var (
// PostRequest creates a POST HTTP request with a body.
// Usage: PostRequest(url)(body) returns Either[error, *http.Request]
//
// Example:
//
// request := http.PostRequest("https://api.example.com/data")([]byte(`{"key":"value"}`))
PostRequest = bodyRequest("POST")
// PutRequest creates a PUT HTTP request with a body.
// Usage: PutRequest(url)(body) returns Either[error, *http.Request]
PutRequest = bodyRequest("PUT")
// GetRequest creates a GET HTTP request without a body.
// Usage: GetRequest(url) returns Either[error, *http.Request]
//
// Example:
//
// request := http.GetRequest("https://api.example.com/data")
GetRequest = noBodyRequest("GET")
// DeleteRequest creates a DELETE HTTP request without a body.
// Usage: DeleteRequest(url) returns Either[error, *http.Request]
DeleteRequest = noBodyRequest("DELETE")
// OptionsRequest creates an OPTIONS HTTP request without a body.
// Usage: OptionsRequest(url) returns Either[error, *http.Request]
OptionsRequest = noBodyRequest("OPTIONS")
// HeadRequest creates a HEAD HTTP request without a body.
// Usage: HeadRequest(url) returns Either[error, *http.Request]
HeadRequest = noBodyRequest("HEAD")
)
func bodyRequest(method string) func(string) func([]byte) (*http.Request, error) {
return func(url string) func([]byte) (*http.Request, error) {
return func(body []byte) (*http.Request, error) {
return http.NewRequest(method, url, bytes.NewReader(body))
}
}
}
func noBodyRequest(method string) func(string) (*http.Request, error) {
return func(url string) (*http.Request, error) {
return http.NewRequest(method, url, nil)
}
}

View File

@@ -0,0 +1,56 @@
// 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 result
import (
"log"
L "github.com/IBM/fp-go/v2/logging"
)
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[A, A] {
return func(a A, err error) (A, error) {
if err != nil {
left("%s: %v", prefix, err)
} else {
right("%s: %v", prefix, a)
}
return a, err
}
}
// Logger creates a logging function for Either values that logs both Left and Right cases.
// The function logs the value and then returns the original Either unchanged.
//
// Parameters:
// - loggers: Optional log.Logger instances. If none provided, uses default logger.
//
// Example:
//
// logger := either.Logger[error, int]()
// result := F.Pipe2(
// either.Right[error](42),
// logger("Processing"),
// either.Map(N.Mul(2)),
// )
// // Logs: "Processing: 42"
// // result is Right(84)
func Logger[A any](loggers ...*log.Logger) func(string) Operator[A, A] {
left, right := L.LoggingCallbacks(loggers...)
return func(prefix string) Operator[A, A] {
return _log[A](left, right, prefix)
}
}

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 result
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLogger(t *testing.T) {
l := Logger[string]()
rVal, rErr := Right("test")
resVal, resErr := Pipe2(
"test",
Right[string],
l("out"),
)
assert.Equal(t, rVal, resVal)
assert.Equal(t, rErr, resErr)
}

View File

@@ -0,0 +1,59 @@
// 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 result
type (
eitherMonad[A, B any] struct{}
Monad[A, B any] interface {
Applicative[A, B]
Chainable[A, B]
}
)
func (o eitherMonad[A, B]) Of(a A) (A, error) {
return Of(a)
}
func (o eitherMonad[A, B]) Map(f func(A) B) Operator[A, B] {
return Map(f)
}
func (o eitherMonad[A, B]) Chain(f func(A) (B, error)) Operator[A, B] {
return Chain(f)
}
func (o eitherMonad[A, B]) Ap(a A, err error) Operator[func(A) B, B] {
return Ap[B](a, err)
}
// MakeMonad creates a Monad instance for Result operations.
// A monad combines the capabilities of Functor (Map), Applicative (Ap), and Chain (flatMap/bind).
// This allows for sequential composition of computations that may fail.
//
// Example:
//
// m := result.MakeMonad[int, string]()
// val, err := m.Chain(func(x int) (string, error) {
// if x > 0 {
// return result.Right[error](strconv.Itoa(x))
// }
// return result.Left[string](errors.New("negative"))
// })(result.Right[error](42))
// // val is "42", err is nil
func MakeMonad[A, B any]() Monad[A, B] {
return eitherMonad[A, B]{}
}

View File

@@ -0,0 +1,59 @@
// 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 result
import (
L "github.com/IBM/fp-go/v2/lazy"
M "github.com/IBM/fp-go/v2/monoid"
)
// AlternativeMonoid creates a monoid for Either using applicative semantics.
// The empty value is Right with the monoid's empty value.
// Combines values using applicative operations.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b })
// m := either.AlternativeMonoid[error](intAdd)
// result := m.Concat(either.Right[error](1), either.Right[error](2))
// // result is Right(3)
func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
return M.AlternativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadAp[A, E, A],
MonadAlt[A],
m,
)
}
// AltMonoid creates a monoid for Either using the Alt operation.
// The empty value is provided as a lazy computation.
// When combining, returns the first Right value, or the second if the first is Left.
//
// Example:
//
// zero := func() either.Either[error, int] { return either.Left[int](errors.New("empty")) }
// 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[A any](zero L.Lazy[Either[A]]) Monoid[A] {
return M.AltMonoid(
zero,
MonadAlt[A],
)
}

View File

@@ -0,0 +1,39 @@
// 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 result
type (
eitherPointed[A any] struct{}
Pointed[A any] interface {
Of(a A) (A, error)
}
)
func (o eitherPointed[A]) Of(a A) (A, error) {
return Of(a)
}
// Pointed implements the pointed functor operations for Either.
// A pointed functor provides the Of operation to lift a value into the Either context.
//
// Example:
//
// p := either.Pointed[error, int]()
// result := p.Of(42) // Right(42)
func MakePointed[A any]() Pointed[A] {
return eitherPointed[A]{}
}

View File

@@ -0,0 +1,114 @@
// 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 result
// TraverseRecordG transforms a map by applying a function that returns an Either to each value.
// If any value produces a Left, the entire result is that Left (short-circuits).
// Otherwise, returns Right containing the map of all Right values.
// The G suffix indicates support for generic map types.
//
// Example:
//
// parse := func(s string) either.Either[error, int] {
// v, err := strconv.Atoi(s)
// return either.FromError(v, err)
// }
// result := either.TraverseRecordG[map[string]string, map[string]int](parse)(map[string]string{"a": "1", "b": "2"})
// // result is Right(map[string]int{"a": 1, "b": 2})
//
//go:inline
func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
return func(ga GA) (GB, error) {
bs := make(GB)
for k, a := range ga {
b, err := f(a)
if err != nil {
return Left[GB](err)
}
bs[k] = b
}
return Of(bs)
}
}
// TraverseRecord transforms a map by applying a function that returns an Either to each value.
// If any value produces a Left, the entire result is that Left (short-circuits).
// Otherwise, returns Right containing the map of all Right values.
//
// Example:
//
// parse := func(s string) either.Either[error, int] {
// v, err := strconv.Atoi(s)
// return either.FromError(v, err)
// }
// result := either.TraverseRecord[string](parse)(map[string]string{"a": "1", "b": "2"})
// // result is Right(map[string]int{"a": 1, "b": 2})
//
//go:inline
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return TraverseRecordG[map[K]A, map[K]B](f)
}
// TraverseRecordWithIndexG transforms a map by applying an indexed function that returns an Either.
// The function receives both the key and the value.
// If any value produces a Left, the entire result is that Left (short-circuits).
// The G suffix indicates support for generic map types.
//
// Example:
//
// validate := func(k string, v string) either.Either[error, string] {
// if len(v) > 0 {
// return either.Right[error](k + ":" + v)
// }
// return either.Left[string](fmt.Errorf("empty value for key %s", k))
// }
// result := either.TraverseRecordWithIndexG[map[string]string, map[string]string](validate)(map[string]string{"a": "1"})
// // result is Right(map[string]string{"a": "a:1"})
//
//go:inline
func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f func(K, A) (B, error)) Kleisli[GA, GB] {
return func(ga GA) (GB, error) {
bs := make(GB)
for k, a := range ga {
b, err := f(k, a)
if err != nil {
return Left[GB](err)
}
bs[k] = b
}
return Of(bs)
}
}
// TraverseRecordWithIndex transforms a map by applying an indexed function that returns an Either.
// The function receives both the key and the value.
// If any value produces a Left, the entire result is that Left (short-circuits).
//
// Example:
//
// validate := func(k string, v string) either.Either[error, string] {
// if len(v) > 0 {
// return either.Right[error](k + ":" + v)
// }
// return either.Left[string](fmt.Errorf("empty value for key %s", k))
// }
// result := either.TraverseRecordWithIndex[string](validate)(map[string]string{"a": "1"})
// // result is Right(map[string]string{"a": "a:1"})
//
//go:inline
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) (B, error)) Kleisli[map[K]A, map[K]B] {
return TraverseRecordWithIndexG[map[K]A, map[K]B](f)
}

View File

@@ -0,0 +1,253 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package result
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestTraverseRecordG_Success tests successful traversal of a map
func TestTraverseRecordG_Success(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
input := map[string]string{"a": "1", "b": "2", "c": "3"}
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
require.NoError(t, err)
assert.Equal(t, 1, result["a"])
assert.Equal(t, 2, result["b"])
assert.Equal(t, 3, result["c"])
}
// TestTraverseRecordG_Error tests that traversal short-circuits on error
func TestTraverseRecordG_Error(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
input := map[string]string{"a": "1", "b": "bad", "c": "3"}
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
require.Error(t, err)
assert.Nil(t, result)
}
// TestTraverseRecordG_EmptyMap tests traversal of an empty map
func TestTraverseRecordG_EmptyMap(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
input := map[string]string{}
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
require.NoError(t, err)
assert.Empty(t, result)
assert.NotNil(t, result) // Should be an empty map, not nil
}
// TestTraverseRecordG_CustomMapType tests with custom map types
func TestTraverseRecordG_CustomMapType(t *testing.T) {
type StringMap map[string]string
type IntMap map[string]int
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
input := StringMap{"x": "10", "y": "20"}
result, err := TraverseRecordG[StringMap, IntMap](parse)(input)
require.NoError(t, err)
assert.Equal(t, IntMap{"x": 10, "y": 20}, result)
}
// TestTraverseRecord_Success tests successful traversal
func TestTraverseRecord_Success(t *testing.T) {
validate := func(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if n < 0 {
return 0, errors.New("negative number")
}
return n * 2, nil
}
input := map[string]string{"a": "1", "b": "2"}
result, err := TraverseRecord[string, string, int](validate)(input)
require.NoError(t, err)
assert.Equal(t, 2, result["a"])
assert.Equal(t, 4, result["b"])
}
// TestTraverseRecord_ValidationError tests validation failure
func TestTraverseRecord_ValidationError(t *testing.T) {
validate := func(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if n < 0 {
return 0, errors.New("negative number")
}
return n, nil
}
input := map[string]string{"a": "1", "b": "-5"}
result, err := TraverseRecord[string, string, int](validate)(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "negative")
assert.Nil(t, result)
}
// TestTraverseRecordWithIndexG_Success tests successful indexed traversal
func TestTraverseRecordWithIndexG_Success(t *testing.T) {
annotate := func(k string, v string) (string, error) {
if len(v) == 0 {
return "", fmt.Errorf("empty value for key %s", k)
}
return fmt.Sprintf("%s=%s", k, v), nil
}
input := map[string]string{"a": "1", "b": "2"}
result, err := TraverseRecordWithIndexG[map[string]string, map[string]string](annotate)(input)
require.NoError(t, err)
assert.Equal(t, "a=1", result["a"])
assert.Equal(t, "b=2", result["b"])
}
// TestTraverseRecordWithIndexG_Error tests error handling with key
func TestTraverseRecordWithIndexG_Error(t *testing.T) {
annotate := func(k string, v string) (string, error) {
if len(v) == 0 {
return "", fmt.Errorf("empty value for key %s", k)
}
return v, nil
}
input := map[string]string{"a": "1", "b": ""}
result, err := TraverseRecordWithIndexG[map[string]string, map[string]string](annotate)(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "key b")
assert.Nil(t, result)
}
// TestTraverseRecordWithIndexG_EmptyMap tests empty map
func TestTraverseRecordWithIndexG_EmptyMap(t *testing.T) {
annotate := func(k string, v string) (string, error) {
return k + ":" + v, nil
}
input := map[string]string{}
result, err := TraverseRecordWithIndexG[map[string]string, map[string]string](annotate)(input)
require.NoError(t, err)
assert.Empty(t, result)
assert.NotNil(t, result)
}
// TestTraverseRecordWithIndex_Success tests successful indexed traversal
func TestTraverseRecordWithIndex_Success(t *testing.T) {
check := func(k string, v int) (string, error) {
if v < 0 {
return "", fmt.Errorf("negative value for key %s", k)
}
return fmt.Sprintf("%s:%d", k, v*2), nil
}
input := map[string]int{"a": 1, "b": 2}
result, err := TraverseRecordWithIndex[string, int, string](check)(input)
require.NoError(t, err)
assert.Equal(t, "a:2", result["a"])
assert.Equal(t, "b:4", result["b"])
}
// TestTraverseRecordWithIndex_Error tests error with key info
func TestTraverseRecordWithIndex_Error(t *testing.T) {
check := func(k string, v int) (int, error) {
if v < 0 {
return 0, fmt.Errorf("negative value for key %s", k)
}
return v, nil
}
input := map[string]int{"ok": 1, "bad": -5}
result, err := TraverseRecordWithIndex[string, int, int](check)(input)
require.Error(t, err)
assert.Contains(t, err.Error(), "key bad")
assert.Nil(t, result)
}
// TestTraverseRecordWithIndex_TypeTransformation tests transforming types with key
func TestTraverseRecordWithIndex_TypeTransformation(t *testing.T) {
prefixKey := func(k string, v string) (string, error) {
return k + "_" + v, nil
}
input := map[string]string{"prefix": "value", "another": "test"}
result, err := TraverseRecordWithIndex[string, string, string](prefixKey)(input)
require.NoError(t, err)
assert.Equal(t, "prefix_value", result["prefix"])
assert.Equal(t, "another_test", result["another"])
}
// TestTraverseRecord_IntKeys tests with integer keys
func TestTraverseRecord_IntKeys(t *testing.T) {
double := func(n int) (int, error) {
return n * 2, nil
}
input := map[int]int{1: 10, 2: 20, 3: 30}
result, err := TraverseRecord[int, int, int](double)(input)
require.NoError(t, err)
assert.Equal(t, 20, result[1])
assert.Equal(t, 40, result[2])
assert.Equal(t, 60, result[3])
}
// TestTraverseRecordG_PreservesKeys tests that keys are preserved
func TestTraverseRecordG_PreservesKeys(t *testing.T) {
identity := func(s string) (string, error) {
return s, nil
}
input := map[string]string{"key1": "val1", "key2": "val2"}
result, err := TraverseRecordG[map[string]string, map[string]string](identity)(input)
require.NoError(t, err)
assert.Contains(t, result, "key1")
assert.Contains(t, result, "key2")
assert.Equal(t, "val1", result["key1"])
assert.Equal(t, "val2", result["key2"])
}

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 result
// 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.
//
// Parameters:
// - onCreate: Function to create/acquire the resource
// - onRelease: Function to release/cleanup the resource
//
// Returns a function that takes an operation to perform on the resource.
//
// Example:
//
// withFile := either.WithResource(
// func() either.Either[error, *os.File] {
// return either.TryCatchError(os.Open("file.txt"))
// },
// func(f *os.File) either.Either[error, any] {
// return either.TryCatchError(f.Close())
// },
// )
// result := withFile(func(f *os.File) either.Either[error, string] {
// // Use file here
// return either.Right[error]("data")
// })
func WithResource[R, A, ANY any](onCreate func() (R, error), onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
return func(f func(R) (A, error)) (A, error) {
r, rerr := onCreate()
if rerr != nil {
return Left[A](rerr)
}
a, aerr := f(r)
_, nerr := onRelease(r)
if aerr != nil {
return Left[A](aerr)
}
if nerr != nil {
return Left[A](nerr)
}
return Of(a)
}
}

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 result
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWithResource(t *testing.T) {
onCreate := func() (*os.File, error) {
return os.CreateTemp("", "*")
}
onDelete := func(f *os.File) (any, error) {
return Chain(func(name string) (any, error) {
return any(name), os.Remove(name)
})(f.Name(), f.Close())
}
onHandler := func(f *os.File) (string, error) {
return Of(f.Name())
}
tempFile := WithResource[*os.File, string](onCreate, onDelete)
res, err := tempFile(onHandler)
assert.True(t, IsRight(res, err))
}

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 result
import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// AltSemigroup creates a semigroup for Either that uses the Alt operation for combining values.
// When combining two Either values, it returns the first Right value, or the second value if the first is Left.
//
// Example:
//
// sg := either.AltSemigroup[error, int]()
// result := sg.Concat(either.Left[int](errors.New("error")), either.Right[error](42))
// // result is Right(42)
// result2 := sg.Concat(either.Right[error](1), either.Right[error](2))
// // result2 is Right(1) - first Right wins
//
//go:inline
func AltSemigroup[A any]() S.Semigroup[Either[A]] {
return S.AltSemigroup(
MonadAlt[A],
)
}

View File

@@ -0,0 +1,103 @@
// 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 provides utilities for testing Either monad laws.
// This is useful for verifying that custom Either implementations satisfy the monad laws.
package testing
import (
"testing"
ET "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"
)
// AssertLaws asserts that the Either monad satisfies the monad laws.
// This includes testing:
// - Identity laws (left and right identity)
// - Associativity law
// - Functor laws
// - Applicative laws
//
// Parameters:
// - t: Testing context
// - eqe, eqa, eqb, eqc: Equality predicates for the types
// - ab: Function from A to B for testing
// - bc: Function from B to C for testing
//
// Returns a function that takes a value of type A and returns true if all laws hold.
//
// Example:
//
// func TestEitherLaws(t *testing.T) {
// eqInt := eq.FromStrictEquals[int]()
// eqString := eq.FromStrictEquals[string]()
// eqError := eq.FromStrictEquals[error]()
//
// ab := strconv.Itoa
// bc := func(s string) bool { return len(s) > 0 }
//
// testing.AssertLaws(t, eqError, eqInt, eqString, eq.FromStrictEquals[bool](), ab, bc)(42)
// }
func AssertLaws[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,
ET.Eq(eqe, eqa),
ET.Eq(eqe, eqb),
ET.Eq(eqe, eqc),
ET.Of[A],
ET.Of[B],
ET.Of[C],
ET.Of[func(A) A],
ET.Of[func(A) B],
ET.Of[func(B) C],
ET.Of[func(func(A) B) B],
ET.MonadMap[A, A],
ET.MonadMap[A, B],
ET.MonadMap[A, C],
ET.MonadMap[B, C],
ET.MonadMap[func(B) C, func(func(A) B) func(A) C],
ET.MonadChain[A, A],
ET.MonadChain[A, B],
ET.MonadChain[A, C],
ET.MonadChain[B, C],
ET.MonadAp[A, E, A],
ET.MonadAp[B, E, A],
ET.MonadAp[C, E, B],
ET.MonadAp[C, E, A],
ET.MonadAp[B, E, func(A) B],
ET.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,72 @@
// 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 result
// Traverse converts an Either of some higher kinded type into the higher kinded type of an Either.
// This is a generic traversal operation that works with any applicative functor.
//
// Parameters:
// - mof: Lifts an Either into the target higher-kinded type
// - mmap: Maps over the target higher-kinded type
//
// Example (conceptual - requires understanding of higher-kinded types):
//
// // Traverse an Either[error, Option[int]] to Option[Either[error, int]]
// result := either.Traverse[int, error, int, option.Option[int], option.Option[either.Either[error, int]]](
// option.Of[either.Either[error, int]],
// option.Map[int, either.Either[error, int]],
// )(f)(eitherOfOption)
func Traverse[A, B, HKTB, HKTRB any](
mof func(B, error) HKTRB,
mmap func(Kleisli[B, B]) func(HKTB) HKTRB,
) func(func(A) HKTB) func(A, error) HKTRB {
return func(f func(A) HKTB) func(A, error) HKTRB {
right := mmap(Right[B])
return func(a A, err error) HKTRB {
if err != nil {
return mof(Left[B](err))
}
return right(f(a))
}
}
}
// Sequence converts an Either of some higher kinded type into the higher kinded type of an Either.
// This is the identity version of Traverse - it doesn't transform the values, just swaps the type constructors.
//
// Parameters:
// - mof: Lifts an Either into the target higher-kinded type
// - mmap: Maps over the target higher-kinded type
//
// Example (conceptual - requires understanding of higher-kinded types):
//
// // Sequence an Either[error, Option[int]] to Option[Either[error, int]]
// result := either.Sequence[error, int, option.Option[int], option.Option[either.Either[error, int]]](
// option.Of[either.Either[error, int]],
// option.Map[int, either.Either[error, int]],
// )(eitherOfOption)
func Sequence[A, HKTA, HKTRA any](
mof func(A, error) HKTRA,
mmap func(Kleisli[A, A]) func(HKTA) HKTRA,
) func(hkta HKTA, err error) HKTRA {
right := mmap(Right[A])
return func(hkta HKTA, err error) HKTRA {
if err != nil {
return mof(Left[A](err))
}
return right(hkta)
}
}

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 result
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
)
// Option is a type alias for option.Option, provided for convenience
// when working with Either and Option together.
type (
Option[A any] = option.Option[A]
Lens[S, T any] = lens.Lens[S, T]
Endomorphism[T any] = endomorphism.Endomorphism[T]
Kleisli[A, B any] = func(A) (B, error)
Operator[A, B any] = func(A, error) (B, error)
)

View File

@@ -0,0 +1,103 @@
// 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 result
import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// ApV applies a function wrapped in a Result to a value wrapped in a Result,
// accumulating errors using a semigroup instead of short-circuiting.
//
// This function is designed for validation scenarios where you want to collect
// all validation errors rather than stopping at the first error. It differs
// from the standard [Ap] function in that it combines errors from both the
// function and the value using the provided semigroup operation.
//
// The function works as follows:
// - If both the value and the function have errors, it combines them using the semigroup
// - If only one has an error, it returns that error
// - If neither has an error, it applies the function to the value
//
// Type Parameters:
// - B: The result type after applying the function
// - A: The input type to the function
//
// Parameters:
// - sg: A semigroup that defines how to combine two error values. The semigroup's
// Concat operation determines how errors are accumulated (e.g., concatenating
// error messages, merging error lists, etc.)
//
// Returns:
// - A curried function that takes a value (A, error), then takes a function
// (func(A) B, error), and returns the result (B, error) with accumulated errors
//
// Behavior:
// - Right + Right: Applies the function to the value and returns Right(result)
// - Right + Left: Returns the Left error from the function
// - Left + Right: Returns the Left error from the value
// - Left + Left: Returns Left(sg.Concat(function_error, value_error))
//
// Example:
//
// import (
// "errors"
// "fmt"
// "strings"
// S "github.com/IBM/fp-go/v2/semigroup"
// "github.com/IBM/fp-go/v2/idiomatic/result"
// )
//
// // Create a semigroup that combines errors by concatenating their messages
// errorSemigroup := S.MakeSemigroup(func(e1, e2 error) error {
// return fmt.Errorf("%v; %v", e1, e2)
// })
//
// // ApV with both function and value having errors
// double := func(x int) int { return x * 2 }
// apv := result.ApV[int, int](errorSemigroup)
//
// value := result.Left[int](errors.New("invalid value"))
// fn := result.Left[func(int) int](errors.New("invalid function"))
//
// result := apv(value)(fn)
// // Left(error: "invalid function; invalid value")
//
// // ApV with successful application
// goodValue, _ := result.Right(5)
// goodFn, _ := result.Right(double)
// result2 := apv(goodValue)(goodFn)
// // Right(10)
func ApV[B, A any](sg S.Semigroup[error]) func(A, error) Operator[func(A) B, B] {
return func(a A, aerr error) Operator[func(A) B, B] {
return func(fab func(A) B, faberr error) (B, error) {
// Both have errors: combine them using the semigroup
if aerr != nil {
if faberr != nil {
return Left[B](sg.Concat(faberr, aerr))
}
// Only value has error
return Left[B](aerr)
}
// Only function has error
if faberr != nil {
return Left[B](faberr)
}
// Both are successful: apply function to value
return Of(fab(a))
}
}
}

View File

@@ -0,0 +1,373 @@
// 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 result
import (
"errors"
"fmt"
"strings"
"testing"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
// Helper function to create a semigroup that concatenates error messages
func makeErrorConcatSemigroup() S.Semigroup[error] {
return S.MakeSemigroup(func(e1, e2 error) error {
return fmt.Errorf("%v; %v", e1, e2)
})
}
// Helper function to create a semigroup that collects error messages in a slice
func makeErrorListSemigroup() S.Semigroup[error] {
return S.MakeSemigroup(func(e1, e2 error) error {
msg1 := e1.Error()
msg2 := e2.Error()
// Parse existing lists
var msgs []string
if strings.HasPrefix(msg1, "[") && strings.HasSuffix(msg1, "]") {
trimmed := strings.Trim(msg1, "[]")
if trimmed != "" {
msgs = strings.Split(trimmed, ", ")
}
} else {
msgs = []string{msg1}
}
if strings.HasPrefix(msg2, "[") && strings.HasSuffix(msg2, "]") {
trimmed := strings.Trim(msg2, "[]")
if trimmed != "" {
msgs = append(msgs, strings.Split(trimmed, ", ")...)
}
} else {
msgs = append(msgs, msg2)
}
return fmt.Errorf("[%s]", strings.Join(msgs, ", "))
})
}
// TestApV_BothRight tests ApV when both the value and function are Right
func TestApV_BothRight(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
value, verr := Right(5)
fn, ferr := Right(double)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, 10, result)
}
// TestApV_ValueLeft_FunctionRight tests ApV when value is Left and function is Right
func TestApV_ValueLeft_FunctionRight(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
valueError := errors.New("invalid value")
value, verr := Left[int](valueError)
fn, ferr := Right(double)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, valueError, err)
assert.Equal(t, 0, result) // zero value for int
}
// TestApV_ValueRight_FunctionLeft tests ApV when value is Right and function is Left
func TestApV_ValueRight_FunctionLeft(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
fnError := errors.New("invalid function")
value, verr := Right(5)
fn, ferr := Left[func(int) int](fnError)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, fnError, err)
assert.Equal(t, 0, result) // zero value for int
}
// TestApV_BothLeft tests ApV when both value and function are Left
func TestApV_BothLeft(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
valueError := errors.New("invalid value")
fnError := errors.New("invalid function")
value, verr := Left[int](valueError)
fn, ferr := Left[func(int) int](fnError)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, 0, result) // zero value for int
// Verify the error message contains both errors
expectedMsg := "invalid function; invalid value"
assert.Equal(t, expectedMsg, err.Error())
}
// TestApV_BothLeft_WithListSemigroup tests error accumulation with a list semigroup
func TestApV_BothLeft_WithListSemigroup(t *testing.T) {
sg := makeErrorListSemigroup()
apv := ApV[string, string](sg)
valueError := errors.New("error1")
fnError := errors.New("error2")
value, verr := Left[string](valueError)
fn, ferr := Left[func(string) string](fnError)
result, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Equal(t, "", result) // zero value for string
// Verify both errors are in the list
expectedMsg := "[error2, error1]"
assert.Equal(t, expectedMsg, err.Error())
}
// TestApV_StringTransformation tests ApV with string transformation
func TestApV_StringTransformation(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[string, string](sg)
toUpper := func(s string) string { return strings.ToUpper(s) }
value, verr := Right("hello")
fn, ferr := Right(toUpper)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "HELLO", result)
}
// TestApV_DifferentTypes tests ApV with different input and output types
func TestApV_DifferentTypes(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[string, int](sg)
intToString := func(x int) string { return fmt.Sprintf("Number: %d", x) }
value, verr := Right(42)
fn, ferr := Right(intToString)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "Number: 42", result)
}
// TestApV_ComplexType tests ApV with complex types (structs)
func TestApV_ComplexType(t *testing.T) {
type Person struct {
Name string
Age int
}
sg := makeErrorConcatSemigroup()
apv := ApV[string, Person](sg)
getName := func(p Person) string { return p.Name }
person := Person{Name: "Alice", Age: 30}
value, verr := Right(person)
fn, ferr := Right(getName)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "Alice", result)
}
// TestApV_MultipleValidations demonstrates chaining multiple validations
func TestApV_MultipleValidations(t *testing.T) {
_ = makeErrorListSemigroup() // Semigroup available for future use
// Validation functions
validatePositive := func(x int) (int, error) {
if x > 0 {
return Right(x)
}
return Left[int](errors.New("must be positive"))
}
validateEven := func(x int) (int, error) {
if x%2 == 0 {
return Right(x)
}
return Left[int](errors.New("must be even"))
}
// Test valid value (positive and even)
t.Run("valid value", func(t *testing.T) {
value, err := Right(4)
validatedPositive, err1 := validatePositive(value)
validatedEven, err2 := validateEven(validatedPositive)
assert.NoError(t, err)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 4, validatedEven)
})
// Test invalid value (negative)
t.Run("negative value", func(t *testing.T) {
value, _ := Right(-3)
_, err := validatePositive(value)
assert.Error(t, err)
assert.Equal(t, "must be positive", err.Error())
})
// Test invalid value (odd)
t.Run("odd value", func(t *testing.T) {
value, _ := Right(3)
validatedPositive, err1 := validatePositive(value)
_, err2 := validateEven(validatedPositive)
assert.NoError(t, err1)
assert.Error(t, err2)
assert.Equal(t, "must be even", err2.Error())
})
}
// TestApV_ZeroValues tests ApV with zero values
func TestApV_ZeroValues(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
identity := func(x int) int { return x }
value, verr := Right(0)
fn, ferr := Right(identity)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, 0, result)
}
// TestApV_NilError tests that nil errors are handled correctly
func TestApV_NilError(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[string, string](sg)
identity := func(s string) string { return s }
// Right is equivalent to (value, nil)
value, verr := Right("test")
fn, ferr := Right(identity)
assert.Nil(t, verr)
assert.Nil(t, ferr)
result, err := apv(value, verr)(fn, ferr)
assert.NoError(t, err)
assert.Equal(t, "test", result)
}
// TestApV_SemigroupAssociativity tests that error combination is associative
func TestApV_SemigroupAssociativity(t *testing.T) {
sg := makeErrorConcatSemigroup()
e1 := errors.New("error1")
e2 := errors.New("error2")
e3 := errors.New("error3")
// (e1 + e2) + e3
left := sg.Concat(sg.Concat(e1, e2), e3)
// e1 + (e2 + e3)
right := sg.Concat(e1, sg.Concat(e2, e3))
assert.Equal(t, left.Error(), right.Error())
}
// TestApV_CustomSemigroup tests ApV with a custom semigroup
func TestApV_CustomSemigroup(t *testing.T) {
// Custom semigroup that counts errors
type ErrorCount struct {
count int
msg string
}
countSemigroup := S.MakeSemigroup(func(e1, e2 error) error {
// Simple counter in error message
return fmt.Errorf("combined: %v | %v", e1, e2)
})
apv := ApV[int, int](countSemigroup)
e1 := errors.New("first")
e2 := errors.New("second")
value, verr := Left[int](e1)
fn, ferr := Left[func(int) int](e2)
_, err := apv(value, verr)(fn, ferr)
assert.Error(t, err)
assert.Contains(t, err.Error(), "first")
assert.Contains(t, err.Error(), "second")
}
// BenchmarkApV_BothRight benchmarks the happy path
func BenchmarkApV_BothRight(b *testing.B) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
double := func(x int) int { return x * 2 }
value, verr := Right(5)
fn, ferr := Right(double)
b.ResetTimer()
for i := 0; i < b.N; i++ {
apv(value, verr)(fn, ferr)
}
}
// BenchmarkApV_BothLeft benchmarks the error accumulation path
func BenchmarkApV_BothLeft(b *testing.B) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
valueError := errors.New("value error")
fnError := errors.New("function error")
value, verr := Left[int](valueError)
fn, ferr := Left[func(int) int](fnError)
b.ResetTimer()
for i := 0; i < b.N; i++ {
apv(value, verr)(fn, ferr)
}
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package result
// Variadic0 converts a function taking a slice and returning (R, error) into a variadic function returning Either.
//
// Example:
//
// sum := func(nums []int) (int, error) {
// total := 0
// for _, n := range nums { total += n }
// return total, nil
// }
// variadicSum := either.Variadic0(sum)
// result := variadicSum(1, 2, 3) // Right(6)
func Variadic0[V, R any](f func([]V) (R, error)) func(...V) Either[error, R] {
return func(v ...V) Either[error, R] {
return TryCatchError(f(v))
}
}
// Variadic1 converts a function with 1 fixed parameter and a slice into a variadic function returning Either.
func Variadic1[T1, V, R any](f func(T1, []V) (R, error)) func(T1, ...V) Either[error, R] {
return func(t1 T1, v ...V) Either[error, R] {
return TryCatchError(f(t1, v))
}
}
// Variadic2 converts a function with 2 fixed parameters and a slice into a variadic function returning Either.
func Variadic2[T1, T2, V, R any](f func(T1, T2, []V) (R, error)) func(T1, T2, ...V) Either[error, R] {
return func(t1 T1, t2 T2, v ...V) Either[error, R] {
return TryCatchError(f(t1, t2, v))
}
}
// Variadic3 converts a function with 3 fixed parameters and a slice into a variadic function returning Either.
func Variadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, []V) (R, error)) func(T1, T2, T3, ...V) Either[error, R] {
return func(t1 T1, t2 T2, t3 T3, v ...V) Either[error, R] {
return TryCatchError(f(t1, t2, t3, v))
}
}
// Variadic4 converts a function with 4 fixed parameters and a slice into a variadic function returning Either.
func Variadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, []V) (R, error)) func(T1, T2, T3, T4, ...V) Either[error, R] {
return func(t1 T1, t2 T2, t3 T3, t4 T4, v ...V) Either[error, R] {
return TryCatchError(f(t1, t2, t3, t4, v))
}
}
// Unvariadic0 converts a variadic function returning (R, error) into a function taking a slice and returning Either.
func Unvariadic0[V, R any](f func(...V) (R, error)) func([]V) Either[error, R] {
return func(v []V) Either[error, R] {
return TryCatchError(f(v...))
}
}
// Unvariadic1 converts a variadic function with 1 fixed parameter into a function taking a slice and returning Either.
func Unvariadic1[T1, V, R any](f func(T1, ...V) (R, error)) func(T1, []V) Either[error, R] {
return func(t1 T1, v []V) Either[error, R] {
return TryCatchError(f(t1, v...))
}
}
// Unvariadic2 converts a variadic function with 2 fixed parameters into a function taking a slice and returning Either.
func Unvariadic2[T1, T2, V, R any](f func(T1, T2, ...V) (R, error)) func(T1, T2, []V) Either[error, R] {
return func(t1 T1, t2 T2, v []V) Either[error, R] {
return TryCatchError(f(t1, t2, v...))
}
}
// Unvariadic3 converts a variadic function with 3 fixed parameters into a function taking a slice and returning Either.
func Unvariadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, ...V) (R, error)) func(T1, T2, T3, []V) Either[error, R] {
return func(t1 T1, t2 T2, t3 T3, v []V) Either[error, R] {
return TryCatchError(f(t1, t2, t3, v...))
}
}
// Unvariadic4 converts a variadic function with 4 fixed parameters into a function taking a slice and returning Either.
func Unvariadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, ...V) (R, error)) func(T1, T2, T3, T4, []V) Either[error, R] {
return func(t1 T1, t2 T2, t3 T3, t4 T4, v []V) Either[error, R] {
return TryCatchError(f(t1, t2, t3, t4, v...))
}
}

View File

@@ -13,6 +13,33 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package testing provides law-based testing utilities for applicatives.
//
// This package implements property-based tests for the four fundamental 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))
//
// Additionally, it validates that applicatives satisfy all prerequisite laws from:
// - Functor (identity, composition)
// - Apply (composition)
//
// Usage:
//
// func TestMyApplicative(t *testing.T) {
// // Set up equality checkers
// eqa := eq.FromEquals[Option[int]](...)
// eqb := eq.FromEquals[Option[string]](...)
// eqc := eq.FromEquals[Option[float64]](...)
//
// // Set up applicative instances
// app := applicative.Applicative[int, string, Option[int], Option[string], Option[func(int) string]]()
//
// // Run the law tests
// lawTest := ApplicativeAssertLaws(t, eqa, eqb, eqc, ...)
// assert.True(t, lawTest(42))
// }
package testing
import (

View File

@@ -21,6 +21,42 @@ import (
"github.com/IBM/fp-go/v2/internal/pointed"
)
// Applicative represents a type that combines the ability to lift pure values into
// a context (Pointed) with the ability to apply wrapped functions to wrapped values (Apply).
//
// Applicative functors allow for function application lifted over a computational context,
// enabling multiple independent effects to be combined. This is the composition of Apply
// and Pointed, providing both the ability to create wrapped values and to apply wrapped
// functions.
//
// An Applicative must satisfy the following 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))
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
// - HKTA: The higher-kinded type containing A
// - HKTB: The higher-kinded type containing B
// - HKTFAB: The higher-kinded type containing a function from A to B
//
// Example:
//
// // Given an Applicative for Option
// var app Applicative[int, string, Option[int], Option[string], Option[func(int) string]]
// value := app.Of(42) // Returns Some(42)
// fn := app.Of(strconv.Itoa)
// result := app.Ap(value)(fn) // Returns Some("42")
type Applicative[A, B, HKTA, HKTB, HKTFAB any] interface {
apply.Apply[A, B, HKTA, HKTB, HKTFAB]
pointed.Pointed[A, HKTA]

View File

@@ -19,8 +19,40 @@ import (
"github.com/IBM/fp-go/v2/internal/functor"
)
// Apply represents a Functor with the ability to apply a function wrapped in a context
// to a value wrapped in a context.
//
// Apply extends Functor by adding the Ap method, which allows for application of
// functions that are themselves wrapped in the same computational context. This enables
// independent computations to be combined.
//
// An Apply must satisfy the following laws:
//
// Composition:
//
// Ap(Ap(Map(compose)(f))(g))(x) == Ap(f)(Ap(g)(x))
//
// Type Parameters:
// - A: The input value type
// - B: The output value type after function application
// - HKTA: The higher-kinded type containing A
// - HKTB: The higher-kinded type containing B
// - HKTFAB: The higher-kinded type containing a function from A to B
//
// Example:
//
// // Given an Apply for Option
// var ap Apply[int, string, Option[int], Option[string], Option[func(int) string]]
// fn := Some(strconv.Itoa)
// applyFn := ap.Ap(Some(42))
// result := applyFn(fn) // Returns Some("42")
type Apply[A, B, HKTA, HKTB, HKTFAB any] interface {
functor.Functor[A, B, HKTA, HKTB]
// Ap applies a function wrapped in a context to a value wrapped in a context.
//
// Takes a value in context (HKTA) and returns a function that takes a function
// in context (HKTFAB) and produces a result in context (HKTB).
Ap(HKTA) func(HKTFAB) HKTB
}

View File

@@ -20,8 +20,45 @@ import (
"github.com/IBM/fp-go/v2/internal/functor"
)
// Chainable represents a type that supports sequential composition of computations,
// where each computation depends on the result of the previous one.
//
// Chainable extends Apply by adding the Chain method (also known as flatMap or bind),
// which allows for dependent sequencing of computations. Unlike Ap, which combines
// independent computations, Chain allows the structure of the second computation to
// depend on the value produced by the first.
//
// A Chainable must satisfy the following laws:
//
// Associativity:
// Chain(f)(Chain(g)(m)) == Chain(x => Chain(f)(g(x)))(m)
//
// Type Parameters:
// - A: The input value type
// - B: The output value type after chaining
// - HKTA: The higher-kinded type containing A
// - HKTB: The higher-kinded type containing B
// - HKTFAB: The higher-kinded type containing a function from A to B
//
// Example:
// // Given a Chainable for Option
// var c Chainable[int, string, Option[int], Option[string], Option[func(int) string]]
// chainFn := c.Chain(func(x int) Option[string] {
// if x > 0 {
// return Some(strconv.Itoa(x))
// }
// return None[string]()
// })
// result := chainFn(Some(42)) // Returns Some("42")
type Chainable[A, B, HKTA, HKTB, HKTFAB any] interface {
apply.Apply[A, B, HKTA, HKTB, HKTFAB]
// Chain sequences computations where the second computation depends on the
// value produced by the first.
//
// Takes a function that produces a new context-wrapped value based on the
// unwrapped input, and returns a function that applies this to a context-wrapped
// input, flattening the nested context.
Chain(func(A) HKTB) func(HKTA) HKTB
}

View File

@@ -15,6 +15,35 @@
package functor
// Functor represents a type that can be mapped over, allowing transformation of values
// contained within a context while preserving the structure of that context.
//
// A Functor must satisfy the following laws:
//
// Identity:
//
// Map(identity) == identity
//
// Composition:
//
// Map(f ∘ g) == Map(f) ∘ Map(g)
//
// Type Parameters:
// - A: The input value type contained in the functor
// - B: The output value type after transformation
// - HKTA: The higher-kinded type containing A (e.g., Option[A], Either[E, A])
// - HKTB: The higher-kinded type containing B (e.g., Option[B], Either[E, B])
//
// Example:
//
// // Given a functor for Option[int]
// var f Functor[int, string, Option[int], Option[string]]
// mapFn := f.Map(strconv.Itoa)
// result := mapFn(Some(42)) // Returns Some("42")
type Functor[A, B, HKTA, HKTB any] interface {
// Map transforms the value inside the functor using the provided function,
// preserving the structure of the functor.
//
// Returns a function that takes a functor containing A and returns a functor containing B.
Map(func(A) B) func(HKTA) HKTB
}

View File

@@ -23,6 +23,47 @@ import (
"github.com/IBM/fp-go/v2/internal/pointed"
)
// Monad represents the full monadic interface, combining Applicative and Chainable.
//
// A Monad provides the complete set of operations for working with computational contexts:
// - Map (from Functor): transform values within a context
// - Of (from Pointed): lift pure values into a context
// - Ap (from Apply): apply wrapped functions to wrapped values
// - Chain (from Chainable): sequence dependent computations
//
// Monads must satisfy the 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)
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
// - HKTA: The higher-kinded type containing A
// - HKTB: The higher-kinded type containing B
// - HKTFAB: The higher-kinded type containing a function from A to B
//
// Example:
// // Given a Monad for Option
// var m Monad[int, string, Option[int], Option[string], Option[func(int) string]]
//
// // Use Of to create a value
// value := m.Of(42) // Some(42)
//
// // Use Chain for dependent operations
// chainFn := m.Chain(func(x int) Option[string] {
// if x > 0 {
// return Some(strconv.Itoa(x))
// }
// return None[string]()
// })
// result := chainFn(value) // Some("42")
type Monad[A, B, HKTA, HKTB, HKTFAB any] interface {
applicative.Applicative[A, B, HKTA, HKTB, HKTFAB]
chain.Chainable[A, B, HKTA, HKTB, HKTFAB]

View File

@@ -13,9 +13,40 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package testing provides law-based testing utilities for monads.
//
// This package implements property-based tests for the three fundamental 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 validates that monads satisfy all prerequisite laws from:
// - Functor (identity, composition)
// - Apply (composition)
// - Applicative (identity, homomorphism, interchange)
// - Chainable (associativity)
//
// Usage:
//
// func TestMyMonad(t *testing.T) {
// // Set up equality checkers
// eqa := eq.FromEquals[Option[int]](...)
// eqb := eq.FromEquals[Option[string]](...)
// eqc := eq.FromEquals[Option[float64]](...)
//
// // Set up monad instances
// maa := &optionMonad[int, int]{}
// mab := &optionMonad[int, string]{}
// // ... etc
//
// // Run the law tests
// lawTest := MonadAssertLaws(t, eqa, eqb, eqc, ...)
// assert.True(t, lawTest(42))
// }
package testing
import (
"fmt"
"testing"
E "github.com/IBM/fp-go/v2/eq"
@@ -29,10 +60,12 @@ import (
"github.com/stretchr/testify/assert"
)
// Apply monad left identity law
//
// AssertLeftIdentity tests the monad left identity law:
// M.chain(M.of(a), f) <-> f(a)
//
// This law ensures that lifting a value into the monad and immediately chaining
// a function over it is equivalent to just applying the function directly.
//
// Deprecated: use [MonadAssertLeftIdentity] instead
func AssertLeftIdentity[HKTA, HKTB, A, B any](t *testing.T,
eq E.Eq[HKTB],
@@ -44,6 +77,8 @@ func AssertLeftIdentity[HKTA, HKTB, A, B any](t *testing.T,
ab func(A) B,
) func(a A) bool {
t.Helper()
return func(a A) bool {
f := func(a A) HKTB {
@@ -53,13 +88,35 @@ func AssertLeftIdentity[HKTA, HKTB, A, B any](t *testing.T,
left := fchain(fofa(a), f)
right := f(a)
return assert.True(t, eq.Equals(left, right), "Monad left identity")
result := eq.Equals(left, right)
if !result {
t.Logf("Monad left identity violated: Chain(Of(%v), f) != f(%v)", a, a)
}
return assert.True(t, result, "Monad left identity")
}
}
// Apply monad left identity law
//
// MonadAssertLeftIdentity tests the monad left identity law:
// M.chain(M.of(a), f) <-> f(a)
//
// This law states that wrapping a value with Of and immediately chaining with a function f
// should be equivalent to just applying f to the value directly.
//
// In other words: lifting a value into the monad and then binding over it should have
// the same effect as just applying the function.
//
// Example:
//
// // For Option monad with value 42 and function f(x) = Some(toString(x)):
// Chain(f)(Of(42)) == f(42)
// // Both produce: Some("42")
//
// Type Parameters:
// - A: Input value type
// - B: Output value type
// - HKTA: Higher-kinded type containing A
// - HKTB: Higher-kinded type containing B
// - HKTFAB: Higher-kinded type containing function A -> B
func MonadAssertLeftIdentity[HKTA, HKTB, HKTFAB, A, B any](t *testing.T,
eq E.Eq[HKTB],
@@ -69,6 +126,8 @@ func MonadAssertLeftIdentity[HKTA, HKTB, HKTFAB, A, B any](t *testing.T,
ab func(A) B,
) func(a A) bool {
t.Helper()
return func(a A) bool {
f := func(a A) HKTB {
@@ -78,14 +137,23 @@ func MonadAssertLeftIdentity[HKTA, HKTB, HKTFAB, A, B any](t *testing.T,
left := ma.Chain(f)(ma.Of(a))
right := f(a)
return assert.True(t, eq.Equals(left, right), "Monad left identity")
result := eq.Equals(left, right)
if !result {
t.Errorf("Monad left identity law violated:\n"+
" Chain(f)(Of(a)) != f(a)\n"+
" where a = %v\n"+
" Expected: Chain(f)(Of(a)) to equal f(a)", a)
}
return assert.True(t, result, "Monad left identity")
}
}
// Apply monad right identity law
//
// AssertRightIdentity tests the monad right identity law:
// M.chain(fa, M.of) <-> fa
//
// This law ensures that chaining a monadic value with the Of (pure/return) function
// returns the original monadic value unchanged.
//
// Deprecated: use [MonadAssertRightIdentity] instead
func AssertRightIdentity[HKTA, A any](t *testing.T,
eq E.Eq[HKTA],
@@ -94,34 +162,72 @@ func AssertRightIdentity[HKTA, A any](t *testing.T,
fchain func(HKTA, func(A) HKTA) HKTA,
) func(fa HKTA) bool {
t.Helper()
return func(fa HKTA) bool {
left := fchain(fa, fofa)
right := fa
return assert.True(t, eq.Equals(left, right), "Monad right identity")
result := eq.Equals(left, right)
if !result {
t.Logf("Monad right identity violated: Chain(fa, Of) != fa")
}
return assert.True(t, result, "Monad right identity")
}
}
// Apply monad right identity law
//
// MonadAssertRightIdentity tests the monad right identity law:
// M.chain(fa, M.of) <-> fa
//
// This law states that chaining a monadic value with the Of (pure/return) function
// should return the original monadic value unchanged. This ensures that Of doesn't
// add any additional structure or effects beyond what's already present.
//
// Example:
//
// // For Option monad with value Some(42):
// Chain(Of)(Some(42)) == Some(42)
// // The value remains unchanged
//
// Type Parameters:
// - A: The value type
// - HKTA: Higher-kinded type containing A
// - HKTAA: Higher-kinded type containing function A -> A
func MonadAssertRightIdentity[HKTA, HKTAA, A any](t *testing.T,
eq E.Eq[HKTA],
ma monad.Monad[A, A, HKTA, HKTA, HKTAA],
) func(fa HKTA) bool {
t.Helper()
return func(fa HKTA) bool {
left := ma.Chain(ma.Of)(fa)
right := fa
return assert.True(t, eq.Equals(left, right), "Monad right identity")
result := eq.Equals(left, right)
if !result {
t.Errorf("Monad right identity law violated:\n" +
" Chain(Of)(fa) != fa\n" +
" Expected: Chain(Of)(fa) to equal fa")
}
return assert.True(t, result, "Monad right identity")
}
}
// AssertLaws asserts the apply laws `identity`, `composition`, `associative composition`, 'applicative identity', 'homomorphism', 'interchange', `associativity`, `left identity`, `right identity`
// AssertLaws tests all monad laws including prerequisite laws from Functor, Apply,
// Applicative, and Chainable.
//
// This function validates:
// - Functor laws: identity, composition
// - Apply law: composition
// - Applicative laws: identity, homomorphism, interchange
// - Chainable law: associativity
// - Monad laws: left identity, right identity
//
// The monad laws build upon and require all the prerequisite laws to hold.
//
// Deprecated: use [MonadAssertLaws] instead
func AssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTABAC, A, B, C any](t *testing.T,
@@ -161,6 +267,8 @@ func AssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTABAC, A
ab func(A) B,
bc func(B) C,
) func(a A) bool {
t.Helper()
// applicative laws
applicative := LA.AssertLaws(t, eqa, eqb, eqc, fofa, fofb, fofaa, fofab, fofbc, fofabb, faa, fab, fac, fbc, fmap, fapaa, fapab, fapbc, fapac, fapabb, fapabac, ab, bc)
// chain laws
@@ -171,11 +279,68 @@ func AssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTABAC, A
return func(a A) bool {
fa := fofa(a)
return applicative(a) && chain(fa) && leftIdentity(a) && rightIdentity(fa)
appOk := applicative(a)
chainOk := chain(fa)
leftIdOk := leftIdentity(a)
rightIdOk := rightIdentity(fa)
if !appOk {
t.Logf("Applicative laws failed for input: %v", a)
}
if !chainOk {
t.Logf("Chain laws failed for input: %v", fa)
}
if !leftIdOk {
t.Logf("Left identity law failed for input: %v", a)
}
if !rightIdOk {
t.Logf("Right identity law failed for input: %v", fa)
}
return appOk && chainOk && leftIdOk && rightIdOk
}
}
// MonadAssertLaws asserts the apply laws `identity`, `composition`, `associative composition`, 'applicative identity', 'homomorphism', 'interchange', `associativity`, `left identity`, `right identity`
// MonadAssertLaws validates all monad laws and prerequisite laws for a monad implementation.
//
// This is the primary testing function for verifying monad implementations. It checks:
//
// Monad Laws (primary):
// - 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)
//
// Prerequisite Laws (inherited from parent type classes):
// - Functor: identity, composition
// - Apply: composition
// - Applicative: identity, homomorphism, interchange
// - Chainable: associativity
//
// Usage:
//
// func TestOptionMonad(t *testing.T) {
// // Create equality checkers
// eqa := eq.FromEquals[Option[int]](optionEq[int])
// eqb := eq.FromEquals[Option[string]](optionEq[string])
// eqc := eq.FromEquals[Option[float64]](optionEq[float64])
//
// // Define test functions
// ab := strconv.Itoa
// bc := func(s string) float64 { v, _ := strconv.ParseFloat(s, 64); return v }
//
// // Create monad instances and other required type class instances
// // ... (setup code)
//
// // Run the law tests
// lawTest := MonadAssertLaws(t, eqa, eqb, eqc, ...)
// assert.True(t, lawTest(42))
// }
//
// Type Parameters:
// - A, B, C: Value types for testing transformations
// - HKTA, HKTB, HKTC: Higher-kinded types containing A, B, C
// - HKTAA, HKTAB, HKTBC, HKTAC: Higher-kinded types containing functions
// - HKTABB, HKTABAC: Higher-kinded types for applicative testing
func MonadAssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTABAC, A, B, C any](t *testing.T,
eqa E.Eq[HKTA],
eqb E.Eq[HKTB],
@@ -199,7 +364,9 @@ func MonadAssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTAB
ab func(A) B,
bc func(B) C,
) func(a A) bool {
// derivations
t.Helper()
// Derive required type class instances from monad instances
fofa := monad.ToPointed(maa)
fofb := monad.ToPointed(mbc)
fofab := applicative.ToPointed(fapabb)
@@ -213,16 +380,140 @@ func MonadAssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTAB
faa := monad.ToFunctor(maa)
// applicative laws
// Test prerequisite laws from parent type classes
apLaw := LA.ApplicativeAssertLaws(t, eqa, eqb, eqc, fofb, fofaa, fofbc, fofabb, faa, fmap, fapaa, fapab, fapbc, fapac, fapabb, fapabac, ab, bc)
// chain laws
chainLaw := LC.ChainAssertLaws(t, eqa, eqc, fofb, fofc, fofab, fofbc, faa, fmap, chainab, chainac, chainbc, applicative.ToApply(fapabac), ab, bc)
// monad laws
// Test monad-specific laws
leftIdentity := MonadAssertLeftIdentity(t, eqb, fofb, mab, ab)
rightIdentity := MonadAssertRightIdentity(t, eqa, maa)
return func(a A) bool {
fa := fofa.Of(a)
return apLaw(a) && chainLaw(fa) && leftIdentity(a) && rightIdentity(fa)
// Run all law tests and collect results
apOk := apLaw(a)
chainOk := chainLaw(fa)
leftIdOk := leftIdentity(a)
rightIdOk := rightIdentity(fa)
// Log detailed failure information
if !apOk {
t.Errorf("Monad prerequisite failure: Applicative laws violated for input: %v", a)
}
if !chainOk {
t.Errorf("Monad prerequisite failure: Chain laws violated for monadic value: %v", fa)
}
if !leftIdOk {
t.Errorf("Monad law failure: Left identity violated for input: %v", a)
}
if !rightIdOk {
t.Errorf("Monad law failure: Right identity violated for monadic value: %v", fa)
}
allOk := apOk && chainOk && leftIdOk && rightIdOk
if allOk {
t.Logf("✓ All monad laws satisfied for input: %v", a)
}
return allOk
}
}
// MonadAssertAssociativity is a convenience function that tests only the monad associativity law
// (which is inherited from Chainable).
//
// Associativity Law:
//
// Chain(g)(Chain(f)(m)) == Chain(x => Chain(g)(f(x)))(m)
//
// This law ensures that the order in which we nest chain operations doesn't matter,
// as long as the sequence of operations remains the same.
//
// Example:
//
// // For Option monad:
// f := func(x int) Option[string] { return Some(strconv.Itoa(x)) }
// g := func(s string) Option[float64] { return Some(parseFloat(s)) }
// m := Some(42)
//
// // These should be equal:
// Chain(g)(Chain(f)(m)) // Some(42.0)
// Chain(func(x int) { Chain(g)(f(x)) })(m) // Some(42.0)
//
// Type Parameters:
// - A, B, C: Value types for the transformation chain
// - HKTA, HKTB, HKTC: Higher-kinded types containing A, B, C
// - HKTAB, HKTAC, HKTBC: Higher-kinded types containing functions
func MonadAssertAssociativity[HKTA, HKTB, HKTC, HKTAB, HKTAC, HKTBC, A, B, C any](
t *testing.T,
eq E.Eq[HKTC],
fofb pointed.Pointed[B, HKTB],
fofc pointed.Pointed[C, HKTC],
mab monad.Monad[A, B, HKTA, HKTB, HKTAB],
mac monad.Monad[A, C, HKTA, HKTC, HKTAC],
mbc monad.Monad[B, C, HKTB, HKTC, HKTBC],
ab func(A) B,
bc func(B) C,
) func(fa HKTA) bool {
t.Helper()
chainab := monad.ToChainable(mab)
chainac := monad.ToChainable(mac)
chainbc := monad.ToChainable(mbc)
return LC.ChainAssertAssociativity(t, eq, fofb, fofc, chainab, chainac, chainbc, ab, bc)
}
// TestMonadLaws is a helper function that runs all monad law tests with common test values.
//
// This is a convenience wrapper around MonadAssertLaws that runs the law tests with
// a set of test values and reports the results. It's useful for quick testing with
// standard inputs.
//
// Parameters:
// - t: The testing.T instance
// - name: A descriptive name for this test suite
// - testValues: A slice of values of type A to test with
// - Other parameters: Same as MonadAssertLaws
//
// Example:
//
// func TestOptionMonadLaws(t *testing.T) {
// testValues := []int{0, 1, -1, 42, 100}
// TestMonadLaws(t, "Option[int]", testValues, eqa, eqb, eqc, ...)
// }
func TestMonadLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTABAC, A, B, C any](
t *testing.T,
name string,
testValues []A,
eqa E.Eq[HKTA],
eqb E.Eq[HKTB],
eqc E.Eq[HKTC],
fofc pointed.Pointed[C, HKTC],
fofaa pointed.Pointed[func(A) A, HKTAA],
fofbc pointed.Pointed[func(B) C, HKTBC],
fofabb pointed.Pointed[func(func(A) B) B, HKTABB],
fmap functor.Functor[func(B) C, func(func(A) B) func(A) C, HKTBC, HKTABAC],
fapabb applicative.Applicative[func(A) B, B, HKTAB, HKTB, HKTABB],
fapabac applicative.Applicative[func(A) B, func(A) C, HKTAB, HKTAC, HKTABAC],
maa monad.Monad[A, A, HKTA, HKTA, HKTAA],
mab monad.Monad[A, B, HKTA, HKTB, HKTAB],
mac monad.Monad[A, C, HKTA, HKTC, HKTAC],
mbc monad.Monad[B, C, HKTB, HKTC, HKTBC],
ab func(A) B,
bc func(B) C,
) {
t.Helper()
lawTest := MonadAssertLaws(t, eqa, eqb, eqc, fofc, fofaa, fofbc, fofabb, fmap, fapabb, fapabac, maa, mab, mac, mbc, ab, bc)
t.Run(fmt.Sprintf("MonadLaws_%s", name), func(t *testing.T) {
for i, val := range testValues {
t.Run(fmt.Sprintf("Value_%d", i), func(t *testing.T) {
result := lawTest(val)
assert.True(t, result, "Monad laws should hold for value: %v", val)
})
}
})
}

View File

@@ -15,7 +15,24 @@
package pointed
// Pointed represents a type that can lift a pure value into a computational context.
//
// Pointed is the minimal extension of a Functor that adds the ability to create
// a context-wrapped value from a bare value. It provides the canonical way to
// construct values of a higher-kinded type.
//
// Type Parameters:
// - A: The value type to be lifted into the context
// - HKTA: The higher-kinded type containing A (e.g., Option[A], Either[E, A])
//
// Example:
// // Given a pointed functor for Option[int]
// var p Pointed[int, Option[int]]
// result := p.Of(42) // Returns Some(42)
type Pointed[A, HKTA any] interface {
// Of lifts a value into its higher kinded type
// Of lifts a pure value into its higher-kinded type context.
//
// This operation wraps a value A in the minimal context required by the type HKTA,
// creating a valid instance of the higher-kinded type.
Of(A) HKTA
}

View File

@@ -17,7 +17,6 @@ package option
import (
F "github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
)
// TraverseArrayG transforms an array by applying a function that returns an Option to each element.
@@ -34,13 +33,17 @@ import (
// result := TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"}) // Some([1, 2, 3])
// result := TraverseArrayG[[]string, []int](parse)([]string{"1", "x", "3"}) // None
func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
return RA.Traverse[GA](
Of[GB],
Map[GB, func(B) GB],
Ap[GB, B],
f,
)
return func(g GA) Option[GB] {
bs := make(GB, len(g))
for i, a := range g {
b := f(a)
if !b.isSome {
return None[GB]()
}
bs[i] = b.value
}
return Some(bs)
}
}
// TraverseArray transforms an array by applying a function that returns an Option to each element.
@@ -54,6 +57,8 @@ func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f Kleisli[A, B]) Kleisli[GA, GB]
// }
// result := TraverseArray(validate)([]int{1, 2, 3}) // Some([2, 4, 6])
// result := TraverseArray(validate)([]int{1, -1, 3}) // None
//
//go:inline
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return TraverseArrayG[[]A, []B](f)
}
@@ -69,13 +74,17 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
// }
// result := TraverseArrayWithIndexG[[]string, []string](f)([]string{"a", "b"}) // Some(["0:a", "1:b"])
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) Option[B]) Kleisli[GA, GB] {
return RA.TraverseWithIndex[GA](
Of[GB],
Map[GB, func(B) GB],
Ap[GB, B],
f,
)
return func(g GA) Option[GB] {
bs := make(GB, len(g))
for i, a := range g {
b := f(i, a)
if !b.isSome {
return None[GB]()
}
bs[i] = b.value
}
return Some(bs)
}
}
// TraverseArrayWithIndex transforms an array by applying an indexed function that returns an Option.
@@ -88,6 +97,8 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) Option[B
// return None[int]()
// }
// result := TraverseArrayWithIndex(f)([]int{1, 2, 3}) // Some([1, 2, 3])
//
//go:inline
func TraverseArrayWithIndex[A, B any](f func(int, A) Option[B]) Kleisli[[]A, []B] {
return TraverseArrayWithIndexG[[]A, []B](f)
}
@@ -101,6 +112,8 @@ func TraverseArrayWithIndex[A, B any](f func(int, A) Option[B]) Kleisli[[]A, []B
// type MySlice []int
// result := SequenceArrayG[MySlice]([]Option[int]{Some(1), Some(2)}) // Some(MySlice{1, 2})
// result := SequenceArrayG[MySlice]([]Option[int]{Some(1), None[int]()}) // None
//
//go:inline
func SequenceArrayG[GA ~[]A, GOA ~[]Option[A], A any](ma GOA) Option[GA] {
return TraverseArrayG[GOA, GA](F.Identity[Option[A]])(ma)
}
@@ -125,9 +138,13 @@ func SequenceArray[A any](ma []Option[A]) Option[[]A] {
// input := []Option[int]{Some(1), None[int](), Some(3)}
// result := CompactArrayG[[]Option[int], MySlice](input) // MySlice{1, 3}
func CompactArrayG[A1 ~[]Option[A], A2 ~[]A, A any](fa A1) A2 {
return RA.Reduce(fa, func(out A2, value Option[A]) A2 {
return MonadFold(value, F.Constant(out), F.Bind1st(RA.Append[A2, A], out))
}, make(A2, 0, len(fa)))
as := make(A2, 0, len(fa))
for _, oa := range fa {
if oa.isSome {
as = append(as, oa.value)
}
}
return as
}
// CompactArray filters an array of Options, keeping only the Some values and discarding None values.
@@ -136,6 +153,8 @@ func CompactArrayG[A1 ~[]Option[A], A2 ~[]A, A any](fa A1) A2 {
//
// input := []Option[int]{Some(1), None[int](), Some(3), Some(5), None[int]()}
// result := CompactArray(input) // [1, 3, 5]
//
//go:inline
func CompactArray[A any](fa []Option[A]) []A {
return CompactArrayG[[]Option[A], []A](fa)
}

209
v2/option/benchmark_test.go Normal file
View File

@@ -0,0 +1,209 @@
// 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 option
import (
"testing"
)
// Benchmark basic construction
func BenchmarkSome(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = Some(42)
}
}
func BenchmarkNone(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = None[int]()
}
}
// Benchmark basic operations
func BenchmarkIsSome(b *testing.B) {
opt := Some(42)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = IsSome(opt)
}
}
func BenchmarkMap(b *testing.B) {
opt := Some(21)
mapper := Map(func(x int) int { return x * 2 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = mapper(opt)
}
}
func BenchmarkChain(b *testing.B) {
opt := Some(21)
chainer := Chain(func(x int) Option[int] {
if x > 0 {
return Some(x * 2)
}
return None[int]()
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = chainer(opt)
}
}
func BenchmarkFilter(b *testing.B) {
opt := Some(42)
filter := Filter(func(x int) bool { return x > 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filter(opt)
}
}
func BenchmarkGetOrElse(b *testing.B) {
opt := Some(42)
getter := GetOrElse(func() int { return 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = getter(opt)
}
}
// Benchmark collection operations
func BenchmarkTraverseArray_Small(b *testing.B) {
data := []int{1, 2, 3, 4, 5}
traverser := TraverseArray(func(x int) Option[int] {
return Some(x * 2)
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = traverser(data)
}
}
func BenchmarkTraverseArray_Large(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
traverser := TraverseArray(func(x int) Option[int] {
return Some(x * 2)
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = traverser(data)
}
}
func BenchmarkSequenceArray_Small(b *testing.B) {
data := []Option[int]{Some(1), Some(2), Some(3), Some(4), Some(5)}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = SequenceArray(data)
}
}
func BenchmarkCompactArray_Small(b *testing.B) {
data := []Option[int]{Some(1), None[int](), Some(3), None[int](), Some(5)}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = CompactArray(data)
}
}
// Benchmark do-notation
func BenchmarkDoBind(b *testing.B) {
type State struct {
x int
y int
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadChain(
MonadChain(
Of(State{}),
func(s State) Option[State] {
s.x = 10
return Some(s)
},
),
func(s State) Option[State] {
s.y = 20
return Some(s)
},
)
}
}
// Benchmark conversions
func BenchmarkFromPredicate(b *testing.B) {
pred := FromPredicate(func(x int) bool { return x > 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = pred(42)
}
}
func BenchmarkFromNillable(b *testing.B) {
val := 42
ptr := &val
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = FromNillable(ptr)
}
}
func BenchmarkTryCatch(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = TryCatch(func() (int, error) {
return 42, nil
})
}
}
// Benchmark complex chains
func BenchmarkComplexChain(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadChain(
MonadChain(
MonadChain(
Some(1),
func(x int) Option[int] { return Some(x + 1) },
),
func(x int) Option[int] { return Some(x * 2) },
),
func(x int) Option[int] { return Some(x - 5) },
)
}
}

View File

@@ -0,0 +1,172 @@
// 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 option
import (
"testing"
)
// Benchmark shallow chain (1 step)
func BenchmarkChain_1Step(b *testing.B) {
opt := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadChain(opt, func(x int) Option[int] {
return Some(x + 1)
})
}
}
// Benchmark moderate chain (3 steps)
func BenchmarkChain_3Steps(b *testing.B) {
opt := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadChain(
MonadChain(
MonadChain(
opt,
func(x int) Option[int] { return Some(x + 1) },
),
func(x int) Option[int] { return Some(x * 2) },
),
func(x int) Option[int] { return Some(x - 5) },
)
}
}
// Benchmark deep chain (5 steps)
func BenchmarkChain_5Steps(b *testing.B) {
opt := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadChain(
MonadChain(
MonadChain(
MonadChain(
MonadChain(
opt,
func(x int) Option[int] { return Some(x + 1) },
),
func(x int) Option[int] { return Some(x * 2) },
),
func(x int) Option[int] { return Some(x - 5) },
),
func(x int) Option[int] { return Some(x * 10) },
),
func(x int) Option[int] { return Some(x + 100) },
)
}
}
// Benchmark very deep chain (10 steps)
func BenchmarkChain_10Steps(b *testing.B) {
opt := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = MonadChain(
MonadChain(
MonadChain(
MonadChain(
MonadChain(
MonadChain(
MonadChain(
MonadChain(
MonadChain(
MonadChain(
opt,
func(x int) Option[int] { return Some(x + 1) },
),
func(x int) Option[int] { return Some(x * 2) },
),
func(x int) Option[int] { return Some(x - 5) },
),
func(x int) Option[int] { return Some(x * 10) },
),
func(x int) Option[int] { return Some(x + 100) },
),
func(x int) Option[int] { return Some(x - 50) },
),
func(x int) Option[int] { return Some(x * 3) },
),
func(x int) Option[int] { return Some(x + 20) },
),
func(x int) Option[int] { return Some(x / 2) },
),
func(x int) Option[int] { return Some(x - 10) },
)
}
}
// Benchmark Map-based chain (should be faster due to inlining)
func BenchmarkMap_5Steps(b *testing.B) {
opt := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Map(func(x int) int { return x - 10 })(
Map(func(x int) int { return x / 2 })(
Map(func(x int) int { return x + 20 })(
Map(func(x int) int { return x * 3 })(
Map(func(x int) int { return x + 1 })(opt),
),
),
),
)
}
}
// Real-world example: parsing and validating user input
func BenchmarkChain_RealWorld_Validation(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
input := Some("42")
_ = MonadChain(
MonadChain(
MonadChain(
input,
// Step 1: Validate not empty
func(s string) Option[string] {
if len(s) > 0 {
return Some(s)
}
return None[string]()
},
),
// Step 2: Parse to int (simulated)
func(s string) Option[int] {
// Simplified: just check if numeric
if s == "42" {
return Some(42)
}
return None[int]()
},
),
// Step 3: Validate range
func(n int) Option[int] {
if n > 0 && n < 100 {
return Some(n)
}
return None[int]()
},
)
}
}

View File

@@ -117,6 +117,8 @@ func (s *Option[A]) UnmarshalJSON(data []byte) error {
// IsNone(opt) // true
// opt := Some(42)
// IsNone(opt) // false
//
//go:inline
func IsNone[T any](val Option[T]) bool {
return !val.isSome
}
@@ -127,6 +129,8 @@ func IsNone[T any](val Option[T]) bool {
//
// opt := Some(42) // Option containing 42
// opt := Some("hello") // Option containing "hello"
//
//go:inline
func Some[T any](value T) Option[T] {
return Option[T]{isSome: true, value: value}
}
@@ -137,6 +141,8 @@ func Some[T any](value T) Option[T] {
// Example:
//
// opt := Of(42) // Option containing 42
//
//go:inline
func Of[T any](value T) Option[T] {
return Some(value)
}
@@ -147,6 +153,8 @@ func Of[T any](value T) Option[T] {
//
// opt := None[int]() // Empty Option of type int
// opt := None[string]() // Empty Option of type string
//
//go:inline
func None[T any]() Option[T] {
return Option[T]{isSome: false}
}
@@ -159,6 +167,8 @@ func None[T any]() Option[T] {
// IsSome(opt) // true
// opt := None[int]()
// IsSome(opt) // false
//
//go:inline
func IsSome[T any](val Option[T]) bool {
return val.isSome
}
@@ -190,6 +200,8 @@ func MonadFold[A, B any](ma Option[A], onNone func() B, onSome func(A) B) B {
// val, ok := Unwrap(opt) // val = 42, ok = true
// opt := None[int]()
// val, ok := Unwrap(opt) // val = 0, ok = false
//
//go:inline
func Unwrap[A any](ma Option[A]) (A, bool) {
return ma.value, ma.isSome
}

233
v2/option/coverage.out Normal file
View File

@@ -0,0 +1,233 @@
mode: set
github.com/IBM/fp-go/v2/option/apply.go:32.69,34.2 1 1
github.com/IBM/fp-go/v2/option/apply.go:47.66,49.2 1 1
github.com/IBM/fp-go/v2/option/array.go:36.82,44.2 1 1
github.com/IBM/fp-go/v2/option/array.go:57.65,59.2 1 1
github.com/IBM/fp-go/v2/option/array.go:71.100,79.2 1 1
github.com/IBM/fp-go/v2/option/array.go:91.83,93.2 1 1
github.com/IBM/fp-go/v2/option/array.go:104.74,106.2 1 1
github.com/IBM/fp-go/v2/option/array.go:115.55,117.2 1 1
github.com/IBM/fp-go/v2/option/array.go:127.63,128.56 1 1
github.com/IBM/fp-go/v2/option/array.go:128.56,130.3 1 1
github.com/IBM/fp-go/v2/option/array.go:139.46,141.2 1 1
github.com/IBM/fp-go/v2/option/bind.go:38.13,40.2 1 1
github.com/IBM/fp-go/v2/option/bind.go:57.20,64.2 1 1
github.com/IBM/fp-go/v2/option/bind.go:81.20,87.2 1 1
github.com/IBM/fp-go/v2/option/bind.go:103.20,109.2 1 1
github.com/IBM/fp-go/v2/option/bind.go:123.19,128.2 1 1
github.com/IBM/fp-go/v2/option/bind.go:145.20,152.2 1 1
github.com/IBM/fp-go/v2/option/bind.go:190.18,192.2 1 0
github.com/IBM/fp-go/v2/option/bind.go:231.18,233.2 1 0
github.com/IBM/fp-go/v2/option/bind.go:267.18,269.2 1 0
github.com/IBM/fp-go/v2/option/bind.go:301.18,303.2 1 0
github.com/IBM/fp-go/v2/option/core.go:54.47,55.12 1 1
github.com/IBM/fp-go/v2/option/core.go:55.12,57.3 1 1
github.com/IBM/fp-go/v2/option/core.go:58.2,58.39 1 1
github.com/IBM/fp-go/v2/option/core.go:64.61,65.11 1 1
github.com/IBM/fp-go/v2/option/core.go:66.11,67.42 1 1
github.com/IBM/fp-go/v2/option/core.go:68.10,69.42 1 1
github.com/IBM/fp-go/v2/option/core.go:74.36,76.2 1 1
github.com/IBM/fp-go/v2/option/core.go:79.48,81.2 1 1
github.com/IBM/fp-go/v2/option/core.go:83.61,84.12 1 1
github.com/IBM/fp-go/v2/option/core.go:84.12,86.3 1 1
github.com/IBM/fp-go/v2/option/core.go:87.2,87.22 1 1
github.com/IBM/fp-go/v2/option/core.go:90.50,92.2 1 1
github.com/IBM/fp-go/v2/option/core.go:97.67,99.33 1 1
github.com/IBM/fp-go/v2/option/core.go:99.33,103.3 3 1
github.com/IBM/fp-go/v2/option/core.go:104.2,105.36 2 1
github.com/IBM/fp-go/v2/option/core.go:108.54,110.2 1 1
github.com/IBM/fp-go/v2/option/core.go:120.40,122.2 1 1
github.com/IBM/fp-go/v2/option/core.go:130.37,132.2 1 1
github.com/IBM/fp-go/v2/option/core.go:140.35,142.2 1 1
github.com/IBM/fp-go/v2/option/core.go:150.30,152.2 1 1
github.com/IBM/fp-go/v2/option/core.go:162.40,164.2 1 1
github.com/IBM/fp-go/v2/option/core.go:177.77,178.16 1 1
github.com/IBM/fp-go/v2/option/core.go:178.16,180.3 1 1
github.com/IBM/fp-go/v2/option/core.go:181.2,181.17 1 1
github.com/IBM/fp-go/v2/option/core.go:193.44,195.2 1 1
github.com/IBM/fp-go/v2/option/eq.go:36.45,44.2 2 1
github.com/IBM/fp-go/v2/option/eq.go:54.56,56.2 1 1
github.com/IBM/fp-go/v2/option/functor.go:24.63,26.2 1 1
github.com/IBM/fp-go/v2/option/functor.go:37.70,39.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:13.53,14.22 1 1
github.com/IBM/fp-go/v2/option/gen.go:14.22,16.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:17.2,17.18 1 1
github.com/IBM/fp-go/v2/option/gen.go:21.67,22.26 1 1
github.com/IBM/fp-go/v2/option/gen.go:22.26,23.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:23.37,25.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:30.69,31.26 1 1
github.com/IBM/fp-go/v2/option/gen.go:31.26,33.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:37.71,38.31 1 1
github.com/IBM/fp-go/v2/option/gen.go:38.31,39.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:39.37,41.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:46.73,47.31 1 1
github.com/IBM/fp-go/v2/option/gen.go:47.31,49.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:53.61,58.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:61.74,66.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:69.101,70.51 1 1
github.com/IBM/fp-go/v2/option/gen.go:70.51,76.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:80.87,81.38 1 1
github.com/IBM/fp-go/v2/option/gen.go:81.38,82.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:82.37,84.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:89.89,90.38 1 1
github.com/IBM/fp-go/v2/option/gen.go:90.38,92.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:96.84,103.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:106.94,112.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:115.145,116.59 1 1
github.com/IBM/fp-go/v2/option/gen.go:116.59,124.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:128.99,129.45 1 1
github.com/IBM/fp-go/v2/option/gen.go:129.45,130.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:130.37,132.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:137.101,138.45 1 1
github.com/IBM/fp-go/v2/option/gen.go:138.45,140.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:144.107,153.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:156.114,163.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:166.189,167.67 1 1
github.com/IBM/fp-go/v2/option/gen.go:167.67,177.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:181.111,182.52 1 1
github.com/IBM/fp-go/v2/option/gen.go:182.52,183.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:183.37,185.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:190.113,191.52 1 1
github.com/IBM/fp-go/v2/option/gen.go:191.52,193.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:197.130,208.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:211.134,219.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:222.233,223.75 1 1
github.com/IBM/fp-go/v2/option/gen.go:223.75,235.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:239.123,240.59 1 1
github.com/IBM/fp-go/v2/option/gen.go:240.59,241.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:241.37,243.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:248.125,249.59 1 1
github.com/IBM/fp-go/v2/option/gen.go:249.59,251.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:255.153,268.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:271.154,280.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:283.277,284.83 1 0
github.com/IBM/fp-go/v2/option/gen.go:284.83,298.3 1 0
github.com/IBM/fp-go/v2/option/gen.go:302.135,303.66 1 1
github.com/IBM/fp-go/v2/option/gen.go:303.66,304.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:304.37,306.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:311.137,312.66 1 1
github.com/IBM/fp-go/v2/option/gen.go:312.66,314.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:318.176,333.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:336.174,346.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:349.321,350.91 1 0
github.com/IBM/fp-go/v2/option/gen.go:350.91,366.3 1 0
github.com/IBM/fp-go/v2/option/gen.go:370.147,371.73 1 1
github.com/IBM/fp-go/v2/option/gen.go:371.73,372.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:372.37,374.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:379.149,380.73 1 1
github.com/IBM/fp-go/v2/option/gen.go:380.73,382.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:386.199,403.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:406.194,417.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:420.365,421.99 1 0
github.com/IBM/fp-go/v2/option/gen.go:421.99,439.3 1 0
github.com/IBM/fp-go/v2/option/gen.go:443.159,444.80 1 1
github.com/IBM/fp-go/v2/option/gen.go:444.80,445.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:445.37,447.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:452.161,453.80 1 1
github.com/IBM/fp-go/v2/option/gen.go:453.80,455.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:459.222,478.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:481.214,493.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:496.409,497.107 1 0
github.com/IBM/fp-go/v2/option/gen.go:497.107,517.3 1 0
github.com/IBM/fp-go/v2/option/gen.go:521.171,522.87 1 1
github.com/IBM/fp-go/v2/option/gen.go:522.87,523.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:523.37,525.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:530.173,531.87 1 1
github.com/IBM/fp-go/v2/option/gen.go:531.87,533.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:537.245,558.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:561.234,574.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:577.453,578.115 1 0
github.com/IBM/fp-go/v2/option/gen.go:578.115,600.3 1 0
github.com/IBM/fp-go/v2/option/gen.go:604.184,605.94 1 1
github.com/IBM/fp-go/v2/option/gen.go:605.94,606.37 1 1
github.com/IBM/fp-go/v2/option/gen.go:606.37,608.4 1 1
github.com/IBM/fp-go/v2/option/gen.go:613.186,614.94 1 1
github.com/IBM/fp-go/v2/option/gen.go:614.94,616.3 1 1
github.com/IBM/fp-go/v2/option/gen.go:620.274,643.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:646.260,660.2 1 1
github.com/IBM/fp-go/v2/option/gen.go:663.509,664.127 1 0
github.com/IBM/fp-go/v2/option/gen.go:664.127,688.3 1 0
github.com/IBM/fp-go/v2/option/iter.go:57.70,68.2 1 1
github.com/IBM/fp-go/v2/option/iter.go:70.60,76.2 1 0
github.com/IBM/fp-go/v2/option/logger.go:25.110,27.20 1 1
github.com/IBM/fp-go/v2/option/logger.go:27.20,30.4 2 1
github.com/IBM/fp-go/v2/option/logger.go:31.23,34.4 2 1
github.com/IBM/fp-go/v2/option/logger.go:58.79,60.51 2 1
github.com/IBM/fp-go/v2/option/logger.go:60.51,62.39 2 1
github.com/IBM/fp-go/v2/option/logger.go:62.39,67.4 1 1
github.com/IBM/fp-go/v2/option/monad.go:24.47,26.2 1 1
github.com/IBM/fp-go/v2/option/monad.go:28.61,30.2 1 1
github.com/IBM/fp-go/v2/option/monad.go:32.67,34.2 1 1
github.com/IBM/fp-go/v2/option/monad.go:36.80,38.2 1 1
github.com/IBM/fp-go/v2/option/monad.go:57.83,59.2 1 1
github.com/IBM/fp-go/v2/option/monoid.go:37.69,38.55 1 1
github.com/IBM/fp-go/v2/option/monoid.go:38.55,41.35 2 1
github.com/IBM/fp-go/v2/option/monoid.go:41.35,42.63 1 1
github.com/IBM/fp-go/v2/option/monoid.go:42.63,43.65 1 1
github.com/IBM/fp-go/v2/option/monoid.go:43.65,45.7 1 1
github.com/IBM/fp-go/v2/option/monoid.go:71.63,73.52 2 1
github.com/IBM/fp-go/v2/option/monoid.go:73.52,75.3 1 1
github.com/IBM/fp-go/v2/option/monoid.go:86.66,94.2 1 1
github.com/IBM/fp-go/v2/option/monoid.go:106.45,111.2 1 1
github.com/IBM/fp-go/v2/option/option.go:29.61,30.13 1 1
github.com/IBM/fp-go/v2/option/option.go:30.13,32.3 1 1
github.com/IBM/fp-go/v2/option/option.go:33.2,33.18 1 1
github.com/IBM/fp-go/v2/option/option.go:44.60,46.2 1 1
github.com/IBM/fp-go/v2/option/option.go:49.45,51.2 1 0
github.com/IBM/fp-go/v2/option/option.go:54.48,56.2 1 0
github.com/IBM/fp-go/v2/option/option.go:59.57,61.2 1 0
github.com/IBM/fp-go/v2/option/option.go:72.43,74.2 1 1
github.com/IBM/fp-go/v2/option/option.go:86.66,88.2 1 1
github.com/IBM/fp-go/v2/option/option.go:99.71,100.62 1 1
github.com/IBM/fp-go/v2/option/option.go:100.62,102.3 1 1
github.com/IBM/fp-go/v2/option/option.go:114.56,116.2 1 1
github.com/IBM/fp-go/v2/option/option.go:125.62,127.2 1 1
github.com/IBM/fp-go/v2/option/option.go:137.50,139.2 1 1
github.com/IBM/fp-go/v2/option/option.go:148.56,150.2 1 1
github.com/IBM/fp-go/v2/option/option.go:158.42,160.2 1 1
github.com/IBM/fp-go/v2/option/option.go:170.53,172.16 2 1
github.com/IBM/fp-go/v2/option/option.go:172.16,174.3 1 1
github.com/IBM/fp-go/v2/option/option.go:175.2,175.18 1 1
github.com/IBM/fp-go/v2/option/option.go:189.79,190.30 1 1
github.com/IBM/fp-go/v2/option/option.go:190.30,192.3 1 1
github.com/IBM/fp-go/v2/option/option.go:202.61,204.2 1 1
github.com/IBM/fp-go/v2/option/option.go:213.58,215.2 1 1
github.com/IBM/fp-go/v2/option/option.go:227.68,229.2 1 1
github.com/IBM/fp-go/v2/option/option.go:241.54,243.2 1 1
github.com/IBM/fp-go/v2/option/option.go:251.66,253.2 1 1
github.com/IBM/fp-go/v2/option/option.go:261.53,263.2 1 1
github.com/IBM/fp-go/v2/option/option.go:273.73,280.2 1 1
github.com/IBM/fp-go/v2/option/option.go:291.59,297.2 1 1
github.com/IBM/fp-go/v2/option/option.go:307.54,309.2 1 1
github.com/IBM/fp-go/v2/option/option.go:318.69,320.2 1 1
github.com/IBM/fp-go/v2/option/option.go:329.55,331.2 1 1
github.com/IBM/fp-go/v2/option/option.go:341.102,342.54 1 1
github.com/IBM/fp-go/v2/option/option.go:342.54,343.55 1 1
github.com/IBM/fp-go/v2/option/option.go:343.55,345.4 1 1
github.com/IBM/fp-go/v2/option/option.go:355.96,356.54 1 1
github.com/IBM/fp-go/v2/option/option.go:356.54,358.3 1 1
github.com/IBM/fp-go/v2/option/option.go:369.68,371.2 1 1
github.com/IBM/fp-go/v2/option/option.go:381.54,383.2 1 1
github.com/IBM/fp-go/v2/option/option.go:392.64,394.2 1 1
github.com/IBM/fp-go/v2/option/option.go:403.49,405.2 1 1
github.com/IBM/fp-go/v2/option/ord.go:38.50,46.2 2 1
github.com/IBM/fp-go/v2/option/ord.go:56.58,58.2 1 1
github.com/IBM/fp-go/v2/option/pair.go:33.88,39.2 1 1
github.com/IBM/fp-go/v2/option/pointed.go:24.46,26.2 1 1
github.com/IBM/fp-go/v2/option/pointed.go:35.53,37.2 1 1
github.com/IBM/fp-go/v2/option/record.go:35.105,43.2 1 1
github.com/IBM/fp-go/v2/option/record.go:56.88,58.2 1 1
github.com/IBM/fp-go/v2/option/record.go:71.121,79.2 1 1
github.com/IBM/fp-go/v2/option/record.go:92.104,94.2 1 1
github.com/IBM/fp-go/v2/option/record.go:105.97,107.2 1 1
github.com/IBM/fp-go/v2/option/record.go:118.78,120.2 1 1
github.com/IBM/fp-go/v2/option/record.go:122.74,125.2 2 1
github.com/IBM/fp-go/v2/option/record.go:135.85,137.69 2 1
github.com/IBM/fp-go/v2/option/record.go:137.69,139.3 1 1
github.com/IBM/fp-go/v2/option/record.go:148.68,150.2 1 1
github.com/IBM/fp-go/v2/option/sequence.go:39.28,41.2 1 0
github.com/IBM/fp-go/v2/option/sequence.go:63.44,66.52 3 0
github.com/IBM/fp-go/v2/option/sequence.go:66.52,68.3 1 0
github.com/IBM/fp-go/v2/option/type.go:22.37,25.2 2 1
github.com/IBM/fp-go/v2/option/type.go:37.39,42.2 1 1
github.com/IBM/fp-go/v2/option/type.go:51.38,53.2 1 1

View File

@@ -20,7 +20,6 @@ import (
"github.com/IBM/fp-go/v2/eq"
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"
P "github.com/IBM/fp-go/v2/predicate"
)
@@ -69,6 +68,8 @@ func FromEq[A any](pred eq.Eq[A]) func(A) Kleisli[A, A] {
// result := FromNillable(ptr) // None
// val := 42
// result := FromNillable(&val) // Some(&val)
//
//go:inline
func FromNillable[A any](a *A) Option[*A] {
return fromPredicate(a, F.IsNonNil[A])
}
@@ -83,6 +84,8 @@ func FromNillable[A any](a *A) Option[*A] {
// return n, err == nil
// })
// result := parseNum("42") // Some(42)
//
//go:inline
func FromValidation[A, B any](f func(A) (B, bool)) Kleisli[A, B] {
return Optionize1(f)
}
@@ -97,9 +100,10 @@ func FromValidation[A, B any](f func(A) (B, bool)) Kleisli[A, B] {
// fa := Some(5)
// result := MonadAp(fab, fa) // Some(10)
func MonadAp[B, A any](fab Option[func(A) B], fa Option[A]) Option[B] {
return MonadFold(fab, None[B], func(ab func(A) B) Option[B] {
return MonadFold(fa, None[B], F.Flow2(ab, Some[B]))
})
if fab.isSome && fa.isSome {
return Some(fab.value(fa.value))
}
return None[B]()
}
// Ap is the curried applicative functor for Option.
@@ -112,7 +116,16 @@ func MonadAp[B, A any](fab Option[func(A) B], fa Option[A]) Option[B] {
// fab := Some(N.Mul(2))
// result := applyTo5(fab) // Some(10)
func Ap[B, A any](fa Option[A]) Operator[func(A) B, B] {
return F.Bind2nd(MonadAp[B, A], fa)
if fa.isSome {
return func(fab Option[func(A) B]) Option[B] {
if fab.isSome {
return Some(fab.value(fa.value))
}
return None[B]()
}
}
// shortcut
return F.Constant1[Option[func(A) B]](None[B]())
}
// MonadMap applies a function to the value inside an Option.
@@ -123,7 +136,10 @@ func Ap[B, A any](fa Option[A]) Operator[func(A) B, B] {
// fa := Some(5)
// result := MonadMap(fa, N.Mul(2)) // Some(10)
func MonadMap[A, B any](fa Option[A], f func(A) B) Option[B] {
return MonadChain(fa, F.Flow2(f, Some[B]))
if fa.isSome {
return Some(f(fa.value))
}
return None[B]()
}
// Map returns a function that applies a transformation to the value inside an Option.
@@ -135,7 +151,12 @@ func MonadMap[A, B any](fa Option[A], f func(A) B) Option[B] {
// result := double(Some(5)) // Some(10)
// result := double(None[int]()) // None
func Map[A, B any](f func(a A) B) Operator[A, B] {
return Chain(F.Flow2(f, Some[B]))
return func(fa Option[A]) Option[B] {
if fa.isSome {
return Some(f(fa.value))
}
return None[B]()
}
}
// MonadMapTo replaces the value inside an Option with a constant value.
@@ -146,7 +167,10 @@ func Map[A, B any](f func(a A) B) Operator[A, B] {
// fa := Some(5)
// result := MonadMapTo(fa, "hello") // Some("hello")
func MonadMapTo[A, B any](fa Option[A], b B) Option[B] {
return MonadMap(fa, F.Constant1[A](b))
if fa.isSome {
return Some(b)
}
return None[B]()
}
// MapTo returns a function that replaces the value inside an Option with a constant.
@@ -156,7 +180,12 @@ func MonadMapTo[A, B any](fa Option[A], b B) Option[B] {
// replaceWith42 := MapTo[string, int](42)
// result := replaceWith42(Some("hello")) // Some(42)
func MapTo[A, B any](b B) Operator[A, B] {
return F.Bind2nd(MonadMapTo[A, B], b)
return func(fa Option[A]) Option[B] {
if fa.isSome {
return Some(b)
}
return None[B]()
}
}
// TryCatch executes a function that may return an error and converts the result to an Option.
@@ -186,9 +215,11 @@ func TryCatch[A any](f func() (A, error)) Option[A] {
// )
// result := handler(Some(42)) // "value: 42"
// result := handler(None[int]()) // "no value"
//
//go:inline
func Fold[A, B any](onNone func() B, onSome func(a A) B) func(ma Option[A]) B {
return func(ma Option[A]) B {
return MonadFold(ma, onNone, onSome)
return func(fa Option[A]) B {
return MonadFold(fa, onNone, onSome)
}
}
@@ -199,6 +230,8 @@ func Fold[A, B any](onNone func() B, onSome func(a A) B) func(ma Option[A]) B {
//
// result := MonadGetOrElse(Some(42), func() int { return 0 }) // 42
// result := MonadGetOrElse(None[int](), func() int { return 0 }) // 0
//
//go:inline
func MonadGetOrElse[A any](fa Option[A], onNone func() A) A {
return MonadFold(fa, onNone, F.Identity[A])
}
@@ -210,6 +243,8 @@ func MonadGetOrElse[A any](fa Option[A], onNone func() A) A {
// getOrZero := GetOrElse(func() int { return 0 })
// result := getOrZero(Some(42)) // 42
// result := getOrZero(None[int]()) // 0
//
//go:inline
func GetOrElse[A any](onNone func() A) func(Option[A]) A {
return Fold(onNone, F.Identity[A])
}
@@ -224,6 +259,8 @@ func GetOrElse[A any](onNone func() A) func(Option[A]) A {
// if x > 0 { return Some(x * 2) }
// return None[int]()
// }) // Some(10)
//
//go:inline
func MonadChain[A, B any](fa Option[A], f Kleisli[A, B]) Option[B] {
return MonadFold(fa, None[B], f)
}
@@ -238,6 +275,8 @@ func MonadChain[A, B any](fa Option[A], f Kleisli[A, B]) Option[B] {
// return None[int]()
// })
// result := validate(Some(5)) // Some(10)
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return Fold(None[B], f)
}
@@ -248,8 +287,11 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
// Example:
//
// result := MonadChainTo(Some(5), Some("hello")) // Some("hello")
func MonadChainTo[A, B any](_ Option[A], mb Option[B]) Option[B] {
return mb
func MonadChainTo[A, B any](ma Option[A], mb Option[B]) Option[B] {
if ma.isSome {
return mb
}
return None[B]()
}
// ChainTo returns a function that ignores its input Option and returns a fixed Option.
@@ -259,7 +301,15 @@ func MonadChainTo[A, B any](_ Option[A], mb Option[B]) Option[B] {
// replaceWith := ChainTo(Some("hello"))
// result := replaceWith(Some(42)) // Some("hello")
func ChainTo[A, B any](mb Option[B]) Operator[A, B] {
return F.Bind2nd(MonadChainTo[A, B], mb)
if mb.isSome {
return func(ma Option[A]) Option[B] {
if ma.isSome {
return mb
}
return None[B]()
}
}
return F.Constant1[Option[A]](None[B]())
}
// MonadChainFirst applies a function that returns an Option but keeps the original value.
@@ -339,11 +389,10 @@ func Alt[A any](that func() Option[A]) Operator[A, A] {
// return Some(a + b)
// }) // Some(5)
func MonadSequence2[T1, T2, R any](o1 Option[T1], o2 Option[T2], f func(T1, T2) Option[R]) Option[R] {
return MonadFold(o1, None[R], func(t1 T1) Option[R] {
return MonadFold(o2, None[R], func(t2 T2) Option[R] {
return f(t1, t2)
})
})
if o1.isSome && o2.isSome {
return f(o1.value, o2.value)
}
return None[R]()
}
// Sequence2 returns a function that sequences two Options with a combining function.
@@ -367,7 +416,12 @@ func Sequence2[T1, T2, R any](f func(T1, T2) Option[R]) func(Option[T1], Option[
// result := sum(Some(5)) // 5
// result := sum(None[int]()) // 0
func Reduce[A, B any](f func(B, A) B, initial B) func(Option[A]) B {
return Fold(F.Constant(initial), F.Bind1st(f, initial))
return func(ma Option[A]) B {
if ma.isSome {
return f(initial, ma.value)
}
return initial
}
}
// Filter keeps the Option if it's Some and the predicate is satisfied, otherwise returns None.
@@ -379,7 +433,12 @@ func Reduce[A, B any](f func(B, A) B, initial B) func(Option[A]) B {
// result := isPositive(Some(-1)) // None
// result := isPositive(None[int]()) // None
func Filter[A any](pred func(A) bool) Operator[A, A] {
return Fold(None[A], F.Ternary(pred, Of[A], F.Ignore1of1[A](None[A])))
return func(ma Option[A]) Option[A] {
if ma.isSome && pred(ma.value) {
return ma
}
return None[A]()
}
}
// MonadFlap applies a value to a function wrapped in an Option.
@@ -390,7 +449,10 @@ func Filter[A any](pred func(A) bool) Operator[A, A] {
// fab := Some(N.Mul(2))
// result := MonadFlap(fab, 5) // Some(10)
func MonadFlap[B, A any](fab Option[func(A) B], a A) Option[B] {
return FC.MonadFlap(MonadMap[func(A) B, B], fab, a)
if fab.isSome {
return Some(fab.value(a))
}
return None[B]()
}
// Flap returns a function that applies a value to an Option-wrapped function.
@@ -401,5 +463,10 @@ func MonadFlap[B, A any](fab Option[func(A) B], a A) Option[B] {
// fab := Some(N.Mul(2))
// result := applyFive(fab) // Some(10)
func Flap[B, A any](a A) Operator[func(A) B, B] {
return FC.Flap(Map[func(A) B, B], a)
return func(fab Option[func(A) B]) Option[B] {
if fab.isSome {
return Some(fab.value(a))
}
return None[B]()
}
}

View File

@@ -122,14 +122,14 @@ func TestMonadChain(t *testing.T) {
func TestMonadChainTo(t *testing.T) {
assert.Equal(t, Some("hello"), MonadChainTo(Some(42), Some("hello")))
assert.Equal(t, None[string](), MonadChainTo(Some(42), None[string]()))
assert.Equal(t, Some("hello"), MonadChainTo(None[int](), Some("hello")))
assert.Equal(t, None[string](), MonadChainTo(None[int](), Some("hello")))
}
// Test ChainTo
func TestChainTo(t *testing.T) {
replaceWith := ChainTo[int](Some("hello"))
assert.Equal(t, Some("hello"), replaceWith(Some(42)))
assert.Equal(t, Some("hello"), replaceWith(None[int]()))
assert.Equal(t, None[string](), replaceWith(None[int]()))
}
// Test MonadChainFirst

View File

@@ -33,12 +33,12 @@ type (
}
pairApplicativeHead[A, B, A1 any] struct {
s semigroup.Semigroup[B]
s Semigroup[B]
m monoid.Monoid[B]
}
pairMonadHead[A, B, A1 any] struct {
s semigroup.Semigroup[B]
s Semigroup[B]
m monoid.Monoid[B]
}

View File

@@ -19,37 +19,22 @@ import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/semigroup"
"github.com/IBM/fp-go/v2/tuple"
)
// String prints some debug info for the object
//
//go:noinline
func pairString(s *pair) string {
return fmt.Sprintf("Pair[%T, %T](%v, %v)", s.h, s.t, s.h, s.t)
}
// Format prints some debug info for the object
//
//go:noinline
func pairFormat(e *pair, f fmt.State, c rune) {
switch c {
case 's':
fmt.Fprint(f, pairString(e))
default:
fmt.Fprint(f, pairString(e))
}
}
// String prints some debug info for the object
func (s Pair[A, B]) String() string {
return pairString((*pair)(&s))
return fmt.Sprintf("Pair[%T, %T](%v, %v)", s.l, s.r, s.l, s.r)
}
// Format prints some debug info for the object
func (s Pair[A, B]) Format(f fmt.State, c rune) {
pairFormat((*pair)(&s), f, c)
switch c {
case 's':
fmt.Fprint(f, s.String())
default:
fmt.Fprint(f, s.String())
}
}
// Of creates a [Pair] with the same value in both the head and tail positions.
@@ -58,7 +43,7 @@ func (s Pair[A, B]) Format(f fmt.State, c rune) {
//
// p := pair.Of(42) // Pair[int, int]{42, 42}
func Of[A any](value A) Pair[A, A] {
return Pair[A, A]{h: value, t: value}
return Pair[A, A]{value, value}
}
// FromTuple creates a [Pair] from a [tuple.Tuple2].
@@ -68,8 +53,10 @@ func Of[A any](value A) Pair[A, A] {
//
// t := tuple.MakeTuple2("hello", 42)
// p := pair.FromTuple(t) // Pair[string, int]{"hello", 42}
//
//go:inline
func FromTuple[A, B any](t tuple.Tuple2[A, B]) Pair[A, B] {
return Pair[A, B]{h: t.F1, t: t.F2}
return Pair[A, B]{t.F2, t.F1}
}
// ToTuple creates a [tuple.Tuple2] from a [Pair].
@@ -79,6 +66,8 @@ func FromTuple[A, B any](t tuple.Tuple2[A, B]) Pair[A, B] {
//
// p := pair.MakePair("hello", 42)
// t := pair.ToTuple(p) // tuple.Tuple2[string, int]{"hello", 42}
//
//go:inline
func ToTuple[A, B any](t Pair[A, B]) tuple.Tuple2[A, B] {
return tuple.MakeTuple2(Head(t), Tail(t))
}
@@ -89,8 +78,10 @@ func ToTuple[A, B any](t Pair[A, B]) tuple.Tuple2[A, B] {
// Example:
//
// p := pair.MakePair("hello", 42) // Pair[string, int]{"hello", 42}
//
//go:inline
func MakePair[A, B any](a A, b B) Pair[A, B] {
return Pair[A, B]{h: a, t: b}
return Pair[A, B]{b, a}
}
// Head returns the head (first) value of the pair.
@@ -99,8 +90,10 @@ func MakePair[A, B any](a A, b B) Pair[A, B] {
//
// p := pair.MakePair("hello", 42)
// h := pair.Head(p) // "hello"
//
//go:inline
func Head[A, B any](fa Pair[A, B]) A {
return fa.h.(A)
return fa.l
}
// Tail returns the tail (second) value of the pair.
@@ -109,8 +102,10 @@ func Head[A, B any](fa Pair[A, B]) A {
//
// p := pair.MakePair("hello", 42)
// t := pair.Tail(p) // 42
//
//go:inline
func Tail[A, B any](fa Pair[A, B]) B {
return fa.t.(B)
return fa.r
}
// First returns the first value of the pair (alias for Head).
@@ -119,8 +114,10 @@ func Tail[A, B any](fa Pair[A, B]) B {
//
// p := pair.MakePair("hello", 42)
// f := pair.First(p) // "hello"
//
//go:inline
func First[A, B any](fa Pair[A, B]) A {
return fa.h.(A)
return fa.l
}
// Second returns the second value of the pair (alias for Tail).
@@ -129,8 +126,10 @@ func First[A, B any](fa Pair[A, B]) A {
//
// p := pair.MakePair("hello", 42)
// s := pair.Second(p) // 42
//
//go:inline
func Second[A, B any](fa Pair[A, B]) B {
return fa.t.(B)
return fa.r
}
// MonadMapHead maps a function over the head value of the pair, leaving the tail unchanged.
@@ -141,20 +140,15 @@ func Second[A, B any](fa Pair[A, B]) B {
// p2 := pair.MonadMapHead(p, func(n int) string {
// return fmt.Sprintf("%d", n)
// }) // Pair[string, string]{"5", "hello"}
//
//go:inline
func MonadMapHead[B, A, A1 any](fa Pair[A, B], f func(A) A1) Pair[A1, B] {
return Pair[A1, B]{f(Head(fa)), fa.t}
return MakePair(f(Head(fa)), fa.r)
}
// MonadMap maps a function over the head value of the pair (alias for MonadMapHead).
//
// Example:
//
// p := pair.MakePair(5, "hello")
// p2 := pair.MonadMap(p, func(n int) string {
// return fmt.Sprintf("%d", n)
// }) // Pair[string, string]{"5", "hello"}
func MonadMap[B, A, A1 any](fa Pair[A, B], f func(A) A1) Pair[A1, B] {
return MonadMapHead(fa, f)
//go:inline
func MonadMap[A, B, B1 any](fa Pair[A, B], f func(B) B1) Pair[A, B1] {
return MonadMapTail(fa, f)
}
// MonadMapTail maps a function over the tail value of the pair, leaving the head unchanged.
@@ -165,8 +159,10 @@ func MonadMap[B, A, A1 any](fa Pair[A, B], f func(A) A1) Pair[A1, B] {
// p2 := pair.MonadMapTail(p, func(s string) int {
// return len(s)
// }) // Pair[int, int]{5, 5}
//
//go:inline
func MonadMapTail[A, B, B1 any](fa Pair[A, B], f func(B) B1) Pair[A, B1] {
return Pair[A, B1]{fa.h, f(Tail(fa))}
return MakePair(fa.l, f(Tail(fa)))
}
// MonadBiMap maps functions over both the head and tail values of the pair.
@@ -178,8 +174,10 @@ func MonadMapTail[A, B, B1 any](fa Pair[A, B], f func(B) B1) Pair[A, B1] {
// func(n int) string { return fmt.Sprintf("%d", n) },
// func(s string) int { return len(s) },
// ) // Pair[string, int]{"5", 5}
//
//go:inline
func MonadBiMap[A, B, A1, B1 any](fa Pair[A, B], f func(A) A1, g func(B) B1) Pair[A1, B1] {
return Pair[A1, B1]{f(Head(fa)), g(Tail(fa))}
return MakePair(f(Head(fa)), g(Tail(fa)))
}
// Map returns a function that maps over the tail value of a pair (alias for MapTail).
@@ -190,7 +188,9 @@ func MonadBiMap[A, B, A1, B1 any](fa Pair[A, B], f func(A) A1, g func(B) B1) Pai
// mapper := pair.Map[int](func(s string) int { return len(s) })
// p := pair.MakePair(5, "hello")
// p2 := mapper(p) // Pair[int, int]{5, 5}
func Map[A, B, B1 any](f func(B) B1) func(Pair[A, B]) Pair[A, B1] {
//
//go:inline
func Map[A, B, B1 any](f func(B) B1) Operator[A, B, B1] {
return MapTail[A](f)
}
@@ -204,6 +204,8 @@ func Map[A, B, B1 any](f func(B) B1) func(Pair[A, B]) Pair[A, B1] {
// })
// p := pair.MakePair(5, "hello")
// p2 := mapper(p) // Pair[string, string]{"5", "hello"}
//
//go:inline
func MapHead[B, A, A1 any](f func(A) A1) func(Pair[A, B]) Pair[A1, B] {
return F.Bind2nd(MonadMapHead[B, A, A1], f)
}
@@ -216,7 +218,9 @@ func MapHead[B, A, A1 any](f func(A) A1) func(Pair[A, B]) Pair[A1, B] {
// mapper := pair.MapTail[int](func(s string) int { return len(s) })
// p := pair.MakePair(5, "hello")
// p2 := mapper(p) // Pair[int, int]{5, 5}
func MapTail[A, B, B1 any](f func(B) B1) func(Pair[A, B]) Pair[A, B1] {
//
//go:inline
func MapTail[A, B, B1 any](f func(B) B1) Operator[A, B, B1] {
return F.Bind2nd(MonadMapTail[A, B, B1], f)
}
@@ -231,6 +235,8 @@ func MapTail[A, B, B1 any](f func(B) B1) func(Pair[A, B]) Pair[A, B1] {
// )
// p := pair.MakePair(5, "hello")
// p2 := mapper(p) // Pair[string, int]{"5", 5}
//
//go:inline
func BiMap[A, B, A1, B1 any](f func(A) A1, g func(B) B1) func(Pair[A, B]) Pair[A1, B1] {
return func(fa Pair[A, B]) Pair[A1, B1] {
return MonadBiMap(fa, f, g)
@@ -250,9 +256,9 @@ func BiMap[A, B, A1, B1 any](f func(A) A1, g func(B) B1) func(Pair[A, B]) Pair[A
// p2 := pair.MonadChainHead(strConcat, p, func(n int) pair.Pair[string, string] {
// return pair.MakePair(fmt.Sprintf("%d", n), "!")
// }) // Pair[string, string]{"5", "hello!"}
func MonadChainHead[B, A, A1 any](sg semigroup.Semigroup[B], fa Pair[A, B], f func(A) Pair[A1, B]) Pair[A1, B] {
func MonadChainHead[B, A, A1 any](sg Semigroup[B], fa Pair[A, B], f func(A) Pair[A1, B]) Pair[A1, B] {
fb := f(Head(fa))
return Pair[A1, B]{fb.h, sg.Concat(Tail(fa), Tail(fb))}
return MakePair(Head(fb), sg.Concat(Tail(fa), Tail(fb)))
}
// MonadChainTail chains a function over the tail value, combining head values using a semigroup.
@@ -268,9 +274,11 @@ func MonadChainHead[B, A, A1 any](sg semigroup.Semigroup[B], fa Pair[A, B], f fu
// p2 := pair.MonadChainTail(intSum, p, func(s string) pair.Pair[int, int] {
// return pair.MakePair(len(s), len(s) * 2)
// }) // Pair[int, int]{10, 10}
func MonadChainTail[A, B, B1 any](sg semigroup.Semigroup[A], fb Pair[A, B], f func(B) Pair[A, B1]) Pair[A, B1] {
//
//go:inline
func MonadChainTail[A, B, B1 any](sg Semigroup[A], fb Pair[A, B], f Kleisli[A, B, B1]) Pair[A, B1] {
fa := f(Tail(fb))
return Pair[A, B1]{sg.Concat(Head(fb), Head(fa)), fa.t}
return MakePair(sg.Concat(Head(fb), Head(fa)), Tail(fa))
}
// MonadChain chains a function over the tail value (alias for MonadChainTail).
@@ -284,7 +292,9 @@ func MonadChainTail[A, B, B1 any](sg semigroup.Semigroup[A], fb Pair[A, B], f fu
// p2 := pair.MonadChain(intSum, p, func(s string) pair.Pair[int, int] {
// return pair.MakePair(len(s), len(s) * 2)
// }) // Pair[int, int]{10, 10}
func MonadChain[A, B, B1 any](sg semigroup.Semigroup[A], fa Pair[A, B], f func(B) Pair[A, B1]) Pair[A, B1] {
//
//go:inline
func MonadChain[A, B, B1 any](sg Semigroup[A], fa Pair[A, B], f Kleisli[A, B, B1]) Pair[A, B1] {
return MonadChainTail(sg, fa, f)
}
@@ -301,7 +311,9 @@ func MonadChain[A, B, B1 any](sg semigroup.Semigroup[A], fa Pair[A, B], f func(B
// })
// p := pair.MakePair(5, "hello")
// p2 := chain(p) // Pair[string, string]{"5", "hello!"}
func ChainHead[B, A, A1 any](sg semigroup.Semigroup[B], f func(A) Pair[A1, B]) func(Pair[A, B]) Pair[A1, B] {
//
//go:inline
func ChainHead[B, A, A1 any](sg Semigroup[B], f func(A) Pair[A1, B]) func(Pair[A, B]) Pair[A1, B] {
return func(fa Pair[A, B]) Pair[A1, B] {
return MonadChainHead(sg, fa, f)
}
@@ -320,7 +332,9 @@ func ChainHead[B, A, A1 any](sg semigroup.Semigroup[B], f func(A) Pair[A1, B]) f
// })
// p := pair.MakePair(5, "hello")
// p2 := chain(p) // Pair[int, int]{10, 10}
func ChainTail[A, B, B1 any](sg semigroup.Semigroup[A], f func(B) Pair[A, B1]) func(Pair[A, B]) Pair[A, B1] {
//
//go:inline
func ChainTail[A, B, B1 any](sg Semigroup[A], f Kleisli[A, B, B1]) Operator[A, B, B1] {
return func(fa Pair[A, B]) Pair[A, B1] {
return MonadChainTail(sg, fa, f)
}
@@ -338,7 +352,9 @@ func ChainTail[A, B, B1 any](sg semigroup.Semigroup[A], f func(B) Pair[A, B1]) f
// })
// p := pair.MakePair(5, "hello")
// p2 := chain(p) // Pair[int, int]{10, 10}
func Chain[A, B, B1 any](sg semigroup.Semigroup[A], f func(B) Pair[A, B1]) func(Pair[A, B]) Pair[A, B1] {
//
//go:inline
func Chain[A, B, B1 any](sg Semigroup[A], f Kleisli[A, B, B1]) Operator[A, B, B1] {
return ChainTail(sg, f)
}
@@ -353,8 +369,10 @@ func Chain[A, B, B1 any](sg semigroup.Semigroup[A], f func(B) Pair[A, B1]) func(
// pf := pair.MakePair(func(n int) string { return fmt.Sprintf("%d", n) }, "!")
// pv := pair.MakePair(42, "hello")
// result := pair.MonadApHead(strConcat, pf, pv) // Pair[string, string]{"42", "!hello"}
func MonadApHead[B, A, A1 any](sg semigroup.Semigroup[B], faa Pair[func(A) A1, B], fa Pair[A, B]) Pair[A1, B] {
return Pair[A1, B]{Head(faa)(Head(fa)), sg.Concat(Tail(fa), Tail(faa))}
//
//go:inline
func MonadApHead[B, A, A1 any](sg Semigroup[B], faa Pair[func(A) A1, B], fa Pair[A, B]) Pair[A1, B] {
return MakePair(Head(faa)(Head(fa)), sg.Concat(Tail(fa), Tail(faa)))
}
// MonadApTail applies a function wrapped in a pair to a value wrapped in a pair,
@@ -368,8 +386,10 @@ func MonadApHead[B, A, A1 any](sg semigroup.Semigroup[B], faa Pair[func(A) A1, B
// pf := pair.MakePair(10, func(s string) int { return len(s) })
// pv := pair.MakePair(5, "hello")
// result := pair.MonadApTail(intSum, pf, pv) // Pair[int, int]{15, 5}
func MonadApTail[A, B, B1 any](sg semigroup.Semigroup[A], fbb Pair[A, func(B) B1], fb Pair[A, B]) Pair[A, B1] {
return Pair[A, B1]{sg.Concat(Head(fb), Head(fbb)), Tail(fbb)(Tail(fb))}
//
//go:inline
func MonadApTail[A, B, B1 any](sg Semigroup[A], fbb Pair[A, func(B) B1], fb Pair[A, B]) Pair[A, B1] {
return MakePair(sg.Concat(Head(fb), Head(fbb)), Tail(fbb)(Tail(fb)))
}
// MonadAp applies a function wrapped in a pair to a value wrapped in a pair,
@@ -383,7 +403,9 @@ func MonadApTail[A, B, B1 any](sg semigroup.Semigroup[A], fbb Pair[A, func(B) B1
// pf := pair.MakePair(10, func(s string) int { return len(s) })
// pv := pair.MakePair(5, "hello")
// result := pair.MonadAp(intSum, pf, pv) // Pair[int, int]{15, 5}
func MonadAp[A, B, B1 any](sg semigroup.Semigroup[A], faa Pair[A, func(B) B1], fa Pair[A, B]) Pair[A, B1] {
//
//go:inline
func MonadAp[A, B, B1 any](sg Semigroup[A], faa Pair[A, func(B) B1], fa Pair[A, B]) Pair[A, B1] {
return MonadApTail(sg, faa, fa)
}
@@ -399,7 +421,7 @@ func MonadAp[A, B, B1 any](sg semigroup.Semigroup[A], faa Pair[A, func(B) B1], f
// ap := pair.ApHead(strConcat, pv)
// pf := pair.MakePair(func(n int) string { return fmt.Sprintf("%d", n) }, "!")
// result := ap(pf) // Pair[string, string]{"42", "!hello"}
func ApHead[B, A, A1 any](sg semigroup.Semigroup[B], fa Pair[A, B]) func(Pair[func(A) A1, B]) Pair[A1, B] {
func ApHead[B, A, A1 any](sg Semigroup[B], fa Pair[A, B]) func(Pair[func(A) A1, B]) Pair[A1, B] {
return func(faa Pair[func(A) A1, B]) Pair[A1, B] {
return MonadApHead(sg, faa, fa)
}
@@ -417,7 +439,7 @@ func ApHead[B, A, A1 any](sg semigroup.Semigroup[B], fa Pair[A, B]) func(Pair[fu
// ap := pair.ApTail(intSum, pv)
// pf := pair.MakePair(10, func(s string) int { return len(s) })
// result := ap(pf) // Pair[int, int]{15, 5}
func ApTail[A, B, B1 any](sg semigroup.Semigroup[A], fb Pair[A, B]) func(Pair[A, func(B) B1]) Pair[A, B1] {
func ApTail[A, B, B1 any](sg Semigroup[A], fb Pair[A, B]) func(Pair[A, func(B) B1]) Pair[A, B1] {
return func(fbb Pair[A, func(B) B1]) Pair[A, B1] {
return MonadApTail(sg, fbb, fb)
}
@@ -435,7 +457,9 @@ func ApTail[A, B, B1 any](sg semigroup.Semigroup[A], fb Pair[A, B]) func(Pair[A,
// ap := pair.Ap(intSum, pv)
// pf := pair.MakePair(10, func(s string) int { return len(s) })
// result := ap(pf) // Pair[int, int]{15, 5}
func Ap[A, B, B1 any](sg semigroup.Semigroup[A], fa Pair[A, B]) func(Pair[A, func(B) B1]) Pair[A, B1] {
//
//go:inline
func Ap[A, B, B1 any](sg Semigroup[A], fa Pair[A, B]) func(Pair[A, func(B) B1]) Pair[A, B1] {
return ApTail[A, B, B1](sg, fa)
}
@@ -445,6 +469,8 @@ func Ap[A, B, B1 any](sg semigroup.Semigroup[A], fa Pair[A, B]) func(Pair[A, fun
//
// p := pair.MakePair("hello", 42)
// swapped := pair.Swap(p) // Pair[int, string]{42, "hello"}
//
//go:inline
func Swap[A, B any](fa Pair[A, B]) Pair[B, A] {
return MakePair(Tail(fa), Head(fa))
}

View File

@@ -81,13 +81,6 @@ func TestMonadMapTail(t *testing.T) {
assert.Equal(t, 5, Tail(p2))
}
func TestMonadMap(t *testing.T) {
p := MakePair(10, "test")
p2 := MonadMap(p, S.Format[int]("value: %d"))
assert.Equal(t, "value: 10", Head(p2))
assert.Equal(t, "test", Tail(p2))
}
func TestMonadBiMap(t *testing.T) {
p := MakePair(5, "hello")
p2 := MonadBiMap(p,

View File

@@ -15,11 +15,17 @@
package pair
import "github.com/IBM/fp-go/v2/semigroup"
type (
pair struct {
h, t any
}
Semigroup[A any] = semigroup.Semigroup[A]
// Pair defines a data structure that holds two strongly typed values
Pair[L, R any] pair
Pair[L, R any] struct {
r R
l L
}
Kleisli[L, R1, R2 any] = func(R1) Pair[L, R2]
Operator[L, R1, R2 any] = func(Pair[L, R1]) Pair[L, R2]
)

View File

@@ -0,0 +1,222 @@
// 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 result
import (
"errors"
"testing"
N "github.com/IBM/fp-go/v2/number"
)
var (
errBench = errors.New("benchmark error")
benchResultInt Result[int]
benchBool bool
benchInt int
)
// Benchmark core constructors
func BenchmarkLeft(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchResultInt = Left[int](errBench)
}
}
func BenchmarkRight(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchResultInt = Right(42)
}
}
func BenchmarkOf(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
benchResultInt = Of(42)
}
}
// Benchmark predicates
func BenchmarkIsLeft(b *testing.B) {
val := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchBool = IsLeft(val)
}
}
func BenchmarkIsRight(b *testing.B) {
val := Right(42)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchBool = IsRight(val)
}
}
// Benchmark fold operations
func BenchmarkFold_Right(b *testing.B) {
val := Right(42)
folder := Fold(
func(e error) int { return 0 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = folder(val)
}
}
func BenchmarkFold_Left(b *testing.B) {
val := Left[int](errBench)
folder := Fold(
func(e error) int { return 0 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = folder(val)
}
}
// Benchmark functor operations
func BenchmarkMap_Right(b *testing.B) {
val := Right(42)
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = mapper(val)
}
}
func BenchmarkMap_Left(b *testing.B) {
val := Left[int](errBench)
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = mapper(val)
}
}
// Benchmark monad operations
func BenchmarkChain_Right(b *testing.B) {
val := Right(42)
chainer := Chain(func(a int) Result[int] { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = chainer(val)
}
}
func BenchmarkChain_Left(b *testing.B) {
val := Left[int](errBench)
chainer := Chain(func(a int) Result[int] { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = chainer(val)
}
}
func BenchmarkChainFirst_Right(b *testing.B) {
val := Right(42)
chainer := ChainFirst(func(a int) Result[string] { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = chainer(val)
}
}
func BenchmarkChainFirst_Left(b *testing.B) {
val := Left[int](errBench)
chainer := ChainFirst(func(a int) Result[string] { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = chainer(val)
}
}
// Benchmark alternative operations
func BenchmarkAlt_RightRight(b *testing.B) {
val := Right(42)
alternative := Alt(func() Result[int] { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = alternative(val)
}
}
func BenchmarkAlt_LeftRight(b *testing.B) {
val := Left[int](errBench)
alternative := Alt(func() Result[int] { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = alternative(val)
}
}
func BenchmarkOrElse_Right(b *testing.B) {
val := Right(42)
recover := OrElse(func(e error) Result[int] { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = recover(val)
}
}
func BenchmarkOrElse_Left(b *testing.B) {
val := Left[int](errBench)
recover := OrElse(func(e error) Result[int] { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchResultInt = recover(val)
}
}
// Benchmark GetOrElse
func BenchmarkGetOrElse_Right(b *testing.B) {
val := Right(42)
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = getter(val)
}
}
func BenchmarkGetOrElse_Left(b *testing.B) {
val := Left[int](errBench)
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
benchInt = getter(val)
}
}

View File

@@ -46,7 +46,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)

107
v2/state/bind.go Normal file
View File

@@ -0,0 +1,107 @@
package state
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"
)
//go:inline
func Do[ST, A any](
empty A,
) State[ST, A] {
return Of[ST](empty)
}
//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, Kleisli[ST, S1, S2], S1, S2],
Map[ST, func(T) S2, T, S2],
setter,
f,
)
}
//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, func(S1) S2, S1, S2],
key,
f,
)
}
//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, func(S1) S2, S1, S2],
key,
b,
)
}
//go:inline
func BindTo[ST, S1, T any](
setter func(T) S1,
) Operator[ST, T, S1] {
return C.BindTo(
Map[ST, func(T) S1, T, S1],
setter,
)
}
//go:inline
func ApS[ST, S1, S2, T any](
setter func(T) func(S1) S2,
fa State[ST, T],
) Operator[ST, S1, S2] {
return A.ApS(
Ap[S2, ST, T],
Map[ST, func(S1) func(T) S2, S1, func(T) S2],
setter,
fa,
)
}
//go:inline
func ApSL[ST, S, T any](
lens Lens[S, T],
fa State[ST, T],
) Endomorphism[State[ST, S]] {
return ApS(lens.Set, fa)
}
//go:inline
func BindL[ST, S, T any](
lens Lens[S, T],
f Kleisli[ST, T, T],
) Endomorphism[State[ST, S]] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
//go:inline
func LetL[ST, S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Endomorphism[State[ST, S]] {
return Let[ST](lens.Set, function.Flow2(lens.Get, f))
}
//go:inline
func LetToL[ST, S, T any](
lens Lens[S, T],
b T,
) Endomorphism[State[ST, S]] {
return LetTo[ST](lens.Set, b)
}

View File

@@ -42,19 +42,19 @@ func (o *stateMonad[S, A, B]) Of(a A) State[S, A] {
return Of[S](a)
}
func (o *stateFunctor[S, A, B]) Map(f func(A) B) func(State[S, A]) State[S, B] {
func (o *stateFunctor[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateApplicative[S, A, B]) Map(f func(A) B) func(State[S, A]) State[S, B] {
func (o *stateApplicative[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateMonad[S, A, B]) Map(f func(A) B) func(State[S, A]) State[S, B] {
func (o *stateMonad[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateMonad[S, A, B]) Chain(f func(A) State[S, B]) func(State[S, A]) State[S, B] {
func (o *stateMonad[S, A, B]) Chain(f Kleisli[S, A, B]) Operator[S, A, B] {
return Chain(f)
}

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