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

Compare commits

...

41 Commits

Author SHA1 Message Date
Carsten Leue
f652a94c3a fix: add template based logger
Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
2025-11-28 10:11:08 +01:00
Dr. Carsten Leue
774db88ca5 fix: add name to prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-27 13:26:36 +01:00
Dr. Carsten Leue
62a3365b20 fix: add conversion prisms for numbers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-27 13:12:18 +01:00
Dr. Carsten Leue
d9a16a6771 fix: add reduce operations to readerioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 17:00:10 +01:00
Dr. Carsten Leue
8949cc7dca fix: expose stats
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 13:44:40 +01:00
Dr. Carsten Leue
fa6b6caf22 fix: generic order for reader.Flap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:53:13 +01:00
Dr. Carsten Leue
a1e8d397c3 fix: better doc and some helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:06:09 +01:00
Dr. Carsten Leue
dbe7102e43 fix: better doc and some helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:05:31 +01:00
Dr. Carsten Leue
09aeb996e2 fix: add GetOrElseOf
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:57:30 +01:00
Dr. Carsten Leue
7cd575d95a fix: improve Prism and Optional
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:22:52 +01:00
Dr. Carsten Leue
dcfb023891 fix: improve assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 17:28:48 +01:00
Dr. Carsten Leue
51cf241a26 fix: add ReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 12:29:55 +01:00
Dr. Carsten Leue
9004c93976 fix: add some idomatic helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 10:40:58 +01:00
Dr. Carsten Leue
d8ab6b0ce5 fix: ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-22 10:39:56 +01:00
Dr. Carsten Leue
4e9998b645 fix: benchmarks and better docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 15:39:41 +01:00
Dr. Carsten Leue
2ea9e292e1 fix: idiomatic/readeresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 15:25:59 +01:00
Dr. Carsten Leue
12a20e30d1 fix: implement BindReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 13:01:27 +01:00
Dr. Carsten Leue
4909ad5473 fix: add missing monoid
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 10:22:50 +01:00
Dr. Carsten Leue
d116317cde fix: add readerresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 10:04:28 +01:00
Dr. Carsten Leue
1428241f2c fix: race condition
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-21 08:36:07 +01:00
Dr. Carsten Leue
ef9216bad7 fix: documentation, tests, some utilities
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-20 08:43:15 +01:00
Dr. Carsten Leue
fe77c770b6 fix: cleanup types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-19 17:36:49 +01:00
Dr. Carsten Leue
1c42b2ac1d fix: implement idiomatic/ioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-19 15:39:02 +01:00
Dr. Carsten Leue
cbd93fdecc fix: add statereaderioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 17:54:04 +01:00
Dr. Carsten Leue
6d94697128 fix: document statereaderioeither
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 16:06:56 +01:00
Dr. Carsten Leue
77dde302ef Merge branch 'main' of github.com:IBM/fp-go 2025-11-18 10:59:57 +01:00
Dr. Carsten Leue
909d626019 fix: serveral performance improvements
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-18 10:58:24 +01:00
renovate[bot]
b01a8f2aff chore(deps): update actions/checkout action to v4.3.1 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-18 06:31:59 +00:00
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
Dr. Carsten Leue
57794ccb34 fix: add idiomatic go options package
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-17 11:10:27 +01:00
Dr. Carsten Leue
404eb875d3 fix: add idiomatic version
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-16 17:27:16 +01:00
Dr. Carsten Leue
ed108812d6 fix: modernize codebase
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-15 17:00:22 +01:00
Dr. Carsten Leue
ab868315d4 fix: traverse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-15 12:13:37 +01:00
Dr. Carsten Leue
02d0be9dad fix: add traversal for sequences
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-14 14:12:44 +01:00
Dr. Carsten Leue
2c1d8196b4 fix: support go iterators and cleanup types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-14 12:56:12 +01:00
Dr. Carsten Leue
17eb8ae66f fix: add Chain...Left methods
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 16:51:15 +01:00
Dr. Carsten Leue
b70e481e7d fix: some minor improvements
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 12:56:51 +01:00
Dr. Carsten Leue
3c3bb7c166 fix: improve lens implementation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 12:15:52 +01:00
Dr. Carsten Leue
d3007cbbfa fix: improve lens generator
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:39:18 +01:00
Dr. Carsten Leue
5aa0e1ea2e fix: handle non comparable types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:35:56 +01:00
455 changed files with 52269 additions and 1580 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,17 @@
{
"permissions": {
"allow": [
"Bash(ls -la \"c:\\d\\fp-go\\v2\\internal\\monad\"\" && ls -la \"c:dfp-gov2internalapplicative\"\")",
"Bash(ls -la \"c:\\d\\fp-go\\v2\\internal\\chain\"\" && ls -la \"c:dfp-gov2internalfunctor\"\")",
"Bash(go build:*)",
"Bash(go test:*)",
"Bash(go doc:*)",
"Bash(go tool cover:*)",
"Bash(sort:*)",
"Bash(tee:*)",
"Bash(find:*)"
],
"deny": [],
"ask": []
}
}

482
v2/BENCHMARK_COMPARISON.md Normal file
View File

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

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(N.Mul(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(N.Mul(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

@@ -0,0 +1,174 @@
# Idiomatic ReadIOResult Functions - Implementation Plan
## Overview
This document outlines the idiomatic functions that should be added to the `readerioresult` package to support Go's native `(value, error)` pattern, similar to what was implemented for `readerresult`.
## Key Concepts
The idiomatic package `github.com/IBM/fp-go/v2/idiomatic/readerioresult` defines:
- `ReaderIOResult[R, A]` as `func(R) func() (A, error)` (idiomatic style)
- This contrasts with `readerioresult.ReaderIOResult[R, A]` which is `Reader[R, IOResult[A]]` (functional style)
## Functions to Add
### In `readerioresult/reader.go`
Add helper functions at the top:
```go
func fromReaderIOResultKleisliI[R, A, B any](f RIORI.Kleisli[R, A, B]) Kleisli[R, A, B] {
return function.Flow2(f, FromReaderIOResultI[R, B])
}
func fromIOResultKleisliI[A, B any](f IORI.Kleisli[A, B]) ioresult.Kleisli[A, B] {
return ioresult.Eitherize1(f)
}
```
### Core Conversion Functions
1. **FromResultI** - Lift `(value, error)` to ReaderIOResult
```go
func FromResultI[R, A any](a A, err error) ReaderIOResult[R, A]
```
2. **FromIOResultI** - Lift idiomatic IOResult to functional
```go
func FromIOResultI[R, A any](ioe func() (A, error)) ReaderIOResult[R, A]
```
3. **FromReaderIOResultI** - Convert idiomatic ReaderIOResult to functional
```go
func FromReaderIOResultI[R, A any](rr RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, A]
```
### Chain Functions
4. **MonadChainI** / **ChainI** - Chain with idiomatic Kleisli
```go
func MonadChainI[R, A, B any](ma ReaderIOResult[R, A], f RIORI.Kleisli[R, A, B]) ReaderIOResult[R, B]
func ChainI[R, A, B any](f RIORI.Kleisli[R, A, B]) Operator[R, A, B]
```
5. **MonadChainEitherIK** / **ChainEitherIK** - Chain with idiomatic Result functions
```go
func MonadChainEitherIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) (B, error)) ReaderIOResult[R, B]
func ChainEitherIK[R, A, B any](f func(A) (B, error)) Operator[R, A, B]
```
6. **MonadChainIOResultIK** / **ChainIOResultIK** - Chain with idiomatic IOResult
```go
func MonadChainIOResultIK[R, A, B any](ma ReaderIOResult[R, A], f func(A) func() (B, error)) ReaderIOResult[R, B]
func ChainIOResultIK[R, A, B any](f func(A) func() (B, error)) Operator[R, A, B]
```
### Applicative Functions
7. **MonadApI** / **ApI** - Apply with idiomatic value
```go
func MonadApI[B, R, A any](fab ReaderIOResult[R, func(A) B], fa RIORI.ReaderIOResult[R, A]) ReaderIOResult[R, B]
func ApI[B, R, A any](fa RIORI.ReaderIOResult[R, A]) Operator[R, func(A) B, B]
```
### Error Handling Functions
8. **OrElseI** - Fallback with idiomatic computation
```go
func OrElseI[R, A any](onLeft RIORI.Kleisli[R, error, A]) Operator[R, A, A]
```
9. **MonadAltI** / **AltI** - Alternative with idiomatic computation
```go
func MonadAltI[R, A any](first ReaderIOResult[R, A], second Lazy[RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
func AltI[R, A any](second Lazy[RIORI.ReaderIOResult[R, A]]) Operator[R, A, A]
```
### Flatten Functions
10. **FlattenI** - Flatten nested idiomatic ReaderIOResult
```go
func FlattenI[R, A any](mma ReaderIOResult[R, RIORI.ReaderIOResult[R, A]]) ReaderIOResult[R, A]
```
### In `readerioresult/bind.go`
11. **BindI** - Bind with idiomatic Kleisli
```go
func BindI[R, S1, S2, T any](setter func(T) func(S1) S2, f RIORI.Kleisli[R, S1, T]) Operator[R, S1, S2]
```
12. **ApIS** - Apply idiomatic value to state
```go
func ApIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa RIORI.ReaderIOResult[R, T]) Operator[R, S1, S2]
```
13. **ApISL** - Apply idiomatic value using lens
```go
func ApISL[R, S, T any](lens L.Lens[S, T], fa RIORI.ReaderIOResult[R, T]) Operator[R, S, S]
```
14. **BindIL** - Bind idiomatic with lens
```go
func BindIL[R, S, T any](lens L.Lens[S, T], f RIORI.Kleisli[R, T, T]) Operator[R, S, S]
```
15. **BindEitherIK** / **BindResultIK** - Bind idiomatic Result
```go
func BindEitherIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
func BindResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) (T, error)) Operator[R, S1, S2]
```
16. **BindIOResultIK** - Bind idiomatic IOResult
```go
func BindIOResultIK[R, S1, S2, T any](setter func(T) func(S1) S2, f func(S1) func() (T, error)) Operator[R, S1, S2]
```
17. **BindToEitherI** / **BindToResultI** - Initialize from idiomatic pair
```go
func BindToEitherI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
func BindToResultI[R, S1, T any](setter func(T) S1) func(T, error) ReaderIOResult[R, S1]
```
18. **BindToIOResultI** - Initialize from idiomatic IOResult
```go
func BindToIOResultI[R, S1, T any](setter func(T) S1) func(func() (T, error)) ReaderIOResult[R, S1]
```
19. **ApEitherIS** / **ApResultIS** - Apply idiomatic pair to state
```go
func ApEitherIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
func ApResultIS[R, S1, S2, T any](setter func(T) func(S1) S2) func(T, error) Operator[R, S1, S2]
```
20. **ApIOResultIS** - Apply idiomatic IOResult to state
```go
func ApIOResultIS[R, S1, S2, T any](setter func(T) func(S1) S2, fa func() (T, error)) Operator[R, S1, S2]
```
## Testing Strategy
Create `readerioresult/idiomatic_test.go` with:
- Tests for each idiomatic function
- Success and error cases
- Integration tests showing real-world usage patterns
- Parallel execution tests where applicable
- Complex scenarios combining multiple idiomatic functions
## Implementation Priority
1. **High Priority** - Core conversion and chain functions (1-6)
2. **Medium Priority** - Bind functions for do-notation (11-16)
3. **Low Priority** - Advanced applicative and error handling (7-10, 17-20)
## Benefits
1. **Seamless Integration** - Mix Go idiomatic code with functional pipelines
2. **Gradual Adoption** - Convert code incrementally from idiomatic to functional
3. **Interoperability** - Work with existing Go libraries that return `(value, error)`
4. **Consistency** - Mirrors the successful pattern from `readerresult`
## References
- See `readerresult` package for similar implementations
- See `idiomatic/readerresult` for the idiomatic types
- See `idiomatic/ioresult` for IO-level idiomatic patterns

View File

@@ -69,7 +69,7 @@ func main() {
none := option.None[int]()
// Map over values
doubled := option.Map(func(x int) int { return x * 2 })(some)
doubled := option.Map(N.Mul(2))(some)
fmt.Println(option.GetOrElse(0)(doubled)) // Output: 84
// Chain operations
@@ -187,7 +187,7 @@ Monadic operations for `Pair` now operate on the **second argument** to align wi
```go
// Operations on first element
pair := MakePair(1, "hello")
result := Map(func(x int) int { return x * 2 })(pair) // Pair(2, "hello")
result := Map(N.Mul(2))(pair) // Pair(2, "hello")
```
**V2:**
@@ -204,8 +204,8 @@ The `Compose` function for endomorphisms now follows **mathematical function com
**V1:**
```go
// Compose executed left-to-right
double := func(x int) int { return x * 2 }
increment := func(x int) int { return x + 1 }
double := N.Mul(2)
increment := N.Add(1)
composed := Compose(double, increment)
result := composed(5) // (5 * 2) + 1 = 11
```
@@ -213,8 +213,8 @@ result := composed(5) // (5 * 2) + 1 = 11
**V2:**
```go
// Compose executes RIGHT-TO-LEFT (mathematical composition)
double := func(x int) int { return x * 2 }
increment := func(x int) int { return x + 1 }
double := N.Mul(2)
increment := N.Add(1)
composed := Compose(double, increment)
result := composed(5) // (5 + 1) * 2 = 12
@@ -368,7 +368,7 @@ If you're using `Pair`, update operations to work on the second element:
```go
pair := MakePair(42, "data")
// Map operates on first element
result := Map(func(x int) int { return x * 2 })(pair)
result := Map(N.Mul(2))(pair)
```
**After (V2):**

View File

@@ -17,11 +17,10 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
)
@@ -50,16 +49,16 @@ func Replicate[A any](n int, a A) []A {
// This is the monadic version of Map that takes the array as the first parameter.
//
//go:inline
func MonadMap[A, B any](as []A, f func(a A) B) []B {
func MonadMap[A, B any](as []A, f func(A) B) []B {
return G.MonadMap[[]A, []B](as, f)
}
// MonadMapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is useful when you need to access elements by reference without copying.
func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
func MonadMapRef[A, B any](as []A, f func(*A) B) []B {
count := len(as)
bs := make([]B, count)
for i := count - 1; i >= 0; i-- {
for i := range count {
bs[i] = f(&as[i])
}
return bs
@@ -68,7 +67,7 @@ func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
// MapWithIndex applies a function to each element and its index in an array, returning a new array with the results.
//
//go:inline
func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
func MapWithIndex[A, B any](f func(int, A) B) Operator[A, B] {
return G.MapWithIndex[[]A, []B](f)
}
@@ -77,39 +76,39 @@ func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
//
// Example:
//
// double := array.Map(func(x int) int { return x * 2 })
// double := array.Map(N.Mul(2))
// result := double([]int{1, 2, 3}) // [2, 4, 6]
//
//go:inline
func Map[A, B any](f func(a A) B) func([]A) []B {
func Map[A, B any](f func(A) B) Operator[A, B] {
return G.Map[[]A, []B](f)
}
// MapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is the curried version that returns a function.
func MapRef[A, B any](f func(a *A) B) func([]A) []B {
func MapRef[A, B any](f func(*A) B) Operator[A, B] {
return F.Bind2nd(MonadMapRef[A, B], f)
}
func filterRef[A any](fa []A, pred func(a *A) bool) []A {
var result []A
func filterRef[A any](fa []A, pred func(*A) bool) []A {
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, a)
var result []A = make([]A, 0, count)
for i := range count {
a := &fa[i]
if pred(a) {
result = append(result, *a)
}
}
return result
}
func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
var result []B
func filterMapRef[A, B any](fa []A, pred func(*A) bool, f func(*A) B) []B {
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, f(&a))
var result []B = make([]B, 0, count)
for i := range count {
a := &fa[i]
if pred(a) {
result = append(result, f(a))
}
}
return result
@@ -118,19 +117,19 @@ func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
// Filter returns a new array with all elements from the original array that match a predicate
//
//go:inline
func Filter[A any](pred func(A) bool) EM.Endomorphism[[]A] {
func Filter[A any](pred func(A) bool) Operator[A, A] {
return G.Filter[[]A](pred)
}
// FilterWithIndex returns a new array with all elements from the original array that match a predicate
//
//go:inline
func FilterWithIndex[A any](pred func(int, A) bool) EM.Endomorphism[[]A] {
func FilterWithIndex[A any](pred func(int, A) bool) Operator[A, A] {
return G.FilterWithIndex[[]A](pred)
}
// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers.
func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
func FilterRef[A any](pred func(*A) bool) Operator[A, A] {
return F.Bind2nd(filterRef[A], pred)
}
@@ -138,7 +137,7 @@ func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
// This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
func MonadFilterMap[A, B any](fa []A, f option.Kleisli[A, B]) []B {
return G.MonadFilterMap[[]A, []B](fa, f)
}
@@ -146,33 +145,33 @@ func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
// keeping only the Some values. This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) O.Option[B]) []B {
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) Option[B]) []B {
return G.MonadFilterMapWithIndex[[]A, []B](fa, f)
}
// FilterMap maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
// FilterMap maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMap[A, B any](f func(A) O.Option[B]) func([]A) []B {
func FilterMap[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return G.FilterMap[[]A, []B](f)
}
// FilterMapWithIndex maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
// FilterMapWithIndex maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMapWithIndex[A, B any](f func(int, A) O.Option[B]) func([]A) []B {
func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return G.FilterMapWithIndex[[]A, []B](f)
}
// FilterChain maps an array with an iterating function that returns an [O.Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
//
//go:inline
func FilterChain[A, B any](f func(A) O.Option[[]B]) func([]A) []B {
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.FilterChain[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B {
func FilterMapRef[A, B any](pred func(a *A) bool, f func(*A) B) Operator[A, B] {
return func(fa []A) []B {
return filterMapRef(fa, pred, f)
}
@@ -180,8 +179,7 @@ func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B
func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
current := initial
count := len(fa)
for i := 0; i < count; i++ {
for i := range len(fa) {
current = f(current, &fa[i])
}
return current
@@ -262,6 +260,8 @@ func Empty[A any]() []A {
}
// Zero returns an empty array of type A (alias for Empty).
//
//go:inline
func Zero[A any]() []A {
return Empty[A]()
}
@@ -277,7 +277,7 @@ func Of[A any](a A) []A {
// This is the monadic version that takes the array as the first parameter (also known as FlatMap).
//
//go:inline
func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
func MonadChain[A, B any](fa []A, f Kleisli[A, B]) []B {
return G.MonadChain(fa, f)
}
@@ -290,7 +290,7 @@ func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
// result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3]
//
//go:inline
func Chain[A, B any](f func(A) []B) func([]A) []B {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return G.Chain[[]A](f)
}
@@ -306,7 +306,7 @@ func MonadAp[B, A any](fab []func(A) B, fa []A) []B {
// This is the curried version.
//
//go:inline
func Ap[B, A any](fa []A) func([]func(A) B) []B {
func Ap[B, A any](fa []A) Operator[func(A) B, B] {
return G.Ap[[]B, []func(A) B](fa)
}
@@ -328,7 +328,7 @@ func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A)
// Returns None if the array is empty.
//
//go:inline
func Tail[A any](as []A) O.Option[[]A] {
func Tail[A any](as []A) Option[[]A] {
return G.Tail(as)
}
@@ -336,7 +336,7 @@ func Tail[A any](as []A) O.Option[[]A] {
// Returns None if the array is empty.
//
//go:inline
func Head[A any](as []A) O.Option[A] {
func Head[A any](as []A) Option[A] {
return G.Head(as)
}
@@ -344,7 +344,7 @@ func Head[A any](as []A) O.Option[A] {
// Returns None if the array is empty.
//
//go:inline
func First[A any](as []A) O.Option[A] {
func First[A any](as []A) Option[A] {
return G.First(as)
}
@@ -352,12 +352,12 @@ func First[A any](as []A) O.Option[A] {
// Returns None if the array is empty.
//
//go:inline
func Last[A any](as []A) O.Option[A] {
func Last[A any](as []A) Option[A] {
return G.Last(as)
}
// PrependAll inserts a separator before each element of an array.
func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
func PrependAll[A any](middle A) Operator[A, A] {
return func(as []A) []A {
count := len(as)
dst := count * 2
@@ -377,7 +377,7 @@ func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
// Example:
//
// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3]
func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
func Intersperse[A any](middle A) Operator[A, A] {
prepend := PrependAll(middle)
return func(as []A) []A {
if IsEmpty(as) {
@@ -406,7 +406,7 @@ func Flatten[A any](mma [][]A) []A {
}
// Slice extracts a subarray from index low (inclusive) to high (exclusive).
func Slice[A any](low, high int) func(as []A) []A {
func Slice[A any](low, high int) Operator[A, A] {
return array.Slice[[]A](low, high)
}
@@ -414,7 +414,7 @@ func Slice[A any](low, high int) func(as []A) []A {
// Returns None if the index is out of bounds.
//
//go:inline
func Lookup[A any](idx int) func([]A) O.Option[A] {
func Lookup[A any](idx int) func([]A) Option[A] {
return G.Lookup[[]A](idx)
}
@@ -422,7 +422,7 @@ func Lookup[A any](idx int) func([]A) O.Option[A] {
// If the index is out of bounds, the element is appended.
//
//go:inline
func UpsertAt[A any](a A) EM.Endomorphism[[]A] {
func UpsertAt[A any](a A) Operator[A, A] {
return G.UpsertAt[[]A](a)
}
@@ -468,7 +468,7 @@ func ConstNil[A any]() []A {
// SliceRight extracts a subarray from the specified start index to the end.
//
//go:inline
func SliceRight[A any](start int) EM.Endomorphism[[]A] {
func SliceRight[A any](start int) Operator[A, A] {
return G.SliceRight[[]A](start)
}
@@ -482,7 +482,7 @@ func Copy[A any](b []A) []A {
// Clone creates a deep copy of the array using the provided endomorphism to clone the values
//
//go:inline
func Clone[A any](f func(A) A) func(as []A) []A {
func Clone[A any](f func(A) A) Operator[A, A] {
return G.Clone[[]A](f)
}
@@ -510,8 +510,8 @@ func Fold[A any](m M.Monoid[A]) func([]A) A {
// Push adds an element to the end of an array (alias for Append).
//
//go:inline
func Push[A any](a A) EM.Endomorphism[[]A] {
return G.Push[EM.Endomorphism[[]A]](a)
func Push[A any](a A) Operator[A, A] {
return G.Push[Operator[A, A]](a)
}
// MonadFlap applies a value to an array of functions, producing an array of results.
@@ -526,13 +526,13 @@ func MonadFlap[B, A any](fab []func(A) B, a A) []B {
// This is the curried version.
//
//go:inline
func Flap[B, A any](a A) func([]func(A) B) []B {
func Flap[B, A any](a A) Operator[func(A) B, B] {
return G.Flap[func(A) B, []func(A) B, []B](a)
}
// Prepend adds an element to the beginning of an array, returning a new array.
//
//go:inline
func Prepend[A any](head A) EM.Endomorphism[[]A] {
return G.Prepend[EM.Endomorphism[[]A]](head)
func Prepend[A any](head A) Operator[A, A] {
return G.Prepend[Operator[A, A]](head)
}

View File

@@ -35,7 +35,7 @@ func TestReplicate(t *testing.T) {
func TestMonadMap(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMap(src, func(x int) int { return x * 2 })
result := MonadMap(src, N.Mul(2))
assert.Equal(t, []int{2, 4, 6}, result)
}
@@ -173,8 +173,8 @@ func TestChain(t *testing.T) {
func TestMonadAp(t *testing.T) {
fns := []func(int) int{
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
N.Mul(2),
N.Add(10),
}
values := []int{1, 2}
result := MonadAp(fns, values)
@@ -268,7 +268,7 @@ func TestCopy(t *testing.T) {
func TestClone(t *testing.T) {
src := []int{1, 2, 3}
cloner := Clone(func(x int) int { return x * 2 })
cloner := Clone(N.Mul(2))
result := cloner(src)
assert.Equal(t, []int{2, 4, 6}, result)
}

View File

@@ -56,8 +56,8 @@ func Do[S any](
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) []T,
) func([]S1) []S2 {
f Kleisli[S1, T],
) Operator[S1, S2] {
return G.Bind[[]S1, []S2](setter, f)
}
@@ -79,7 +79,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.Let[[]S1, []S2](setter, f)
}
@@ -101,7 +101,7 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.LetTo[[]S1, []S2](setter, b)
}
@@ -120,7 +120,7 @@ func LetTo[S1, S2, T any](
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) func([]T) []S1 {
) Operator[T, S1] {
return G.BindTo[[]S1, []T](setter)
}
@@ -143,6 +143,6 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa []T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.ApS[[]S1, []S2](setter, fa)
}

View File

@@ -36,7 +36,7 @@
// generated := array.MakeBy(5, func(i int) int { return i * 2 })
//
// // Transforming arrays
// doubled := array.Map(func(x int) int { return x * 2 })(arr)
// doubled := array.Map(N.Mul(2))(arr)
// filtered := array.Filter(func(x int) bool { return x > 2 })(arr)
//
// // Combining arrays
@@ -50,7 +50,7 @@
// numbers := []int{1, 2, 3, 4, 5}
//
// // Map transforms each element
// doubled := array.Map(func(x int) int { return x * 2 })(numbers)
// doubled := array.Map(N.Mul(2))(numbers)
// // Result: [2, 4, 6, 8, 10]
//
// // Filter keeps elements matching a predicate

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/option"
)
// FindFirst finds the first element which satisfies a predicate function.
@@ -30,7 +30,7 @@ import (
// result2 := findGreaterThan3([]int{1, 2, 3}) // None
//
//go:inline
func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
func FindFirst[A any](pred func(A) bool) option.Kleisli[[]A, A] {
return G.FindFirst[[]A](pred)
}
@@ -45,7 +45,7 @@ func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
// result := findEvenAtEvenIndex([]int{1, 3, 4, 5}) // Some(4)
//
//go:inline
func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
func FindFirstWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
return G.FindFirstWithIndex[[]A](pred)
}
@@ -65,7 +65,7 @@ func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
// result := parseFirst([]string{"a", "42", "b"}) // Some(42)
//
//go:inline
func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
func FindFirstMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
return G.FindFirstMap[[]A](sel)
}
@@ -73,7 +73,7 @@ func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
// The selector receives both the index and the element.
//
//go:inline
func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
func FindFirstMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
return G.FindFirstMapWithIndex[[]A](sel)
}
@@ -86,7 +86,7 @@ func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.O
// result := findGreaterThan3([]int{1, 4, 2, 5}) // Some(5)
//
//go:inline
func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
func FindLast[A any](pred func(A) bool) option.Kleisli[[]A, A] {
return G.FindLast[[]A](pred)
}
@@ -94,7 +94,7 @@ func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
// Returns Some(element) if found, None if no element matches.
//
//go:inline
func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
func FindLastWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
return G.FindLastWithIndex[[]A](pred)
}
@@ -102,7 +102,7 @@ func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
// This combines finding and mapping in a single operation, searching from the end.
//
//go:inline
func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
func FindLastMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
return G.FindLastMap[[]A](sel)
}
@@ -110,6 +110,6 @@ func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
// The selector receives both the index and the element, searching from the end.
//
//go:inline
func FindLastMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
func FindLastMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
return G.FindLastMapWithIndex[[]A](sel)
}

View File

@@ -25,8 +25,10 @@ import (
)
// Of constructs a single element array
//
//go:inline
func Of[GA ~[]A, A any](value A) GA {
return GA{value}
return array.Of[GA](value)
}
func Reduce[GA ~[]A, A, B any](f func(B, A) B, initial B) func(GA) B {
@@ -82,7 +84,7 @@ func MakeBy[AS ~[]A, F ~func(int) A, A any](n int, f F) AS {
}
// run the generator function across the input
as := make(AS, n)
for i := n - 1; i >= 0; i-- {
for i := range n {
as[i] = f(i)
}
return as
@@ -165,10 +167,9 @@ func Size[GA ~[]A, A any](as GA) int {
func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for _, a := range fa {
O.Map(func(b B) B {
if b, ok := O.Unwrap(f(a)); ok {
result = append(result, b)
return b
})(f(a))
}
}
return result
}
@@ -176,10 +177,9 @@ func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
func filterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(int, A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for i, a := range fa {
O.Map(func(b B) B {
if b, ok := O.Unwrap(f(i, a)); ok {
result = append(result, b)
return b
})(f(i, a))
}
}
return result
}

View File

@@ -42,8 +42,7 @@ func FindFirst[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) O.Option[
func FindFirstMapWithIndex[AS ~[]A, PRED ~func(int, A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
none := O.None[B]()
return func(as AS) O.Option[B] {
count := len(as)
for i := 0; i < count; i++ {
for i := range len(as) {
out := pred(i, as[i])
if O.IsSome(out) {
return out

View File

@@ -0,0 +1,34 @@
package generic
import (
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
// Monoid returns a Monoid instance for arrays.
// The Monoid combines arrays through concatenation, with an empty array as the identity element.
//
// Example:
//
// m := array.Monoid[int]()
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
// empty := m.Empty() // []
//
//go:inline
func Monoid[GT ~[]T, T any]() M.Monoid[GT] {
return M.MakeMonoid(array.Concat[GT], Empty[GT]())
}
// Semigroup returns a Semigroup instance for arrays.
// The Semigroup combines arrays through concatenation.
//
// Example:
//
// s := array.Semigroup[int]()
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
//
//go:inline
func Semigroup[GT ~[]T, T any]() S.Semigroup[GT] {
return S.MakeSemigroup(array.Concat[GT])
}

View File

@@ -26,7 +26,7 @@ import (
func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, fb BS, f FCT) CS {
l := N.Min(len(fa), len(fb))
res := make(CS, l)
for i := l - 1; i >= 0; i-- {
for i := range l {
res[i] = f(fa[i], fb[i])
}
return res
@@ -43,7 +43,7 @@ func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS,
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := l - 1; i >= 0; i-- {
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2

View File

@@ -18,7 +18,6 @@ package array
import (
"testing"
O "github.com/IBM/fp-go/v2/option"
OR "github.com/IBM/fp-go/v2/ord"
"github.com/stretchr/testify/assert"
)
@@ -103,39 +102,6 @@ func TestSortByKey(t *testing.T) {
assert.Equal(t, "Charlie", result[2].Name)
}
func TestMonadTraverse(t *testing.T) {
result := MonadTraverse(
O.Of[[]int],
O.Map[[]int, func(int) []int],
O.Ap[[]int, int],
[]int{1, 3, 5},
func(n int) O.Option[int] {
if n%2 == 1 {
return O.Some(n * 2)
}
return O.None[int]()
},
)
assert.Equal(t, O.Some([]int{2, 6, 10}), result)
// Test with None case
result2 := MonadTraverse(
O.Of[[]int],
O.Map[[]int, func(int) []int],
O.Ap[[]int, int],
[]int{1, 2, 3},
func(n int) O.Option[int] {
if n%2 == 1 {
return O.Some(n * 2)
}
return O.None[int]()
},
)
assert.Equal(t, O.None[[]int](), result2)
}
func TestUniqByKey(t *testing.T) {
type Person struct {
Name string

View File

@@ -16,27 +16,12 @@
package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
func concat[T any](left, right []T) []T {
// some performance checks
ll := len(left)
if ll == 0 {
return right
}
lr := len(right)
if lr == 0 {
return left
}
// need to copy
buf := make([]T, ll+lr)
copy(buf[copy(buf, left):], right)
return buf
}
// Monoid returns a Monoid instance for arrays.
// The Monoid combines arrays through concatenation, with an empty array as the identity element.
//
@@ -45,8 +30,10 @@ func concat[T any](left, right []T) []T {
// m := array.Monoid[int]()
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
// empty := m.Empty() // []
//
//go:inline
func Monoid[T any]() M.Monoid[[]T] {
return M.MakeMonoid(concat[T], Empty[T]())
return G.Monoid[[]T]()
}
// Semigroup returns a Semigroup instance for arrays.
@@ -56,8 +43,10 @@ func Monoid[T any]() M.Monoid[[]T] {
//
// s := array.Semigroup[int]()
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
//
//go:inline
func Semigroup[T any]() S.Semigroup[[]T] {
return S.MakeSemigroup(concat[T])
return G.Semigroup[[]T]()
}
func addLen[A any](count int, data []A) int {

View File

@@ -16,10 +16,18 @@
package array
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
)
func MonadSequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
ma []HKTA) HKTRA {
return array.MonadSequence(fof, m.Empty(), m.Concat, ma)
}
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
// using an applicative of that HKT, returns an HKT of []A.
//
@@ -55,16 +63,11 @@ import (
// option.MonadAp[[]int, int],
// )
// result := seq(opts) // Some([1, 2, 3])
func Sequence[A, HKTA, HKTRA, HKTFRA any](
_of func([]A) HKTRA,
_map func(HKTRA, func([]A) func(A) []A) HKTFRA,
_ap func(HKTFRA, HKTA) HKTRA,
func Sequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
) func([]HKTA) HKTRA {
ca := F.Curry2(Append[A])
empty := _of(Empty[A]())
return Reduce(func(fas HKTRA, fa HKTA) HKTRA {
return _ap(_map(fas, ca), fa)
}, empty)
return array.Sequence[[]HKTA](fof, m.Empty(), m.Concat)
}
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.
@@ -86,10 +89,10 @@ func Sequence[A, HKTA, HKTRA, HKTFRA any](
// option.Some(3),
// }
// result2 := array.ArrayOption[int]()(opts2) // None
func ArrayOption[A any]() func([]O.Option[A]) O.Option[[]A] {
return Sequence(
O.Of[[]A],
O.MonadMap[[]A, func(A) []A],
O.MonadAp[[]A, A],
func ArrayOption[A any](ma []Option[A]) Option[[]A] {
return MonadSequence(
O.Map(Of[A]),
O.ApplicativeMonoid(Monoid[A]()),
ma,
)
}

View File

@@ -24,8 +24,7 @@ import (
)
func TestSequenceOption(t *testing.T) {
seq := ArrayOption[int]()
assert.Equal(t, O.Of([]int{1, 3}), seq([]O.Option[int]{O.Of(1), O.Of(3)}))
assert.Equal(t, O.None[[]int](), seq([]O.Option[int]{O.Of(1), O.None[int]()}))
assert.Equal(t, O.Of([]int{1, 3}), ArrayOption([]O.Option[int]{O.Of(1), O.Of(3)}))
assert.Equal(t, O.None[[]int](), ArrayOption([]O.Option[int]{O.Of(1), O.None[int]()}))
}

View File

@@ -18,6 +18,7 @@ package array
import (
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -243,7 +244,7 @@ func TestSliceComposition(t *testing.T) {
t.Run("slice then map", func(t *testing.T) {
sliced := Slice[int](2, 5)(data)
mapped := Map(func(x int) int { return x * 2 })(sliced)
mapped := Map(N.Mul(2))(sliced)
assert.Equal(t, []int{4, 6, 8}, mapped)
})

View File

@@ -32,7 +32,7 @@ import (
// // Result: [1, 1, 2, 3, 4, 5, 6, 9]
//
//go:inline
func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
func Sort[T any](ord O.Ord[T]) Operator[T, T] {
return G.Sort[[]T](ord)
}
@@ -62,7 +62,7 @@ func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
// // Result: [{"Bob", 25}, {"Alice", 30}, {"Charlie", 35}]
//
//go:inline
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) Operator[T, T] {
return G.SortByKey[[]T](ord, f)
}
@@ -93,6 +93,6 @@ func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
// // Result: [{"Jones", "Bob"}, {"Smith", "Alice"}, {"Smith", "John"}]
//
//go:inline
func SortBy[T any](ord []O.Ord[T]) func(ma []T) []T {
func SortBy[T any](ord []O.Ord[T]) Operator[T, T] {
return G.SortBy[[]T](ord)
}

View File

@@ -80,3 +80,25 @@ func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
return array.MonadTraverse(fof, fmap, fap, ta, f)
}
//go:inline
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
f func(int, A) HKTB) func([]A) HKTRB {
return array.TraverseWithIndex[[]A](fof, fmap, fap, f)
}
//go:inline
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta []A,
f func(int, A) HKTB) HKTRB {
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
}

9
v2/array/types.go Normal file
View File

@@ -0,0 +1,9 @@
package array
import "github.com/IBM/fp-go/v2/option"
type (
Kleisli[A, B any] = func(A) []B
Operator[A, B any] = Kleisli[[]A, B]
Option[A any] = option.Option[A]
)

View File

@@ -46,6 +46,6 @@ func StrictUniq[A comparable](as []A) []A {
// // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}]
//
//go:inline
func Uniq[A any, K comparable](f func(A) K) func(as []A) []A {
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
return G.Uniq[[]A](f)
}

470
v2/assert/assert.go Normal file
View File

@@ -0,0 +1,470 @@
// 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 assert provides functional assertion helpers for testing.
//
// This package wraps testify/assert functions in a Reader monad pattern,
// allowing for composable and functional test assertions. Each assertion
// returns a Reader that takes a *testing.T and performs the assertion.
//
// The package supports:
// - Equality and inequality assertions
// - Collection assertions (arrays, maps, strings)
// - Error handling assertions
// - Result type assertions
// - Custom predicate assertions
// - Composable test suites
//
// Example:
//
// func TestExample(t *testing.T) {
// value := 42
// assert.Equal(42)(value)(t) // Curried style
//
// // Composing multiple assertions
// arr := []int{1, 2, 3}
// assertions := assert.AllOf([]assert.Reader{
// assert.ArrayNotEmpty(arr),
// assert.ArrayLength[int](3)(arr),
// assert.ArrayContains(2)(arr),
// })
// assertions(t)
// }
package assert
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
var (
// Eq is the equal predicate checking if objects are equal
Eq = eq.FromEquals(assert.ObjectsAreEqual)
)
// wrap1 is an internal helper function that wraps testify assertion functions
// into the Reader monad pattern with curried parameters.
//
// It takes a testify assertion function and converts it into a curried function
// that first takes an expected value, then an actual value, and finally returns
// a Reader that performs the assertion when given a *testing.T.
//
// Parameters:
// - wrapped: The testify assertion function to wrap
// - expected: The expected value for comparison
// - msgAndArgs: Optional message and arguments for assertion failure
//
// Returns:
// - A Kleisli function that takes the actual value and returns a Reader
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, expected T, msgAndArgs ...any) Kleisli[T] {
return func(actual T) Reader {
return func(t *testing.T) bool {
return wrapped(t, expected, actual, msgAndArgs...)
}
}
}
// NotEqual tests if the expected and the actual values are not equal
func NotEqual[T any](expected T) Kleisli[T] {
return wrap1(assert.NotEqual, expected)
}
// Equal tests if the expected and the actual values are equal
func Equal[T any](expected T) Kleisli[T] {
return wrap1(assert.Equal, expected)
}
// ArrayNotEmpty checks if an array is not empty
func ArrayNotEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, arr)
}
}
// RecordNotEmpty checks if an map is not empty
func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, mp)
}
}
// StringNotEmpty checks if a string is not empty
func StringNotEmpty(s string) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, s)
}
}
// ArrayLength tests if an array has the expected length
func ArrayLength[T any](expected int) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// RecordLength tests if a map has the expected length
func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// StringLength tests if a string has the expected length
func StringLength[K comparable, T any](expected int) Kleisli[string] {
return func(actual string) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// NoError validates that there is no error
func NoError(err error) Reader {
return func(t *testing.T) bool {
return assert.NoError(t, err)
}
}
// Error validates that there is an error
func Error(err error) Reader {
return func(t *testing.T) bool {
return assert.Error(t, err)
}
}
// Success checks if a [Result] represents success
func Success[T any](res Result[T]) Reader {
return NoError(result.ToError(res))
}
// Failure checks if a [Result] represents failure
func Failure[T any](res Result[T]) Reader {
return Error(result.ToError(res))
}
// ArrayContains tests if a value is contained in an array
func ArrayContains[T any](expected T) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
return assert.Contains(t, actual, expected)
}
}
}
// ContainsKey tests if a key is contained in a map
func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Contains(t, actual, expected)
}
}
}
// NotContainsKey tests if a key is not contained in a map
func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.NotContains(t, actual, expected)
}
}
}
// That asserts that a particular predicate matches
func That[T any](pred Predicate[T]) Kleisli[T] {
return func(a T) Reader {
return func(t *testing.T) bool {
if pred(a) {
return true
}
return assert.Fail(t, fmt.Sprintf("Preficate %v does not match value %v", pred, a))
}
}
}
// AllOf combines multiple assertion Readers into a single Reader that passes
// only if all assertions pass.
//
// This function uses boolean AND logic (MonoidAll) to combine the results of
// all assertions. If any assertion fails, the combined assertion fails.
//
// This is useful for grouping related assertions together and ensuring all
// conditions are met.
//
// Parameters:
// - readers: Array of assertion Readers to combine
//
// Returns:
// - A single Reader that performs all assertions and returns true only if all pass
//
// Example:
//
// func TestUser(t *testing.T) {
// user := User{Name: "Alice", Age: 30, Active: true}
// assertions := assert.AllOf([]assert.Reader{
// assert.Equal("Alice")(user.Name),
// assert.Equal(30)(user.Age),
// assert.Equal(true)(user.Active),
// })
// assertions(t)
// }
//
//go:inline
func AllOf(readers []Reader) Reader {
return reader.MonadReduceArrayM(readers, boolean.MonoidAll)
}
// RunAll executes a map of named test cases, running each as a subtest.
//
// This function creates a Reader that runs multiple named test cases using
// Go's t.Run for proper test isolation and reporting. Each test case is
// executed as a separate subtest with its own name.
//
// The function returns true only if all subtests pass. This allows for
// better test organization and clearer test output.
//
// Parameters:
// - testcases: Map of test names to assertion Readers
//
// Returns:
// - A Reader that executes all named test cases and returns true if all pass
//
// Example:
//
// func TestMathOperations(t *testing.T) {
// testcases := map[string]assert.Reader{
// "addition": assert.Equal(4)(2 + 2),
// "multiplication": assert.Equal(6)(2 * 3),
// "subtraction": assert.Equal(1)(3 - 2),
// }
// assert.RunAll(testcases)(t)
// }
//
//go:inline
func RunAll(testcases map[string]Reader) Reader {
return func(t *testing.T) bool {
current := true
for k, r := range testcases {
current = current && t.Run(k, func(t1 *testing.T) {
r(t1)
})
}
return current
}
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
// by providing a function that converts R2 to R1. This allows you to focus a test on a
// specific property or subset of a larger data structure.
//
// This is particularly useful when you have an assertion that operates on a specific field
// or property, and you want to apply it to a complete object. Instead of extracting the
// property and then asserting on it, you can transform the assertion to work directly
// on the whole object.
//
// Parameters:
// - f: A function that extracts or transforms R2 into R1
//
// Returns:
// - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader]
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
//
// // Create an assertion that checks if age is positive
// ageIsPositive := assert.That(func(age int) bool { return age > 0 })
//
// // Focus this assertion on the Age field of User
// userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
//
// // Now we can test the whole User object
// user := User{Name: "Alice", Age: 30}
// userAgeIsPositive(user)(t)
//
//go:inline
func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] {
return reader.Local[Reader](f)
}
// LocalL is similar to Local but uses a Lens to focus on a specific property.
// A Lens is a functional programming construct that provides a composable way to
// focus on a part of a data structure.
//
// This function is particularly useful when you want to focus a test on a specific
// field of a struct using a lens, making the code more declarative and composable.
// Lenses are often code-generated or predefined for common data structures.
//
// Parameters:
// - l: A Lens that focuses from type S to type T
//
// Returns:
// - A function that transforms a Reader[T, Reader] into a Reader[S, Reader]
//
// Example:
//
// type Person struct {
// Name string
// Email string
// }
//
// // Assume we have a lens that focuses on the Email field
// var emailLens = lens.Prop[Person, string]("Email")
//
// // Create an assertion for email format
// validEmail := assert.That(func(email string) bool {
// return strings.Contains(email, "@")
// })
//
// // Focus this assertion on the Email property using a lens
// validPersonEmail := assert.LocalL(emailLens)(validEmail)
//
// // Test a Person object
// person := Person{Name: "Bob", Email: "bob@example.com"}
// validPersonEmail(person)(t)
//
//go:inline
func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] {
return reader.Local[Reader](l.Get)
}
// fromOptionalGetter is an internal helper that creates an assertion Reader from
// an optional getter function. It asserts that the optional value is present (Some).
func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] {
return func(s S) Reader {
return func(t *testing.T) bool {
return assert.True(t, option.IsSome(getter(s)), msgAndArgs...)
}
}
}
// FromOptional creates an assertion that checks if an Optional can successfully extract a value.
// An Optional is an optic that represents an optional reference to a subpart of a data structure.
//
// This function is useful when you have an Optional optic and want to assert that the optional
// value is present (Some) rather than absent (None). The assertion passes if the Optional's
// GetOption returns Some, and fails if it returns None.
//
// This enables property-focused testing where you verify that a particular optional field or
// sub-structure exists and is accessible.
//
// Parameters:
// - opt: An Optional optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the optional value is present when applied to a value of type S
//
// Example:
//
// type Config struct {
// Database *DatabaseConfig // Optional field
// }
//
// type DatabaseConfig struct {
// Host string
// Port int
// }
//
// // Create an Optional that focuses on the Database field
// dbOptional := optional.MakeOptional(
// func(c Config) option.Option[*DatabaseConfig] {
// if c.Database != nil {
// return option.Some(c.Database)
// }
// return option.None[*DatabaseConfig]()
// },
// func(c Config, db *DatabaseConfig) Config {
// c.Database = db
// return c
// },
// )
//
// // Assert that the database config is present
// hasDatabaseConfig := assert.FromOptional(dbOptional)
//
// config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
// hasDatabaseConfig(config)(t) // Passes
//
// emptyConfig := Config{Database: nil}
// hasDatabaseConfig(emptyConfig)(t) // Fails
//
//go:inline
func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(opt.GetOption, "Optional: %s", opt)
}
// FromPrism creates an assertion that checks if a Prism can successfully extract a value.
// A Prism is an optic used to select part of a sum type (tagged union or variant).
//
// This function is useful when you have a Prism optic and want to assert that a value
// matches a specific variant of a sum type. The assertion passes if the Prism's GetOption
// returns Some (meaning the value is of the expected variant), and fails if it returns None
// (meaning the value is a different variant).
//
// This enables variant-focused testing where you verify that a value is of a particular
// type or matches a specific condition within a sum type.
//
// Parameters:
// - p: A Prism optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the prism successfully extracts when applied to a value of type S
//
// Example:
//
// type Result interface{ isResult() }
// type Success struct{ Value int }
// type Failure struct{ Error string }
//
// func (Success) isResult() {}
// func (Failure) isResult() {}
//
// // Create a Prism that focuses on Success variant
// successPrism := prism.MakePrism(
// func(r Result) option.Option[int] {
// if s, ok := r.(Success); ok {
// return option.Some(s.Value)
// }
// return option.None[int]()
// },
// func(v int) Result { return Success{Value: v} },
// )
//
// // Assert that the result is a Success
// isSuccess := assert.FromPrism(successPrism)
//
// result1 := Success{Value: 42}
// isSuccess(result1)(t) // Passes
//
// result2 := Failure{Error: "something went wrong"}
// isSuccess(result2)(t) // Fails
//
//go:inline
func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(p.GetOption, "Prism: %s", p)
}

View File

@@ -16,94 +16,676 @@
package assert
import (
"fmt"
"errors"
"testing"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
var (
errTest = fmt.Errorf("test failure")
// Eq is the equal predicate checking if objects are equal
Eq = eq.FromEquals(assert.ObjectsAreEqual)
)
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, t *testing.T, expected T) result.Kleisli[T, T] {
return func(actual T) Result[T] {
ok := wrapped(t, expected, actual)
if ok {
return result.Of(actual)
func TestEqual(t *testing.T) {
t.Run("should pass when values are equal", func(t *testing.T) {
result := Equal(42)(42)(t)
if !result {
t.Error("Expected Equal to pass for equal values")
}
return result.Left[T](errTest)
}
}
})
// NotEqual tests if the expected and the actual values are not equal
func NotEqual[T any](t *testing.T, expected T) result.Kleisli[T, T] {
return wrap1(assert.NotEqual, t, expected)
}
// Equal tests if the expected and the actual values are equal
func Equal[T any](t *testing.T, expected T) result.Kleisli[T, T] {
return wrap1(assert.Equal, t, expected)
}
// Length tests if an array has the expected length
func Length[T any](t *testing.T, expected int) result.Kleisli[[]T, []T] {
return func(actual []T) Result[[]T] {
ok := assert.Len(t, actual, expected)
if ok {
return result.Of(actual)
t.Run("should fail when values are not equal", func(t *testing.T) {
mockT := &testing.T{}
result := Equal(42)(43)(mockT)
if result {
t.Error("Expected Equal to fail for different values")
}
return result.Left[[]T](errTest)
}
})
t.Run("should work with strings", func(t *testing.T) {
result := Equal("hello")("hello")(t)
if !result {
t.Error("Expected Equal to pass for equal strings")
}
})
}
// NoError validates that there is no error
func NoError[T any](t *testing.T) result.Operator[T, T] {
return func(actual Result[T]) Result[T] {
return result.MonadFold(actual, func(e error) Result[T] {
assert.NoError(t, e)
return result.Left[T](e)
}, func(value T) Result[T] {
assert.NoError(t, nil)
return result.Of(value)
func TestNotEqual(t *testing.T) {
t.Run("should pass when values are not equal", func(t *testing.T) {
result := NotEqual(42)(43)(t)
if !result {
t.Error("Expected NotEqual to pass for different values")
}
})
t.Run("should fail when values are equal", func(t *testing.T) {
mockT := &testing.T{}
result := NotEqual(42)(42)(mockT)
if result {
t.Error("Expected NotEqual to fail for equal values")
}
})
}
func TestArrayNotEmpty(t *testing.T) {
t.Run("should pass for non-empty array", func(t *testing.T) {
arr := []int{1, 2, 3}
result := ArrayNotEmpty(arr)(t)
if !result {
t.Error("Expected ArrayNotEmpty to pass for non-empty array")
}
})
t.Run("should fail for empty array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{}
result := ArrayNotEmpty(arr)(mockT)
if result {
t.Error("Expected ArrayNotEmpty to fail for empty array")
}
})
}
func TestRecordNotEmpty(t *testing.T) {
t.Run("should pass for non-empty map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
result := RecordNotEmpty(mp)(t)
if !result {
t.Error("Expected RecordNotEmpty to pass for non-empty map")
}
})
t.Run("should fail for empty map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{}
result := RecordNotEmpty(mp)(mockT)
if result {
t.Error("Expected RecordNotEmpty to fail for empty map")
}
})
}
func TestArrayLength(t *testing.T) {
t.Run("should pass when length matches", func(t *testing.T) {
arr := []int{1, 2, 3}
result := ArrayLength[int](3)(arr)(t)
if !result {
t.Error("Expected ArrayLength to pass when length matches")
}
})
t.Run("should fail when length doesn't match", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayLength[int](5)(arr)(mockT)
if result {
t.Error("Expected ArrayLength to fail when length doesn't match")
}
})
t.Run("should work with empty array", func(t *testing.T) {
arr := []string{}
result := ArrayLength[string](0)(arr)(t)
if !result {
t.Error("Expected ArrayLength to pass for empty array with expected length 0")
}
})
}
func TestRecordLength(t *testing.T) {
t.Run("should pass when map length matches", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
result := RecordLength[string, int](2)(mp)(t)
if !result {
t.Error("Expected RecordLength to pass when length matches")
}
})
t.Run("should fail when map length doesn't match", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1}
result := RecordLength[string, int](3)(mp)(mockT)
if result {
t.Error("Expected RecordLength to fail when length doesn't match")
}
})
}
func TestStringLength(t *testing.T) {
t.Run("should pass when string length matches", func(t *testing.T) {
str := "hello"
result := StringLength[string, int](5)(str)(t)
if !result {
t.Error("Expected StringLength to pass when length matches")
}
})
t.Run("should fail when string length doesn't match", func(t *testing.T) {
mockT := &testing.T{}
str := "hello"
result := StringLength[string, int](10)(str)(mockT)
if result {
t.Error("Expected StringLength to fail when length doesn't match")
}
})
t.Run("should work with empty string", func(t *testing.T) {
str := ""
result := StringLength[string, int](0)(str)(t)
if !result {
t.Error("Expected StringLength to pass for empty string with expected length 0")
}
})
}
func TestNoError(t *testing.T) {
t.Run("should pass when error is nil", func(t *testing.T) {
result := NoError(nil)(t)
if !result {
t.Error("Expected NoError to pass when error is nil")
}
})
t.Run("should fail when error is not nil", func(t *testing.T) {
mockT := &testing.T{}
err := errors.New("test error")
result := NoError(err)(mockT)
if result {
t.Error("Expected NoError to fail when error is not nil")
}
})
}
func TestError(t *testing.T) {
t.Run("should pass when error is not nil", func(t *testing.T) {
err := errors.New("test error")
result := Error(err)(t)
if !result {
t.Error("Expected Error to pass when error is not nil")
}
})
t.Run("should fail when error is nil", func(t *testing.T) {
mockT := &testing.T{}
result := Error(nil)(mockT)
if result {
t.Error("Expected Error to fail when error is nil")
}
})
}
func TestSuccess(t *testing.T) {
t.Run("should pass for successful result", func(t *testing.T) {
res := result.Of(42)
result := Success(res)(t)
if !result {
t.Error("Expected Success to pass for successful result")
}
})
t.Run("should fail for error result", func(t *testing.T) {
mockT := &testing.T{}
res := result.Left[int](errors.New("test error"))
result := Success(res)(mockT)
if result {
t.Error("Expected Success to fail for error result")
}
})
}
func TestFailure(t *testing.T) {
t.Run("should pass for error result", func(t *testing.T) {
res := result.Left[int](errors.New("test error"))
result := Failure(res)(t)
if !result {
t.Error("Expected Failure to pass for error result")
}
})
t.Run("should fail for successful result", func(t *testing.T) {
mockT := &testing.T{}
res := result.Of(42)
result := Failure(res)(mockT)
if result {
t.Error("Expected Failure to fail for successful result")
}
})
}
func TestArrayContains(t *testing.T) {
t.Run("should pass when element is in array", func(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
result := ArrayContains(3)(arr)(t)
if !result {
t.Error("Expected ArrayContains to pass when element is in array")
}
})
t.Run("should fail when element is not in array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayContains(10)(arr)(mockT)
if result {
t.Error("Expected ArrayContains to fail when element is not in array")
}
})
t.Run("should work with strings", func(t *testing.T) {
arr := []string{"apple", "banana", "cherry"}
result := ArrayContains("banana")(arr)(t)
if !result {
t.Error("Expected ArrayContains to pass for string element")
}
})
}
func TestContainsKey(t *testing.T) {
t.Run("should pass when key exists in map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2, "c": 3}
result := ContainsKey[int]("b")(mp)(t)
if !result {
t.Error("Expected ContainsKey to pass when key exists")
}
})
t.Run("should fail when key doesn't exist in map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := ContainsKey[int]("z")(mp)(mockT)
if result {
t.Error("Expected ContainsKey to fail when key doesn't exist")
}
})
}
func TestNotContainsKey(t *testing.T) {
t.Run("should pass when key doesn't exist in map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
result := NotContainsKey[int]("z")(mp)(t)
if !result {
t.Error("Expected NotContainsKey to pass when key doesn't exist")
}
})
t.Run("should fail when key exists in map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := NotContainsKey[int]("a")(mp)(mockT)
if result {
t.Error("Expected NotContainsKey to fail when key exists")
}
})
}
func TestThat(t *testing.T) {
t.Run("should pass when predicate is true", func(t *testing.T) {
isEven := func(n int) bool { return n%2 == 0 }
result := That(isEven)(42)(t)
if !result {
t.Error("Expected That to pass when predicate is true")
}
})
t.Run("should fail when predicate is false", func(t *testing.T) {
mockT := &testing.T{}
isEven := func(n int) bool { return n%2 == 0 }
result := That(isEven)(43)(mockT)
if result {
t.Error("Expected That to fail when predicate is false")
}
})
t.Run("should work with string predicates", func(t *testing.T) {
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' }
result := That(startsWithH)("hello")(t)
if !result {
t.Error("Expected That to pass for string predicate")
}
})
}
func TestAllOf(t *testing.T) {
t.Run("should pass when all assertions pass", func(t *testing.T) {
assertions := AllOf([]Reader{
Equal(42)(42),
Equal("hello")("hello"),
ArrayNotEmpty([]int{1, 2, 3}),
})
}
result := assertions(t)
if !result {
t.Error("Expected AllOf to pass when all assertions pass")
}
})
t.Run("should fail when any assertion fails", func(t *testing.T) {
mockT := &testing.T{}
assertions := AllOf([]Reader{
Equal(42)(42),
Equal("hello")("goodbye"),
ArrayNotEmpty([]int{1, 2, 3}),
})
result := assertions(mockT)
if result {
t.Error("Expected AllOf to fail when any assertion fails")
}
})
t.Run("should work with empty array", func(t *testing.T) {
assertions := AllOf([]Reader{})
result := assertions(t)
if !result {
t.Error("Expected AllOf to pass for empty array")
}
})
t.Run("should combine multiple array assertions", func(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
assertions := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
})
result := assertions(t)
if !result {
t.Error("Expected AllOf to pass for multiple array assertions")
}
})
}
// ArrayContains tests if a value is contained in an array
func ArrayContains[T any](t *testing.T, expected T) result.Kleisli[[]T, []T] {
return func(actual []T) Result[[]T] {
ok := assert.Contains(t, actual, expected)
if ok {
return result.Of(actual)
func TestRunAll(t *testing.T) {
t.Run("should run all named test cases", func(t *testing.T) {
testcases := map[string]Reader{
"equality": Equal(42)(42),
"string_check": Equal("test")("test"),
"array_check": ArrayNotEmpty([]int{1, 2, 3}),
}
return result.Left[[]T](errTest)
}
result := RunAll(testcases)(t)
if !result {
t.Error("Expected RunAll to pass when all test cases pass")
}
})
// Note: Testing failure behavior of RunAll is tricky because subtests
// will actually fail in the test framework. The function works correctly
// as demonstrated by the passing test above.
t.Run("should work with empty test cases", func(t *testing.T) {
testcases := map[string]Reader{}
result := RunAll(testcases)(t)
if !result {
t.Error("Expected RunAll to pass for empty test cases")
}
})
}
// ContainsKey tests if a key is contained in a map
func ContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
return func(actual map[K]T) Result[map[K]T] {
ok := assert.Contains(t, actual, expected)
if ok {
return result.Of(actual)
func TestEq(t *testing.T) {
t.Run("should return true for equal values", func(t *testing.T) {
if !Eq.Equals(42, 42) {
t.Error("Expected Eq to return true for equal integers")
}
return result.Left[map[K]T](errTest)
}
})
t.Run("should return false for different values", func(t *testing.T) {
if Eq.Equals(42, 43) {
t.Error("Expected Eq to return false for different integers")
}
})
t.Run("should work with strings", func(t *testing.T) {
if !Eq.Equals("hello", "hello") {
t.Error("Expected Eq to return true for equal strings")
}
if Eq.Equals("hello", "world") {
t.Error("Expected Eq to return false for different strings")
}
})
t.Run("should work with slices", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{1, 2, 3}
if !Eq.Equals(arr1, arr2) {
t.Error("Expected Eq to return true for equal slices")
}
})
}
// NotContainsKey tests if a key is not contained in a map
func NotContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
return func(actual map[K]T) Result[map[K]T] {
ok := assert.NotContains(t, actual, expected)
if ok {
return result.Of(actual)
}
return result.Left[map[K]T](errTest)
func TestLocal(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("should focus assertion on a property", func(t *testing.T) {
// Create an assertion that checks if age is positive
ageIsPositive := That(func(age int) bool { return age > 0 })
// Focus this assertion on the Age field of User
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has a positive age
user := User{Name: "Alice", Age: 30}
result := userAgeIsPositive(user)(t)
if !result {
t.Error("Expected focused assertion to pass for positive age")
}
})
t.Run("should fail when focused property doesn't match", func(t *testing.T) {
mockT := &testing.T{}
ageIsPositive := That(func(age int) bool { return age > 0 })
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has zero age
user := User{Name: "Bob", Age: 0}
result := userAgeIsPositive(user)(mockT)
if result {
t.Error("Expected focused assertion to fail for zero age")
}
})
t.Run("should compose with other assertions", func(t *testing.T) {
// Create multiple focused assertions
nameNotEmpty := Local(func(u User) string { return u.Name })(
That(func(name string) bool { return len(name) > 0 }),
)
ageInRange := Local(func(u User) int { return u.Age })(
That(func(age int) bool { return age >= 18 && age <= 100 }),
)
user := User{Name: "Charlie", Age: 25}
assertions := AllOf([]Reader{
nameNotEmpty(user),
ageInRange(user),
})
result := assertions(t)
if !result {
t.Error("Expected composed focused assertions to pass")
}
})
t.Run("should work with Equal assertion", func(t *testing.T) {
// Focus Equal assertion on Name field
nameIsAlice := Local(func(u User) string { return u.Name })(Equal("Alice"))
user := User{Name: "Alice", Age: 30}
result := nameIsAlice(user)(t)
if !result {
t.Error("Expected focused Equal assertion to pass")
}
})
}
func TestLocalL(t *testing.T) {
// Note: LocalL requires lens package which provides lens operations.
// This test demonstrates the concept, but actual usage would require
// proper lens definitions.
t.Run("conceptual test for LocalL", func(t *testing.T) {
// LocalL is similar to Local but uses lenses for focusing.
// It would be used like:
// validEmail := That(func(email string) bool { return strings.Contains(email, "@") })
// validPersonEmail := LocalL(emailLens)(validEmail)
//
// The actual implementation would require lens definitions from the lens package.
// This test serves as documentation for the intended usage.
})
}
func TestFromOptional(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
}
type Config struct {
Database *DatabaseConfig
}
// Create an Optional that focuses on the Database field
dbOptional := Optional[Config, *DatabaseConfig]{
GetOption: func(c Config) option.Option[*DatabaseConfig] {
if c.Database != nil {
return option.Of(c.Database)
}
return option.None[*DatabaseConfig]()
},
Set: func(db *DatabaseConfig) func(Config) Config {
return func(c Config) Config {
c.Database = db
return c
}
},
}
t.Run("should pass when optional value is present", func(t *testing.T) {
config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(config)(t)
if !result {
t.Error("Expected FromOptional to pass when optional value is present")
}
})
t.Run("should fail when optional value is absent", func(t *testing.T) {
mockT := &testing.T{}
emptyConfig := Config{Database: nil}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(emptyConfig)(mockT)
if result {
t.Error("Expected FromOptional to fail when optional value is absent")
}
})
t.Run("should work with nested optionals", func(t *testing.T) {
type AdvancedSettings struct {
Cache bool
}
type Settings struct {
Advanced *AdvancedSettings
}
advancedOptional := Optional[Settings, *AdvancedSettings]{
GetOption: func(s Settings) option.Option[*AdvancedSettings] {
if s.Advanced != nil {
return option.Of(s.Advanced)
}
return option.None[*AdvancedSettings]()
},
Set: func(adv *AdvancedSettings) func(Settings) Settings {
return func(s Settings) Settings {
s.Advanced = adv
return s
}
},
}
settings := Settings{Advanced: &AdvancedSettings{Cache: true}}
hasAdvanced := FromOptional(advancedOptional)
result := hasAdvanced(settings)(t)
if !result {
t.Error("Expected FromOptional to pass for nested optional")
}
})
}
// Helper types for Prism testing
type PrismTestResult interface {
isPrismTestResult()
}
type PrismTestSuccess struct {
Value int
}
type PrismTestFailure struct {
Error string
}
func (PrismTestSuccess) isPrismTestResult() {}
func (PrismTestFailure) isPrismTestResult() {}
func TestFromPrism(t *testing.T) {
// Create a Prism that focuses on Success variant using prism.MakePrism
successPrism := prism.MakePrism(
func(r PrismTestResult) option.Option[int] {
if s, ok := r.(PrismTestSuccess); ok {
return option.Of(s.Value)
}
return option.None[int]()
},
func(v int) PrismTestResult {
return PrismTestSuccess{Value: v}
},
)
// Create a Prism that focuses on Failure variant
failurePrism := prism.MakePrism(
func(r PrismTestResult) option.Option[string] {
if f, ok := r.(PrismTestFailure); ok {
return option.Of(f.Error)
}
return option.None[string]()
},
func(err string) PrismTestResult {
return PrismTestFailure{Error: err}
},
)
t.Run("should pass when prism successfully extracts", func(t *testing.T) {
result := PrismTestSuccess{Value: 42}
isSuccess := FromPrism(successPrism)
testResult := isSuccess(result)(t)
if !testResult {
t.Error("Expected FromPrism to pass when prism extracts successfully")
}
})
t.Run("should fail when prism cannot extract", func(t *testing.T) {
mockT := &testing.T{}
result := PrismTestFailure{Error: "something went wrong"}
isSuccess := FromPrism(successPrism)
testResult := isSuccess(result)(mockT)
if testResult {
t.Error("Expected FromPrism to fail when prism cannot extract")
}
})
t.Run("should work with failure prism", func(t *testing.T) {
result := PrismTestFailure{Error: "test error"}
isFailure := FromPrism(failurePrism)
testResult := isFailure(result)(t)
if !testResult {
t.Error("Expected FromPrism to pass for failure prism on failure result")
}
})
t.Run("should fail with failure prism on success result", func(t *testing.T) {
mockT := &testing.T{}
result := PrismTestSuccess{Value: 100}
isFailure := FromPrism(failurePrism)
testResult := isFailure(result)(mockT)
if testResult {
t.Error("Expected FromPrism to fail for failure prism on success result")
}
})
}

View File

@@ -1,7 +1,22 @@
package assert
import "github.com/IBM/fp-go/v2/result"
import (
"testing"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
Result[T any] = result.Result[T]
Result[T any] = result.Result[T]
Reader = reader.Reader[*testing.T, bool]
Kleisli[T any] = reader.Reader[T, Reader]
Predicate[T any] = predicate.Predicate[T]
Lens[S, T any] = lens.Lens[S, T]
Optional[S, T any] = optional.Optional[S, T]
Prism[S, T any] = prism.Prism[S, T]
)

View File

@@ -8,5 +8,5 @@ import (
// BuilderPrism createa a [Prism] that converts between a builder and its type
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
return prism.MakePrism(F.Flow2(B.Build, result.ToOption[T]), creator)
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
}

View File

@@ -382,7 +382,7 @@ func BenchmarkToString(b *testing.B) {
data := []byte("Hello, World!")
b.Run("small", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = ToString(data)
}
})
@@ -393,7 +393,7 @@ func BenchmarkToString(b *testing.B) {
large[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = ToString(large)
}
})
@@ -402,7 +402,7 @@ func BenchmarkToString(b *testing.B) {
func BenchmarkSize(b *testing.B) {
data := []byte("Hello, World!")
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Size(data)
}
}
@@ -412,7 +412,7 @@ func BenchmarkMonoidConcat(b *testing.B) {
c := []byte(" World")
b.Run("small slices", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Monoid.Concat(a, c)
}
})
@@ -421,7 +421,7 @@ func BenchmarkMonoidConcat(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Monoid.Concat(large1, large2)
}
})
@@ -436,7 +436,7 @@ func BenchmarkConcatAll(b *testing.B) {
}
b.Run("few slices", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = ConcatAll(slices...)
}
})
@@ -447,7 +447,7 @@ func BenchmarkConcatAll(b *testing.B) {
many[i] = []byte{byte(i)}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = ConcatAll(many...)
}
})
@@ -458,13 +458,13 @@ func BenchmarkOrdCompare(b *testing.B) {
c := []byte("abd")
b.Run("equal", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Ord.Compare(a, a)
}
})
b.Run("different", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Ord.Compare(a, c)
}
})
@@ -474,7 +474,7 @@ func BenchmarkOrdCompare(b *testing.B) {
large2 := make([]byte, 10000)
large2[9999] = 1
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Ord.Compare(large1, large2)
}
})

View File

@@ -53,9 +53,11 @@ var (
// structInfo holds information about a struct that needs lens generation
type structInfo struct {
Name string
Fields []fieldInfo
Imports map[string]string // package path -> alias
Name string
TypeParams string // e.g., "[T any]" or "[K comparable, V any]" - for type declarations
TypeParamNames string // e.g., "[T]" or "[K, V]" - for type usage in function signatures
Fields []fieldInfo
Imports map[string]string // package path -> alias
}
// fieldInfo holds information about a struct field
@@ -75,84 +77,96 @@ type templateData struct {
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
type {{.Name}}Lenses struct {
type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
{{.Name}} L.Lens[{{$.Name}}, {{.TypeName}}]
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{.Name}}O LO.LensO[{{$.Name}}, {{.TypeName}}]
{{- if .IsComparable}}
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
}
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
type {{.Name}}RefLenses struct {
type {{.Name}}RefLenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
{{.Name}} L.Lens[*{{$.Name}}, {{.TypeName}}]
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{.Name}}O LO.LensO[*{{$.Name}}, {{.TypeName}}]
{{- if .IsComparable}}
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
}
`
const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
func Make{{.Name}}Lenses() {{.Name}}Lenses {
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
lens{{.Name}} := L.MakeLens(
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- end}}
// optional lenses
{{- range .Fields}}
lens{{.Name}}O := LO.FromIso[{{$.Name}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
return {{.Name}}Lenses{
{{- end}}
return {{.Name}}Lenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}}
{{.Name}}: lens{{.Name}},
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O: lens{{.Name}}O,
{{- end}}
{{- end}}
}
}
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
{{- if .IsComparable}}
lens{{.Name}} := L.MakeLensStrict(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- else}}
lens{{.Name}} := L.MakeLensRef(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- end}}
{{- end}}
// optional lenses
{{- range .Fields}}
lens{{.Name}}O := LO.FromIso[*{{$.Name}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
return {{.Name}}RefLenses{
{{- end}}
return {{.Name}}RefLenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}}
{{.Name}}: lens{{.Name}},
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O: lens{{.Name}}O,
{{- end}}
{{- end}}
}
}
@@ -290,9 +304,17 @@ func isPointerType(expr ast.Expr) bool {
// - Slices
// - Maps
// - Functions
func isComparableType(expr ast.Expr) bool {
//
// typeParams is a map of type parameter names to their constraints (e.g., "T" -> "any", "K" -> "comparable")
func isComparableType(expr ast.Expr, typeParams map[string]string) bool {
switch t := expr.(type) {
case *ast.Ident:
// Check if this is a type parameter
if constraint, isTypeParam := typeParams[t.Name]; isTypeParam {
// Type parameter - check its constraint
return constraint == "comparable"
}
// Basic types and named types
// We assume named types are comparable unless they're known non-comparable types
name := t.Name
@@ -315,7 +337,7 @@ func isComparableType(expr ast.Expr) bool {
return false
}
// Fixed-size array, check element type
return isComparableType(t.Elt)
return isComparableType(t.Elt, typeParams)
case *ast.MapType:
// Maps are not comparable
return false
@@ -383,6 +405,145 @@ func isComparableType(expr ast.Expr) bool {
}
}
// embeddedFieldResult holds both the field info and its AST type for import extraction
type embeddedFieldResult struct {
fieldInfo fieldInfo
fieldType ast.Expr
}
// extractEmbeddedFields extracts fields from an embedded struct type
// It returns a slice of embeddedFieldResult for all exported fields in the embedded struct
// typeParamsMap contains the type parameters of the parent struct (for checking comparability)
func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, file *ast.File, typeParamsMap map[string]string) []embeddedFieldResult {
var results []embeddedFieldResult
// Get the type name of the embedded field
var typeName string
var typeIdent *ast.Ident
switch t := embedType.(type) {
case *ast.Ident:
// Direct embedded type: type MyStruct struct { EmbeddedType }
typeName = t.Name
typeIdent = t
case *ast.StarExpr:
// Pointer embedded type: type MyStruct struct { *EmbeddedType }
if ident, ok := t.X.(*ast.Ident); ok {
typeName = ident.Name
typeIdent = ident
}
case *ast.SelectorExpr:
// Qualified embedded type: type MyStruct struct { pkg.EmbeddedType }
// We can't easily resolve this without full type information
// For now, skip these
return results
}
if typeName == "" || typeIdent == nil {
return results
}
// Find the struct definition in the same file
var embeddedStructType *ast.StructType
ast.Inspect(file, func(n ast.Node) bool {
if ts, ok := n.(*ast.TypeSpec); ok {
if ts.Name.Name == typeName {
if st, ok := ts.Type.(*ast.StructType); ok {
embeddedStructType = st
return false
}
}
}
return true
})
if embeddedStructType == nil {
// Struct not found in this file, might be from another package
return results
}
// Extract fields from the embedded struct
for _, field := range embeddedStructType.Fields.List {
// Skip embedded fields within embedded structs (for now, to avoid infinite recursion)
if len(field.Names) == 0 {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
isOptional := false
baseType := fieldTypeName
// Check if field is optional
if isPointerType(field.Type) {
isOptional = true
baseType = strings.TrimPrefix(fieldTypeName, "*")
} else if hasOmitEmpty(field.Tag) {
isOptional = true
}
// Check if the type is comparable
isComparable := isComparableType(field.Type, typeParamsMap)
results = append(results, embeddedFieldResult{
fieldInfo: fieldInfo{
Name: name.Name,
TypeName: fieldTypeName,
BaseType: baseType,
IsOptional: isOptional,
IsComparable: isComparable,
},
fieldType: field.Type,
})
}
}
}
return results
}
// extractTypeParams extracts type parameters from a type spec
// Returns two strings: full params like "[T any]" and names only like "[T]"
func extractTypeParams(typeSpec *ast.TypeSpec) (string, string) {
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
return "", ""
}
var params []string
var names []string
for _, field := range typeSpec.TypeParams.List {
for _, name := range field.Names {
constraint := getTypeName(field.Type)
params = append(params, name.Name+" "+constraint)
names = append(names, name.Name)
}
}
fullParams := "[" + strings.Join(params, ", ") + "]"
nameParams := "[" + strings.Join(names, ", ") + "]"
return fullParams, nameParams
}
// buildTypeParamsMap creates a map of type parameter names to their constraints
// e.g., for "type Box[T any, K comparable]", returns {"T": "any", "K": "comparable"}
func buildTypeParamsMap(typeSpec *ast.TypeSpec) map[string]string {
typeParamsMap := make(map[string]string)
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
return typeParamsMap
}
for _, field := range typeSpec.TypeParams.List {
constraint := getTypeName(field.Type)
for _, name := range field.Names {
typeParamsMap[name.Name] = constraint
}
}
return typeParamsMap
}
// parseFile parses a Go file and extracts structs with lens annotations
func parseFile(filename string) ([]structInfo, string, error) {
fset := token.NewFileSet()
@@ -446,9 +607,27 @@ func parseFile(filename string) ([]structInfo, string, error) {
var fields []fieldInfo
structImports := make(map[string]string)
// Build type parameters map for this struct
typeParamsMap := buildTypeParamsMap(typeSpec)
for _, field := range structType.Fields.List {
if len(field.Names) == 0 {
// Embedded field, skip for now
// Embedded field - promote its fields
embeddedResults := extractEmbeddedFields(field.Type, fileImports, node, typeParamsMap)
for _, embResult := range embeddedResults {
// Extract imports from embedded field's type
fieldImports := make(map[string]string)
extractImports(embResult.fieldType, fieldImports)
// Resolve package names to full import paths
for pkgName := range fieldImports {
if importPath, ok := fileImports[pkgName]; ok {
structImports[importPath] = pkgName
}
}
fields = append(fields, embResult.fieldInfo)
}
continue
}
for _, name := range field.Names {
@@ -473,7 +652,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
// Check if the type is comparable (for non-optional fields)
// For optional fields, we don't need to check since they use LensO
isComparable = isComparableType(field.Type)
isComparable = isComparableType(field.Type, typeParamsMap)
// log.Printf("field %s, type: %v, isComparable: %b\n", name, field.Type, isComparable)
// Extract imports from this field's type
@@ -499,10 +678,13 @@ func parseFile(filename string) ([]structInfo, string, error) {
}
if len(fields) > 0 {
typeParams, typeParamNames := extractTypeParams(typeSpec)
structs = append(structs, structInfo{
Name: typeSpec.Name.Name,
Fields: fields,
Imports: structImports,
Name: typeSpec.Name.Name,
TypeParams: typeParams,
TypeParamNames: typeParamNames,
Fields: fields,
Imports: structImports,
})
}

View File

@@ -247,7 +247,7 @@ func TestIsComparableType(t *testing.T) {
})
require.NotNil(t, fieldType)
result := isComparableType(fieldType)
result := isComparableType(fieldType, map[string]string{})
assert.Equal(t, tt.expected, result)
})
}
@@ -657,8 +657,8 @@ func TestLensTemplates(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "*int", IsOptional: true},
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "*int", IsOptional: true, IsComparable: true},
},
}
@@ -693,10 +693,10 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
s := structInfo{
Name: "ConfigStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "string", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: true}, // pointer
},
}
@@ -710,9 +710,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]")
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]")
assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]")
@@ -755,3 +755,330 @@ func TestLensCommandFlags(t *testing.T) {
assert.True(t, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag")
}
func TestParseFileWithEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Base struct {
ID int
Name string
}
// fp-go:Lens
type Extended struct {
Base
Extra string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Extended struct
extended := structs[0]
assert.Equal(t, "Extended", extended.Name)
assert.Len(t, extended.Fields, 3, "Should have 3 fields: ID, Name (from Base), and Extra")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range extended.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["ID"], "Should have promoted ID field from Base")
assert.True(t, fieldNames["Name"], "Should have promoted Name field from Base")
assert.True(t, fieldNames["Extra"], "Should have Extra field")
}
func TestGenerateLensHelpersWithEmbeddedStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// Base struct to be embedded
type Address struct {
Street string
City string
}
// fp-go:Lens
type Person struct {
Address
Name string
Age int
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "PersonLenses")
assert.Contains(t, contentStr, "MakePersonLenses")
// Check that embedded fields are included
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")
// Check that optional lenses are also generated for embedded fields
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
}
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Metadata struct {
CreatedAt string
UpdatedAt string
}
// fp-go:Lens
type Document struct {
*Metadata
Title string
Content string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Document struct
doc := structs[0]
assert.Equal(t, "Document", doc.Name)
assert.Len(t, doc.Fields, 4, "Should have 4 fields: CreatedAt, UpdatedAt (from *Metadata), Title, and Content")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range doc.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["CreatedAt"], "Should have promoted CreatedAt field from *Metadata")
assert.True(t, fieldNames["UpdatedAt"], "Should have promoted UpdatedAt field from *Metadata")
assert.True(t, fieldNames["Title"], "Should have Title field")
assert.True(t, fieldNames["Content"], "Should have Content field")
}
func TestParseFileWithGenericStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Container[T any] struct {
Value T
Count int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Container struct
container := structs[0]
assert.Equal(t, "Container", container.Name)
assert.Equal(t, "[T any]", container.TypeParams, "Should have type parameter [T any]")
assert.Len(t, container.Fields, 2)
assert.Equal(t, "Value", container.Fields[0].Name)
assert.Equal(t, "T", container.Fields[0].TypeName)
assert.Equal(t, "Count", container.Fields[1].Name)
assert.Equal(t, "int", container.Fields[1].TypeName)
}
func TestParseFileWithMultipleTypeParams(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Pair[K comparable, V any] struct {
Key K
Value V
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Pair struct
pair := structs[0]
assert.Equal(t, "Pair", pair.Name)
assert.Equal(t, "[K comparable, V any]", pair.TypeParams, "Should have type parameters [K comparable, V any]")
assert.Len(t, pair.Fields, 2)
assert.Equal(t, "Key", pair.Fields[0].Name)
assert.Equal(t, "K", pair.Fields[0].TypeName)
assert.Equal(t, "Value", pair.Fields[1].Name)
assert.Equal(t, "V", pair.Fields[1].TypeName)
}
func TestGenerateLensHelpersWithGenericStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type Box[T any] struct {
Content T
Label string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type BoxLenses[T any] struct", "Should have generic BoxLenses type")
assert.Contains(t, contentStr, "type BoxRefLenses[T any] struct", "Should have generic BoxRefLenses type")
assert.Contains(t, contentStr, "func MakeBoxLenses[T any]() BoxLenses[T]", "Should have generic constructor")
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
// Check that fields use the generic type parameter
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")
// Check optional lenses - only for comparable types
// T any is not comparable, so ContentO should NOT be generated
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
// string is comparable, so LabelO should be generated
assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens")
}
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type ComparableBox[T comparable] struct {
Key T
Value string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type ComparableBoxLenses[T comparable] struct", "Should have generic ComparableBoxLenses type")
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict")
// Check that Value field (string, always comparable) also uses MakeLensStrict
assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict")
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable")
}

11
v2/constant/monoid.go Normal file
View File

@@ -0,0 +1,11 @@
package constant
import (
"github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a [M.Monoid] that returns a constant value in all operations
func Monoid[A any](a A) M.Monoid[A] {
return M.MakeMonoid(function.Constant2[A, A](a), a)
}

View File

@@ -53,12 +53,12 @@ import (
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
RIOEH "github.com/IBM/fp-go/v2/context/readerioresult/http"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/http/builder"
H "github.com/IBM/fp-go/v2/http/headers"
LZ "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
// Requester converts an http/builder.Builder into a ReaderIOResult that produces HTTP requests.
@@ -143,10 +143,10 @@ func Requester(builder *R.Builder) RIOEH.Requester {
return F.Pipe5(
builder.GetBody(),
O.Fold(LZ.Of(E.Of[error](withoutBody)), E.Map[error](withBody)),
E.Ap[func(string) RIOE.ReaderIOResult[*http.Request]](builder.GetTargetURL()),
E.Flap[error, RIOE.ReaderIOResult[*http.Request]](builder.GetMethod()),
E.GetOrElse(RIOE.Left[*http.Request]),
O.Fold(LZ.Of(result.Of(withoutBody)), result.Map(withBody)),
result.Ap[RIOE.Kleisli[string, *http.Request]](builder.GetTargetURL()),
result.Flap[RIOE.ReaderIOResult[*http.Request]](builder.GetMethod()),
result.GetOrElse(RIOE.Left[*http.Request]),
RIOE.Map(func(req *http.Request) *http.Request {
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
return req

View File

@@ -180,6 +180,11 @@ func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIORe
return RIOR.MonadChainFirst(ma, f)
}
//go:inline
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTap(ma, f)
}
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
// This is the curried version of [MonadChainFirst].
//
@@ -193,6 +198,11 @@ func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirst(f)
}
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.Tap(f)
}
// Of creates a [ReaderIOResult] that always succeeds with the given value.
// This is the same as [Right] and represents the monadic return operation.
//
@@ -403,6 +413,11 @@ func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B])
return RIOR.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
return RIOR.MonadTapEitherK(ma, f)
}
// ChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
// This is the curried version of [MonadChainFirstEitherK].
//
@@ -416,6 +431,11 @@ func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.ChainFirstEitherK[context.Context](f)
}
//go:inline
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.TapEitherK[context.Context](f)
}
// ChainOptionK chains a function that returns an [Option] into a [ReaderIOResult] computation.
// If the Option is None, the provided error function is called.
//
@@ -538,6 +558,11 @@ func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderI
return RIOR.MonadChainFirstIOK(ma, f)
}
//go:inline
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
return RIOR.MonadTapIOK(ma, f)
}
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// This is the curried version of [MonadChainFirstIOK].
//
@@ -551,6 +576,11 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.ChainFirstIOK[context.Context](f)
}
//go:inline
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.TapIOK[context.Context](f)
}
// ChainIOEitherK chains a function that returns an [IOResult] into a [ReaderIOResult] computation.
// This is useful for integrating IOResult-returning functions into ReaderIOResult workflows.
//
@@ -628,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.
@@ -782,11 +812,21 @@ func MonadChainFirstReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[con
return RIOR.MonadChainFirstReaderK(ma, f)
}
//go:inline
func MonadTapReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderK(ma, f)
}
//go:inline
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderK(f)
}
//go:inline
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderK(f)
}
//go:inline
func MonadChainReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderResultK(ma, f)
@@ -802,11 +842,21 @@ func MonadChainFirstReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult
return RIOR.MonadChainFirstReaderResultK(ma, f)
}
//go:inline
func MonadTapReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderResultK(ma, f)
}
//go:inline
func ChainFirstReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderResultK(f)
}
//go:inline
func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapReaderResultK(f)
}
//go:inline
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderIOK(ma, f)
@@ -822,11 +872,21 @@ func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli
return RIOR.MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(f)
}
//go:inline
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderIOK(f)
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
@@ -837,7 +897,64 @@ func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kl
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type. If the input is a Right value, it passes through unchanged.
//
//go:inline
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
return RIOR.MonadChainLeft(fa, f)
}
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
return RIOR.ChainLeft(f)
}
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
// but always returns the original Left error regardless of what f returns (Left or Right).
// If the input is a Right value, it passes through unchanged without calling f.
//
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
//
//go:inline
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstLeft(ma, f)
}
//go:inline
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadTapLeft(ma, f)
}
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
//
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
// ensuring the error path is preserved.
//
//go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.ChainFirstLeft[A](f)
}
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](f)
}

View File

@@ -24,6 +24,7 @@ import (
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither"
N "github.com/IBM/fp-go/v2/number"
)
var (
@@ -37,21 +38,21 @@ var (
// Benchmark core constructors
func BenchmarkLeft(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Left[int](benchErr)
}
}
func BenchmarkRight(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Right(42)
}
}
func BenchmarkOf(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Of(42)
}
}
@@ -60,7 +61,7 @@ func BenchmarkFromEither_Right(b *testing.B) {
either := E.Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromEither(either)
}
}
@@ -69,7 +70,7 @@ func BenchmarkFromEither_Left(b *testing.B) {
either := E.Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromEither(either)
}
}
@@ -77,7 +78,7 @@ func BenchmarkFromEither_Left(b *testing.B) {
func BenchmarkFromIO(b *testing.B) {
io := func() int { return 42 }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromIO(io)
}
}
@@ -85,7 +86,7 @@ func BenchmarkFromIO(b *testing.B) {
func BenchmarkFromIOEither_Right(b *testing.B) {
ioe := IOE.Of[error](42)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromIOEither(ioe)
}
}
@@ -93,7 +94,7 @@ func BenchmarkFromIOEither_Right(b *testing.B) {
func BenchmarkFromIOEither_Left(b *testing.B) {
ioe := IOE.Left[int](benchErr)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromIOEither(ioe)
}
}
@@ -103,7 +104,7 @@ func BenchmarkExecute_Right(b *testing.B) {
rioe := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -112,7 +113,7 @@ func BenchmarkExecute_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -123,7 +124,7 @@ func BenchmarkExecute_WithContext(b *testing.B) {
defer cancel()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(ctx)()
}
}
@@ -131,40 +132,40 @@ func BenchmarkExecute_WithContext(b *testing.B) {
// Benchmark functor operations
func BenchmarkMonadMap_Right(b *testing.B) {
rioe := Right(42)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadMap(rioe, mapper)
}
}
func BenchmarkMonadMap_Left(b *testing.B) {
rioe := Left[int](benchErr)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadMap(rioe, mapper)
}
}
func BenchmarkMap_Right(b *testing.B) {
rioe := Right(42)
mapper := Map(func(a int) int { return a * 2 })
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = mapper(rioe)
}
}
func BenchmarkMap_Left(b *testing.B) {
rioe := Left[int](benchErr)
mapper := Map(func(a int) int { return a * 2 })
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = mapper(rioe)
}
}
@@ -174,7 +175,7 @@ func BenchmarkMapTo_Right(b *testing.B) {
mapper := MapTo[int](99)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = mapper(rioe)
}
}
@@ -185,7 +186,7 @@ func BenchmarkMonadChain_Right(b *testing.B) {
chainer := func(a int) ReaderIOResult[int] { return Right(a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadChain(rioe, chainer)
}
}
@@ -195,7 +196,7 @@ func BenchmarkMonadChain_Left(b *testing.B) {
chainer := func(a int) ReaderIOResult[int] { return Right(a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadChain(rioe, chainer)
}
}
@@ -205,7 +206,7 @@ func BenchmarkChain_Right(b *testing.B) {
chainer := Chain(func(a int) ReaderIOResult[int] { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -215,7 +216,7 @@ func BenchmarkChain_Left(b *testing.B) {
chainer := Chain(func(a int) ReaderIOResult[int] { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -225,7 +226,7 @@ func BenchmarkChainFirst_Right(b *testing.B) {
chainer := ChainFirst(func(a int) ReaderIOResult[string] { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -235,7 +236,7 @@ func BenchmarkChainFirst_Left(b *testing.B) {
chainer := ChainFirst(func(a int) ReaderIOResult[string] { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -244,7 +245,7 @@ func BenchmarkFlatten_Right(b *testing.B) {
nested := Right(Right(42))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Flatten(nested)
}
}
@@ -253,28 +254,28 @@ func BenchmarkFlatten_Left(b *testing.B) {
nested := Left[ReaderIOResult[int]](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Flatten(nested)
}
}
// Benchmark applicative operations
func BenchmarkMonadApSeq_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApSeq(fab, fa)
}
}
func BenchmarkMonadApSeq_RightLeft(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApSeq(fab, fa)
}
}
@@ -284,27 +285,27 @@ func BenchmarkMonadApSeq_LeftRight(b *testing.B) {
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApSeq(fab, fa)
}
}
func BenchmarkMonadApPar_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApPar(fab, fa)
}
}
func BenchmarkMonadApPar_RightLeft(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApPar(fab, fa)
}
}
@@ -314,30 +315,30 @@ func BenchmarkMonadApPar_LeftRight(b *testing.B) {
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApPar(fab, fa)
}
}
// Benchmark execution of applicative operations
func BenchmarkExecuteApSeq_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
rioe := MonadApSeq(fab, fa)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
func BenchmarkExecuteApPar_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
rioe := MonadApPar(fab, fa)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -348,7 +349,7 @@ func BenchmarkAlt_RightRight(b *testing.B) {
alternative := Alt(func() ReaderIOResult[int] { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = alternative(rioe)
}
}
@@ -358,7 +359,7 @@ func BenchmarkAlt_LeftRight(b *testing.B) {
alternative := Alt(func() ReaderIOResult[int] { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = alternative(rioe)
}
}
@@ -368,7 +369,7 @@ func BenchmarkOrElse_Right(b *testing.B) {
recover := OrElse(func(e error) ReaderIOResult[int] { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = recover(rioe)
}
}
@@ -378,7 +379,7 @@ func BenchmarkOrElse_Left(b *testing.B) {
recover := OrElse(func(e error) ReaderIOResult[int] { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = recover(rioe)
}
}
@@ -389,7 +390,7 @@ func BenchmarkChainEitherK_Right(b *testing.B) {
chainer := ChainEitherK(func(a int) Either[int] { return E.Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -399,7 +400,7 @@ func BenchmarkChainEitherK_Left(b *testing.B) {
chainer := ChainEitherK(func(a int) Either[int] { return E.Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -409,7 +410,7 @@ func BenchmarkChainIOK_Right(b *testing.B) {
chainer := ChainIOK(func(a int) func() int { return func() int { return a * 2 } })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -419,7 +420,7 @@ func BenchmarkChainIOK_Left(b *testing.B) {
chainer := ChainIOK(func(a int) func() int { return func() int { return a * 2 } })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -429,7 +430,7 @@ func BenchmarkChainIOEitherK_Right(b *testing.B) {
chainer := ChainIOEitherK(func(a int) IOEither[int] { return IOE.Of[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -439,7 +440,7 @@ func BenchmarkChainIOEitherK_Left(b *testing.B) {
chainer := ChainIOEitherK(func(a int) IOEither[int] { return IOE.Of[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -447,7 +448,7 @@ func BenchmarkChainIOEitherK_Left(b *testing.B) {
// Benchmark context operations
func BenchmarkAsk(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Ask()
}
}
@@ -455,7 +456,7 @@ func BenchmarkAsk(b *testing.B) {
func BenchmarkDefer(b *testing.B) {
gen := func() ReaderIOResult[int] { return Right(42) }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Defer(gen)
}
}
@@ -463,7 +464,7 @@ func BenchmarkDefer(b *testing.B) {
func BenchmarkMemoize(b *testing.B) {
rioe := Right(42)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Memoize(rioe)
}
}
@@ -472,14 +473,14 @@ func BenchmarkMemoize(b *testing.B) {
func BenchmarkDelay_Construction(b *testing.B) {
rioe := Right(42)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Delay[int](time.Millisecond)(rioe)
}
}
func BenchmarkTimer_Construction(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Timer(time.Millisecond)
}
}
@@ -490,7 +491,7 @@ func BenchmarkTryCatch_Success(b *testing.B) {
return func() (int, error) { return 42, nil }
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = TryCatch(f)
}
}
@@ -500,7 +501,7 @@ func BenchmarkTryCatch_Error(b *testing.B) {
return func() (int, error) { return 0, benchErr }
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = TryCatch(f)
}
}
@@ -512,7 +513,7 @@ func BenchmarkExecuteTryCatch_Success(b *testing.B) {
rioe := TryCatch(f)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -524,7 +525,7 @@ func BenchmarkExecuteTryCatch_Error(b *testing.B) {
rioe := TryCatch(f)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -534,10 +535,10 @@ func BenchmarkPipeline_Map_Right(b *testing.B) {
rioe := Right(21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -546,10 +547,10 @@ func BenchmarkPipeline_Map_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -558,7 +559,7 @@ func BenchmarkPipeline_Chain_Right(b *testing.B) {
rioe := Right(21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Chain(func(x int) ReaderIOResult[int] { return Right(x * 2) }),
@@ -570,7 +571,7 @@ func BenchmarkPipeline_Chain_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Chain(func(x int) ReaderIOResult[int] { return Right(x * 2) }),
@@ -582,12 +583,12 @@ func BenchmarkPipeline_Complex_Right(b *testing.B) {
rioe := Right(10)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe3(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -596,12 +597,12 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe3(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -609,13 +610,13 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
rioe := F.Pipe3(
Right(10),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -624,7 +625,7 @@ func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
func BenchmarkDo(b *testing.B) {
type State struct{ value int }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Do(State{})
}
}
@@ -642,7 +643,7 @@ func BenchmarkBind_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = binder(initial)
}
}
@@ -658,7 +659,7 @@ func BenchmarkLet_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = letter(initial)
}
}
@@ -674,7 +675,7 @@ func BenchmarkApS_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = aps(initial)
}
}
@@ -687,7 +688,7 @@ func BenchmarkTraverseArray_Empty(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -699,7 +700,7 @@ func BenchmarkTraverseArray_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -714,7 +715,7 @@ func BenchmarkTraverseArray_Medium(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -726,7 +727,7 @@ func BenchmarkTraverseArraySeq_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -738,7 +739,7 @@ func BenchmarkTraverseArrayPar_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -751,7 +752,7 @@ func BenchmarkSequenceArray_Small(b *testing.B) {
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = SequenceArray(arr)
}
}
@@ -763,7 +764,7 @@ func BenchmarkExecuteTraverseArray_Small(b *testing.B) {
})(arr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = rioe(benchCtx)()
}
}
@@ -775,7 +776,7 @@ func BenchmarkExecuteTraverseArraySeq_Small(b *testing.B) {
})(arr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = rioe(benchCtx)()
}
}
@@ -787,7 +788,7 @@ func BenchmarkExecuteTraverseArrayPar_Small(b *testing.B) {
})(arr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = rioe(benchCtx)()
}
}
@@ -800,7 +801,7 @@ func BenchmarkTraverseRecord_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(rec)
}
}
@@ -813,7 +814,7 @@ func BenchmarkSequenceRecord_Small(b *testing.B) {
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = SequenceRecord(rec)
}
}
@@ -826,7 +827,7 @@ func BenchmarkWithResource_Success(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = WithResource[int](acquire, release)(body)
}
}
@@ -839,7 +840,7 @@ func BenchmarkExecuteWithResource_Success(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -852,7 +853,7 @@ func BenchmarkExecuteWithResource_ErrorInBody(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -865,13 +866,13 @@ func BenchmarkExecute_CanceledContext(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(ctx)()
}
}
func BenchmarkExecuteApPar_CanceledContext(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
rioe := MonadApPar(fab, fa)
ctx, cancel := context.WithCancel(benchCtx)
@@ -879,7 +880,7 @@ func BenchmarkExecuteApPar_CanceledContext(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(ctx)()
}
}

View File

@@ -26,6 +26,7 @@ import (
IOG "github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither"
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
@@ -77,27 +78,27 @@ func TestOf(t *testing.T) {
func TestMonadMap(t *testing.T) {
t.Run("Map over Right", func(t *testing.T) {
result := MonadMap(Of(5), func(x int) int { return x * 2 })
result := MonadMap(Of(5), N.Mul(2))
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("Map over Left", func(t *testing.T) {
err := errors.New("test error")
result := MonadMap(Left[int](err), func(x int) int { return x * 2 })
result := MonadMap(Left[int](err), N.Mul(2))
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestMap(t *testing.T) {
t.Run("Map with success", func(t *testing.T) {
mapper := Map(func(x int) int { return x * 2 })
mapper := Map(N.Mul(2))
result := mapper(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("Map with error", func(t *testing.T) {
err := errors.New("test error")
mapper := Map(func(x int) int { return x * 2 })
mapper := Map(N.Mul(2))
result := mapper(Left[int](err))
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
@@ -182,7 +183,7 @@ func TestChainFirst(t *testing.T) {
func TestMonadApSeq(t *testing.T) {
t.Run("ApSeq with success", func(t *testing.T) {
fab := Of(func(x int) int { return x * 2 })
fab := Of(N.Mul(2))
fa := Of(5)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
@@ -198,7 +199,7 @@ func TestMonadApSeq(t *testing.T) {
t.Run("ApSeq with error in value", func(t *testing.T) {
err := errors.New("test error")
fab := Of(func(x int) int { return x * 2 })
fab := Of(N.Mul(2))
fa := Left[int](err)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Left[int](err), result(context.Background())())
@@ -207,7 +208,7 @@ func TestMonadApSeq(t *testing.T) {
func TestApSeq(t *testing.T) {
fa := Of(5)
fab := Of(func(x int) int { return x * 2 })
fab := Of(N.Mul(2))
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
@@ -215,7 +216,7 @@ func TestApSeq(t *testing.T) {
func TestApPar(t *testing.T) {
t.Run("ApPar with success", func(t *testing.T) {
fa := Of(5)
fab := Of(func(x int) int { return x * 2 })
fab := Of(N.Mul(2))
result := MonadApPar(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
@@ -224,7 +225,7 @@ func TestApPar(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
fa := Of(5)
fab := Of(func(x int) int { return x * 2 })
fab := Of(N.Mul(2))
result := MonadApPar(fab, fa)
res := result(ctx)()
assert.True(t, E.IsLeft(res))
@@ -587,14 +588,14 @@ func TestFlatten(t *testing.T) {
}
func TestMonadFlap(t *testing.T) {
fab := Of(func(x int) int { return x * 2 })
fab := Of(N.Mul(2))
result := MonadFlap(fab, 5)
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestFlap(t *testing.T) {
flapper := Flap[int](5)
result := flapper(Of(func(x int) int { return x * 2 }))
result := flapper(Of(N.Mul(2)))
assert.Equal(t, E.Right[error](10), result(context.Background())())
}

View File

@@ -284,3 +284,160 @@ func TestWithResourceErrorInRelease(t *testing.T) {
assert.Equal(t, 0, countRelease)
assert.Equal(t, E.Left[int](err), res)
}
func TestMonadChainFirstLeft(t *testing.T) {
ctx := context.Background()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
sideEffectCalled := false
originalErr := fmt.Errorf("original error")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
sideEffectCalled = true
return Left[int](fmt.Errorf("new error")) // This error is ignored
},
)
actualResult := result(ctx)()
assert.True(t, sideEffectCalled)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var capturedError error
originalErr := fmt.Errorf("validation failed")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
capturedError = e
return Right(999) // This Right value is ignored
},
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, capturedError)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
sideEffectCalled := false
result := MonadChainFirstLeft(
Right(42),
func(e error) ReaderIOResult[int] {
sideEffectCalled = true
return Left[int](fmt.Errorf("should not be called"))
},
)
assert.False(t, sideEffectCalled)
assert.Equal(t, E.Right[error](42), result(ctx)())
})
// Test that side effects are executed but original error is always preserved
t.Run("Side effects executed but original error preserved", func(t *testing.T) {
effectCount := 0
originalErr := fmt.Errorf("original error")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
effectCount++
// Try to return Right, but original Left should still be returned
return Right(999)
},
)
actualResult := result(ctx)()
assert.Equal(t, 1, effectCount)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
}
func TestChainFirstLeft(t *testing.T) {
ctx := context.Background()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
var captured error
originalErr := fmt.Errorf("test error")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
captured = e
return Left[int](fmt.Errorf("ignored error"))
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, captured)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var captured error
originalErr := fmt.Errorf("test error")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
captured = e
return Right(42) // This Right is ignored
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, captured)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
called := false
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
called = true
return Right(0)
})
result := F.Pipe1(
Right(100),
chainFn,
)
assert.False(t, called)
assert.Equal(t, E.Right[error](100), result(ctx)())
})
// Test that original error is always preserved regardless of what f returns
t.Run("Original error always preserved", func(t *testing.T) {
originalErr := fmt.Errorf("original")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
// Try to return Right, but original Left should still be returned
return Right(999)
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
assert.Equal(t, E.Left[int](originalErr), result(ctx)())
})
// Test logging with Left preservation
t.Run("Logging with Left preservation", func(t *testing.T) {
errorLog := []string{}
originalErr := fmt.Errorf("step1")
logError := ChainFirstLeft[string](func(e error) ReaderIOResult[string] {
errorLog = append(errorLog, "Logged: "+e.Error())
return Left[string](fmt.Errorf("log entry")) // This is ignored
})
result := F.Pipe2(
Left[string](originalErr),
logError,
ChainLeft(func(e error) ReaderIOResult[string] {
return Left[string](fmt.Errorf("step2"))
}),
)
actualResult := result(ctx)()
assert.Equal(t, []string{"Logged: step1"}, errorLog)
assert.Equal(t, E.Left[string](fmt.Errorf("step2")), actualResult)
})
}

View File

@@ -16,8 +16,8 @@
package readerioresult
import (
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/internal/record"
)
@@ -29,7 +29,7 @@ import (
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
return array.Traverse(
Of[[]B],
Map[[]B, func(B) []B],
Ap[[]B, B],
@@ -46,7 +46,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
return array.TraverseWithIndex(
Of[[]B],
Map[[]B, func(B) []B],
Ap[[]B, B],
@@ -135,22 +135,20 @@ func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
return array.Traverse(
Of[[]B],
Map[[]B, func(B) []B],
ApSeq[[]B, B],
f,
)
}
// TraverseArrayWithIndexSeq uses transforms an array [[]A] into [[]ReaderIOResult[B]] and then resolves that into a [ReaderIOResult[[]B]]
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
return array.TraverseWithIndex(
Of[[]B],
Map[[]B, func(B) []B],
ApSeq[[]B, B],
f,
)
}
@@ -230,22 +228,20 @@ func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
return array.Traverse(
Of[[]B],
Map[[]B, func(B) []B],
ApPar[[]B, B],
f,
)
}
// TraverseArrayWithIndexPar uses transforms an array [[]A] into [[]ReaderIOResult[B]] and then resolves that into a [ReaderIOResult[[]B]]
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
return array.TraverseWithIndex(
Of[[]B],
Map[[]B, func(B) []B],
ApPar[[]B, B],
f,
)
}

View File

@@ -0,0 +1,209 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Do starts a do-notation chain for building computations in a fluent style.
// This is typically used with Bind, Let, and other combinators to compose
// stateful, context-dependent computations that can fail.
//
// Example:
//
// type State struct {
// name string
// age int
// }
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(...),
// statereaderioresult.Let(...),
// )
//
//go:inline
func Do[ST, A any](
empty A,
) StateReaderIOResult[ST, A] {
return Of[ST](empty)
}
// Bind executes a computation and binds its result to a field in the accumulator state.
// This is used in do-notation to sequence dependent computations.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(
// func(name string) func(State) State {
// return func(s State) State { return State{name: name, age: s.age} }
// },
// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState]("John")
// },
// ),
// )
//
//go:inline
func Bind[ST, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[ST, S1, T],
) Operator[ST, S1, S2] {
return C.Bind(
Chain[ST, S1, S2],
Map[ST, T, S2],
setter,
f,
)
}
// Let computes a derived value and binds it to a field in the accumulator state.
// Unlike Bind, this does not execute a monadic computation, just a pure function.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{age: 25}),
// statereaderioresult.Let(
// func(isAdult bool) func(State) State {
// return func(s State) State { return State{age: s.age, isAdult: isAdult} }
// },
// func(s State) bool { return s.age >= 18 },
// ),
// )
//
//go:inline
func Let[ST, S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
) Operator[ST, S1, S2] {
return F.Let(
Map[ST, S1, S2],
key,
f,
)
}
// LetTo binds a constant value to a field in the accumulator state.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.LetTo(
// func(status string) func(State) State {
// return func(s State) State { return State{...s, status: status} }
// },
// "active",
// ),
// )
//
//go:inline
func LetTo[ST, S1, S2, T any](
key func(T) func(S1) S2,
b T,
) Operator[ST, S1, S2] {
return F.LetTo(
Map[ST, S1, S2],
key,
b,
)
}
// BindTo wraps a value in a simple constructor, typically used to start a do-notation chain
// after getting an initial value.
//
// Example:
//
// result := function.Pipe2(
// statereaderioresult.Of[AppState](42),
// statereaderioresult.BindTo[AppState](func(x int) State { return State{value: x} }),
// )
//
//go:inline
func BindTo[ST, S1, T any](
setter func(T) S1,
) Operator[ST, T, S1] {
return C.BindTo(
Map[ST, T, S1],
setter,
)
}
// ApS applies a computation in sequence and binds the result to a field.
// This is the applicative version of Bind.
//
//go:inline
func ApS[ST, S1, S2, T any](
setter func(T) func(S1) S2,
fa StateReaderIOResult[ST, T],
) Operator[ST, S1, S2] {
return A.ApS(
Ap[S2, ST, T],
Map[ST, S1, func(T) S2],
setter,
fa,
)
}
// ApSL is a lens-based variant of ApS for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func ApSL[ST, S, T any](
lens Lens[S, T],
fa StateReaderIOResult[ST, T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return ApS(lens.Set, fa)
}
// BindL is a lens-based variant of Bind for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func BindL[ST, S, T any](
lens Lens[S, T],
f Kleisli[ST, T, T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL is a lens-based variant of Let for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func LetL[ST, S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Endomorphism[StateReaderIOResult[ST, S]] {
return Let[ST](lens.Set, function.Flow2(lens.Get, f))
}
// LetToL is a lens-based variant of LetTo for working with nested structures.
// It uses a lens to focus on a specific field in the state.
//
//go:inline
func LetToL[ST, S, T any](
lens Lens[S, T],
b T,
) Endomorphism[StateReaderIOResult[ST, S]] {
return LetTo[ST](lens.Set, b)
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package statereaderioresult provides a functional programming abstraction that combines
// four powerful concepts: State, Reader, IO, and Result monads, specialized for Go's context.Context.
//
// # StateReaderIOResult
//
// StateReaderIOResult[S, A] represents a computation that:
// - Manages state of type S (State)
// - Depends on a [context.Context] (Reader)
// - Performs side effects (IO)
// - Can fail with an [error] or succeed with a value of type A (Result)
//
// This is a specialization of StateReaderIOEither with:
// - Context type fixed to [context.Context]
// - Error type fixed to [error]
//
// This is particularly useful for:
// - Stateful computations with dependency injection using Go contexts
// - Error handling in effectful computations with state
// - Composing operations that need access to context, manage state, and can fail
// - Working with Go's standard context patterns (cancellation, deadlines, values)
//
// # Core Operations
//
// Construction:
// - Of/Right: Create a successful computation with a value
// - Left: Create a failed computation with an error
// - FromState: Lift a State into StateReaderIOResult
// - FromIO: Lift an IO into StateReaderIOResult
// - FromResult: Lift a Result into StateReaderIOResult
// - FromIOResult: Lift an IOResult into StateReaderIOResult
// - FromReaderIOResult: Lift a ReaderIOResult into StateReaderIOResult
//
// Transformation:
// - Map: Transform the success value
// - Chain: Sequence dependent computations (monadic bind)
// - Flatten: Flatten nested StateReaderIOResult
//
// Combination:
// - Ap: Apply a function in a context to a value in a context
//
// Context Access:
// - Asks: Get a value derived from the context
// - Local: Run a computation with a modified context
//
// Kleisli Arrows:
// - FromResultK: Lift a Result-returning function to a Kleisli arrow
// - FromIOK: Lift an IO-returning function to a Kleisli arrow
// - FromIOResultK: Lift an IOResult-returning function to a Kleisli arrow
// - FromReaderIOResultK: Lift a ReaderIOResult-returning function to a Kleisli arrow
// - ChainResultK: Chain with a Result-returning function
// - ChainIOResultK: Chain with an IOResult-returning function
// - ChainReaderIOResultK: Chain with a ReaderIOResult-returning function
//
// Do Notation (Monadic Composition):
// - Do: Start a do-notation chain
// - Bind: Bind a value from a computation
// - BindTo: Bind a value to a simple constructor
// - Let: Compute a derived value
// - LetTo: Set a constant value
// - ApS: Apply in sequence (for applicative composition)
// - BindL/ApSL/LetL/LetToL: Lens-based variants for working with nested structures
//
// # Example Usage
//
// type AppState struct {
// RequestCount int
// LastError error
// }
//
// // A computation that manages state, depends on context, performs IO, and can fail
// func processRequest(data string) statereaderioresult.StateReaderIOResult[AppState, string] {
// return func(state AppState) readerioresult.ReaderIOResult[pair.Pair[AppState, string]] {
// return func(ctx context.Context) ioresult.IOResult[pair.Pair[AppState, string]] {
// return func() result.Result[pair.Pair[AppState, string]] {
// // Check context for cancellation
// if ctx.Err() != nil {
// return result.Error[pair.Pair[AppState, string]](ctx.Err())
// }
// // Update state
// newState := AppState{RequestCount: state.RequestCount + 1}
// // Perform IO operations
// return result.Of(pair.MakePair(newState, "processed: " + data))
// }
// }
// }
// }
//
// // Compose operations using do-notation
// result := function.Pipe3(
// statereaderioresult.Do[AppState](State{}),
// statereaderioresult.Bind(
// func(result string) func(State) State { return func(s State) State { return State{result: result} } },
// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] {
// return processRequest(s.input)
// },
// ),
// statereaderioresult.Map[AppState](func(s State) string { return s.result }),
// )
//
// // Execute with initial state and context
// initialState := AppState{RequestCount: 0}
// ctx := context.Background()
// outcome := result(initialState)(ctx)() // Returns result.Result[pair.Pair[AppState, string]]
//
// # Context Integration
//
// This package is designed to work seamlessly with Go's context.Context:
//
// // Using context values
// getUserID := statereaderioresult.Asks[AppState, string](func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] {
// userID, ok := ctx.Value("userID").(string)
// if !ok {
// return statereaderioresult.Left[AppState, string](errors.New("missing userID"))
// }
// return statereaderioresult.Of[AppState](userID)
// })
//
// // Using context cancellation
// withTimeout := statereaderioresult.Local[AppState, string](func(ctx context.Context) context.Context {
// ctx, _ = context.WithTimeout(ctx, 5*time.Second)
// return ctx
// })
//
// # Monad Laws
//
// StateReaderIOResult satisfies the monad laws:
// - Left Identity: Of(a) >>= f ≡ f(a)
// - Right Identity: m >>= Of ≡ m
// - Associativity: (m >>= f) >>= g ≡ m >>= (x => f(x) >>= g)
//
// These laws are verified in the testing subpackage.
package statereaderioresult

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// Eq implements the equals predicate for values contained in the [StateReaderIOResult] monad
func Eq[S, A any](eqr eq.Eq[ReaderIOResult[Pair[S, A]]]) func(S) eq.Eq[StateReaderIOResult[S, A]] {
return func(s S) eq.Eq[StateReaderIOResult[S, A]] {
return eq.FromEquals(func(l, r StateReaderIOResult[S, A]) bool {
return eqr.Equals(l(s), r(s))
})
}
}
// FromStrictEquals constructs an [eq.Eq] from the canonical comparison function
func FromStrictEquals[S comparable, A comparable]() func(context.Context) func(S) eq.Eq[StateReaderIOResult[S, A]] {
return function.Flow2(
RIOR.FromStrictEquals[context.Context, Pair[S, A]](),
Eq[S, A],
)
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import (
"github.com/IBM/fp-go/v2/internal/applicative"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/monad"
"github.com/IBM/fp-go/v2/internal/pointed"
)
type stateReaderIOResultPointed[
S, A any,
] struct{}
type stateReaderIOResultFunctor[
S, A, B any,
] struct{}
type stateReaderIOResultApplicative[
S, A, B any,
] struct{}
type stateReaderIOResultMonad[
S, A, B any,
] struct{}
func (o *stateReaderIOResultPointed[S, A]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultMonad[S, A, B]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Of(a A) StateReaderIOResult[S, A] {
return Of[S](a)
}
func (o *stateReaderIOResultMonad[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultFunctor[S, A, B]) Map(f func(A) B) Operator[S, A, B] {
return Map[S](f)
}
func (o *stateReaderIOResultMonad[S, A, B]) Chain(f Kleisli[S, A, B]) Operator[S, A, B] {
return Chain(f)
}
func (o *stateReaderIOResultMonad[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return Ap[B](fa)
}
func (o *stateReaderIOResultApplicative[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return Ap[B](fa)
}
// Pointed implements the [pointed.Pointed] operations for [StateReaderIOResult]
func Pointed[
S, A any,
]() pointed.Pointed[A, StateReaderIOResult[S, A]] {
return &stateReaderIOResultPointed[S, A]{}
}
// Functor implements the [functor.Functor] operations for [StateReaderIOResult]
func Functor[
S, A, B any,
]() functor.Functor[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B]] {
return &stateReaderIOResultFunctor[S, A, B]{}
}
// Applicative implements the [applicative.Applicative] operations for [StateReaderIOResult]
func Applicative[
S, A, B any,
]() applicative.Applicative[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] {
return &stateReaderIOResultApplicative[S, A, B]{}
}
// Monad implements the [monad.Monad] operations for [StateReaderIOResult]
func Monad[
S, A, B any,
]() monad.Monad[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] {
return &stateReaderIOResultMonad[S, A, B]{}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import "github.com/IBM/fp-go/v2/statereaderioeither"
// WithResource constructs a function that creates a resource with state management, operates on it,
// and then releases the resource. This ensures proper resource cleanup even in the presence of errors,
// following the Resource Acquisition Is Initialization (RAII) pattern.
//
// The state is threaded through all operations: resource creation, usage, and release.
//
// The resource lifecycle with state management is:
// 1. onCreate: Acquires the resource (may modify state)
// 2. use: Operates on the resource with current state (provided as argument to the returned function)
// 3. onRelease: Releases the resource with current state (called regardless of success or failure)
//
// Type parameters:
// - A: The type of the result produced by using the resource
// - S: The state type that is threaded through all operations
// - RES: The resource type
// - ANY: The type returned by the release function (typically ignored)
//
// Parameters:
// - onCreate: A stateful computation that acquires the resource
// - onRelease: A stateful function that releases the resource, called with the resource and current state,
// executed regardless of errors
//
// Returns:
//
// A function that takes a resource-using function and returns a StateReaderIOResult that manages
// the resource lifecycle with state
//
// Example:
//
// type AppState struct {
// openFiles int
// }
//
// // Resource creation that updates state
// openFile := func(filename string) StateReaderIOResult[AppState, *File] {
// return func(state AppState) ReaderIOResult[Pair[AppState, *File]] {
// return func(ctx context.Context) IOResult[Pair[AppState, *File]] {
// return func() Result[Pair[AppState, *File]] {
// file, err := os.Open(filename)
// if err != nil {
// return result.Error[Pair[AppState, *File]](err)
// }
// newState := AppState{openFiles: state.openFiles + 1}
// return result.Of(pair.MakePair(newState, file))
// }
// }
// }
// }
//
// // Resource release that updates state
// closeFile := func(f *File) StateReaderIOResult[AppState, int] {
// return func(state AppState) ReaderIOResult[Pair[AppState, int]] {
// return func(ctx context.Context) IOResult[Pair[AppState, int]] {
// return func() Result[Pair[AppState, int]] {
// f.Close()
// newState := AppState{openFiles: state.openFiles - 1}
// return result.Of(pair.MakePair(newState, 0))
// }
// }
// }
// }
//
// // Use the resource with automatic cleanup
// withFile := WithResource(
// openFile("data.txt"),
// closeFile,
// )
//
// result := withFile(func(f *File) StateReaderIOResult[AppState, string] {
// return readContent(f) // File will be closed automatically
// })
//
// // Execute the computation
// initialState := AppState{openFiles: 0}
// ctx := context.Background()
// outcome := result(initialState)(ctx)()
func WithResource[A, S, RES, ANY any](
onCreate StateReaderIOResult[S, RES],
onRelease Kleisli[S, RES, ANY],
) Kleisli[S, Kleisli[S, RES, A], A] {
return statereaderioeither.WithResource[A](onCreate, onRelease)
}

View File

@@ -0,0 +1,415 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import (
"context"
"errors"
"testing"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// resourceState tracks the lifecycle of resources for testing
type resourceState struct {
resourcesCreated int
resourcesReleased int
lastError error
}
// mockResource represents a test resource
type mockResource struct {
id int
isValid bool
}
// TestWithResourceSuccess tests successful resource creation, usage, and release
func TestWithResourceSuccess(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
// Create a resource
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
// Release a resource
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Use the resource
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, string]] {
return func(ctx context.Context) IOResult[Pair[resourceState, string]] {
return func() Result[Pair[resourceState, string]] {
result := "used resource " + string(rune(res.id+'0'))
return RES.Of(P.MakePair(s, result))
}
}
}
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, string]) Pair[resourceState, string] {
state := P.Head(p)
value := P.Tail(p)
// Verify state updates
// Note: Final state comes from the use function, not the release function
// onCreate: 0->1, use: sees 1 (doesn't modify), release: sees 1 and increments released
// The final state is from use function which saw state=1 with resourcesReleased=0
assert.Equal(t, 1, state.resourcesCreated, "Resource should be created once")
assert.Equal(t, 0, state.resourcesReleased, "Final state is from use function, before release")
// Verify result
assert.Equal(t, "used resource 1", value)
return p
})(outcome)
}
// TestWithResourceErrorInCreate tests error handling when resource creation fails
func TestWithResourceErrorInCreate(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
createError := errors.New("failed to create resource")
// onCreate that fails
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
return RES.Left[Pair[resourceState, mockResource]](createError)
}
}
}
// Release should not be called if onCreate fails
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
t.Error("onRelease should not be called when onCreate fails")
return RES.Of(P.MakePair(s, 0))
}
}
}
}
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Of[resourceState]("should not reach here")
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
RES.Fold(
func(err error) bool {
assert.Equal(t, createError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}
// TestWithResourceErrorInUse tests that resources are released even when usage fails
func TestWithResourceErrorInUse(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
useError := errors.New("failed to use resource")
// Create a resource
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: 1, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
releaseWasCalled := false
// Release should still be called even if use fails
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
releaseWasCalled = true
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Use that fails
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Left[resourceState, string](useError)
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
assert.True(t, releaseWasCalled, "onRelease should be called even when use fails")
RES.Fold(
func(err error) bool {
assert.Equal(t, useError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}
// TestWithResourceStateThreading tests that state is properly threaded through all operations
func TestWithResourceStateThreading(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
// Create increments counter
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
// Use observes the state after creation
useResource := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
// Verify state was updated by onCreate
assert.Equal(t, 1, s.resourcesCreated)
assert.Equal(t, 0, s.resourcesReleased)
return RES.Of(P.MakePair(s, s.resourcesCreated))
}
}
}
}
// Release increments released counter
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
// Verify state was updated by onCreate and use
assert.Equal(t, 1, s.resourcesCreated)
assert.Equal(t, 0, s.resourcesReleased)
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
withResource := WithResource[int](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] {
finalState := P.Head(p)
value := P.Tail(p)
// Verify final state
// Note: Final state is from the use function, which preserves the state it received
// onCreate: 0->1, use: sees 1, release: sees 1 and increments released to 1
// But final state is from use function where resourcesReleased=0
assert.Equal(t, 1, finalState.resourcesCreated)
assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before release")
assert.Equal(t, 1, value)
return p
})(outcome)
}
// TestWithResourceMultipleResources tests using WithResource multiple times (nesting)
func TestWithResourceMultipleResources(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx := context.Background()
createResource := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: newState.resourcesCreated, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
releaseResource := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
// Create two nested resources
withResource1 := WithResource[int](createResource, releaseResource)
withResource2 := WithResource[int](createResource, releaseResource)
result := withResource1(func(res1 mockResource) StateReaderIOResult[resourceState, int] {
return withResource2(func(res2 mockResource) StateReaderIOResult[resourceState, int] {
// Both resources should be available
return Of[resourceState](res1.id + res2.id)
})
})
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsRight(outcome))
RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] {
finalState := P.Head(p)
value := P.Tail(p)
// Both resources created, but final state is from innermost use function
// onCreate1: 0->1, onCreate2: 1->2, use (Of): sees 2
// Release functions execute but their state changes aren't in the final result
assert.Equal(t, 2, finalState.resourcesCreated)
assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before releases")
// res1.id = 1, res2.id = 2, sum = 3
assert.Equal(t, 3, value)
return p
})(outcome)
}
// TestWithResourceContextCancellation tests behavior with context cancellation
func TestWithResourceContextCancellation(t *testing.T) {
initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0}
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
cancelError := errors.New("context cancelled")
// Create should respect context cancellation
onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] {
return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] {
return func() Result[Pair[resourceState, mockResource]] {
if ctx.Err() != nil {
return RES.Left[Pair[resourceState, mockResource]](cancelError)
}
newState := resourceState{
resourcesCreated: s.resourcesCreated + 1,
resourcesReleased: s.resourcesReleased,
}
res := mockResource{id: 1, isValid: true}
return RES.Of(P.MakePair(newState, res))
}
}
}
onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] {
return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] {
return func(ctx context.Context) IOResult[Pair[resourceState, int]] {
return func() Result[Pair[resourceState, int]] {
newState := resourceState{
resourcesCreated: s.resourcesCreated,
resourcesReleased: s.resourcesReleased + 1,
}
return RES.Of(P.MakePair(newState, 0))
}
}
}
}
useResource := func(res mockResource) StateReaderIOResult[resourceState, string] {
return Of[resourceState]("should not reach here")
}
withResource := WithResource[string](onCreate, onRelease)
result := withResource(useResource)
outcome := result(initialState)(ctx)()
assert.True(t, RES.IsLeft(outcome))
RES.Fold(
func(err error) bool {
assert.Equal(t, cancelError, err)
return true
},
func(p Pair[resourceState, string]) bool {
t.Error("Expected error but got success")
return false
},
)(outcome)
}

View File

@@ -0,0 +1,309 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import (
"context"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/statet"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// Left creates a StateReaderIOResult that represents a failed computation with the given error.
// The error value is immediately available and does not depend on state or context.
//
// Example:
//
// result := statereaderioresult.Left[AppState, string](errors.New("validation failed"))
// // Returns a failed computation that ignores state and context
func Left[S, A any](e error) StateReaderIOResult[S, A] {
return function.Constant1[S](RIORES.Left[Pair[S, A]](e))
}
// Right creates a StateReaderIOResult that represents a successful computation with the given value.
// The value is wrapped and the state is passed through unchanged.
//
// Example:
//
// result := statereaderioresult.Right[AppState](42)
// // Returns a successful computation containing 42
func Right[S, A any](a A) StateReaderIOResult[S, A] {
return statet.Of[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], a)
}
// Of creates a StateReaderIOResult that represents a successful computation with the given value.
// This is the monadic return/pure operation for StateReaderIOResult.
// Equivalent to [Right].
//
// Example:
//
// result := statereaderioresult.Of[AppState](42)
// // Returns a successful computation containing 42
func Of[S, A any](a A) StateReaderIOResult[S, A] {
return Right[S](a)
}
// MonadMap transforms the success value of a StateReaderIOResult using the provided function.
// If the computation fails, the error is propagated unchanged.
// The state is threaded through the computation.
// This is the functor map operation.
//
// Example:
//
// result := statereaderioresult.MonadMap(
// statereaderioresult.Of[AppState](21),
// N.Mul(2),
// ) // Result contains 42
func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReaderIOResult[S, B] {
return statet.MonadMap[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
RIORES.MonadMap[Pair[S, A], Pair[S, B]],
fa,
f,
)
}
// Map is the curried version of [MonadMap].
// Returns a function that transforms a StateReaderIOResult.
//
// Example:
//
// double := statereaderioresult.Map[AppState](N.Mul(2))
// result := function.Pipe1(statereaderioresult.Of[AppState](21), double)
func Map[S, A, B any](f func(A) B) Operator[S, A, B] {
return statet.Map[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
RIORES.Map[Pair[S, A], Pair[S, B]],
f,
)
}
// MonadChain sequences two computations, passing the result of the first to a function
// that produces the second computation. This is the monadic bind operation.
// The state is threaded through both computations.
//
// Example:
//
// result := statereaderioresult.MonadChain(
// statereaderioresult.Of[AppState](5),
// func(x int) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](fmt.Sprintf("value: %d", x))
// },
// )
func MonadChain[S, A, B any](fa StateReaderIOResult[S, A], f Kleisli[S, A, B]) StateReaderIOResult[S, B] {
return statet.MonadChain(
RIORES.MonadChain[Pair[S, A], Pair[S, B]],
fa,
f,
)
}
// Chain is the curried version of [MonadChain].
// Returns a function that sequences computations.
//
// Example:
//
// stringify := statereaderioresult.Chain[AppState](func(x int) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](fmt.Sprintf("%d", x))
// })
// result := function.Pipe1(statereaderioresult.Of[AppState](42), stringify)
func Chain[S, A, B any](f Kleisli[S, A, B]) Operator[S, A, B] {
return statet.Chain[StateReaderIOResult[S, A]](
RIORES.Chain[Pair[S, A], Pair[S, B]],
f,
)
}
// MonadAp applies a function wrapped in a StateReaderIOResult to a value wrapped in a StateReaderIOResult.
// If either the function or the value fails, the error is propagated.
// The state is threaded through both computations sequentially.
// This is the applicative apply operation.
//
// Example:
//
// fab := statereaderioresult.Of[AppState](N.Mul(2))
// fa := statereaderioresult.Of[AppState](21)
// result := statereaderioresult.MonadAp(fab, fa) // Result contains 42
func MonadAp[B, S, A any](fab StateReaderIOResult[S, func(A) B], fa StateReaderIOResult[S, A]) StateReaderIOResult[S, B] {
return statet.MonadAp[StateReaderIOResult[S, A], StateReaderIOResult[S, B]](
RIORES.MonadMap[Pair[S, A], Pair[S, B]],
RIORES.MonadChain[Pair[S, func(A) B], Pair[S, B]],
fab,
fa,
)
}
// Ap is the curried version of [MonadAp].
// Returns a function that applies a wrapped function to the given wrapped value.
func Ap[B, S, A any](fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] {
return statet.Ap[StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]](
RIORES.Map[Pair[S, A], Pair[S, B]],
RIORES.Chain[Pair[S, func(A) B], Pair[S, B]],
fa,
)
}
// FromReaderIOResult lifts a ReaderIOResult into a StateReaderIOResult.
// The state is passed through unchanged.
//
// Example:
//
// riores := readerioresult.Of(42)
// result := statereaderioresult.FromReaderIOResult[AppState](riores)
func FromReaderIOResult[S, A any](fa ReaderIOResult[A]) StateReaderIOResult[S, A] {
return statet.FromF[StateReaderIOResult[S, A]](
RIORES.MonadMap[A],
fa,
)
}
// FromIOResult lifts an IOResult into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
func FromIOResult[S, A any](fa IOResult[A]) StateReaderIOResult[S, A] {
return FromReaderIOResult[S](RIORES.FromIOResult(fa))
}
// FromState lifts a State computation into a StateReaderIOResult.
// The computation cannot fail (uses the error type).
func FromState[S, A any](sa State[S, A]) StateReaderIOResult[S, A] {
return statet.FromState[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], sa)
}
// FromIO lifts an IO computation into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
func FromIO[S, A any](fa IO[A]) StateReaderIOResult[S, A] {
return FromReaderIOResult[S](RIORES.FromIO(fa))
}
// FromResult lifts a Result into a StateReaderIOResult.
// The state is passed through unchanged and the context is ignored.
//
// Example:
//
// result := statereaderioresult.FromResult[AppState](result.Of(42))
func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] {
return result.Fold(Left[S, A], Right[S, A])(ma)
}
// Combinators
// Local runs a computation with a modified context.
// The function f transforms the context before passing it to the computation.
//
// Example:
//
// // Modify context before running computation
// withTimeout := statereaderioresult.Local[AppState](
// func(ctx context.Context) context.Context {
// ctx, _ = context.WithTimeout(ctx, 60*time.Second)
// return ctx
// }
// )
// result := withTimeout(computation)
func Local[S, A any](f func(context.Context) context.Context) func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return func(ma StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return function.Flow2(ma, RIOR.Local[Pair[S, A]](f))
}
}
// Asks creates a computation that derives a value from the context.
// The function receives the context and returns a StateReaderIOResult.
//
// Example:
//
// getValue := statereaderioresult.Asks[AppState, string](
// func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] {
// return statereaderioresult.Of[AppState](ctx.Value("key").(string))
// },
// )
func Asks[S, A any](f func(context.Context) StateReaderIOResult[S, A]) StateReaderIOResult[S, A] {
return func(s S) ReaderIOResult[Pair[S, A]] {
return func(ctx context.Context) IOResult[Pair[S, A]] {
return f(ctx)(s)(ctx)
}
}
}
// FromResultK lifts a Result-returning function into a Kleisli arrow for StateReaderIOResult.
//
// Example:
//
// validate := func(x int) result.Result[int] {
// if x > 0 { return result.Of(x) }
// return result.Error[int](errors.New("negative"))
// }
// kleisli := statereaderioresult.FromResultK[AppState](validate)
func FromResultK[S, A, B any](f func(A) Result[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromResult[S, B],
)
}
// FromIOK lifts an IO-returning function into a Kleisli arrow for StateReaderIOResult.
func FromIOK[S, A, B any](f func(A) IO[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromIO[S, B],
)
}
// FromIOResultK lifts an IOResult-returning function into a Kleisli arrow for StateReaderIOResult.
func FromIOResultK[S, A, B any](f func(A) IOResult[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromIOResult[S, B],
)
}
// FromReaderIOResultK lifts a ReaderIOResult-returning function into a Kleisli arrow for StateReaderIOResult.
func FromReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Kleisli[S, A, B] {
return function.Flow2(
f,
FromReaderIOResult[S, B],
)
}
// MonadChainReaderIOResultK chains a StateReaderIOResult with a ReaderIOResult-returning function.
func MonadChainReaderIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) ReaderIOResult[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromReaderIOResultK[S](f))
}
// ChainReaderIOResultK is the curried version of [MonadChainReaderIOResultK].
func ChainReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Operator[S, A, B] {
return Chain(FromReaderIOResultK[S](f))
}
// MonadChainIOResultK chains a StateReaderIOResult with an IOResult-returning function.
func MonadChainIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) IOResult[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromIOResultK[S](f))
}
// ChainIOResultK is the curried version of [MonadChainIOResultK].
func ChainIOResultK[S, A, B any](f func(A) IOResult[B]) Operator[S, A, B] {
return Chain(FromIOResultK[S](f))
}
// MonadChainResultK chains a StateReaderIOResult with a Result-returning function.
func MonadChainResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) Result[B]) StateReaderIOResult[S, B] {
return MonadChain(ma, FromResultK[S](f))
}
// ChainResultK is the curried version of [MonadChainResultK].
func ChainResultK[S, A, B any](f func(A) Result[B]) Operator[S, A, B] {
return Chain(FromResultK[S](f))
}

View File

@@ -0,0 +1,567 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import (
"context"
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
IOR "github.com/IBM/fp-go/v2/ioresult"
N "github.com/IBM/fp-go/v2/number"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type testState struct {
counter int
}
func TestOf(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := Of[testState](42)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Fold(
func(err error) bool {
t.Fatalf("Expected Success but got Error: %v", err)
return false
},
func(p P.Pair[testState, int]) bool {
assert.Equal(t, 42, P.Tail(p))
assert.Equal(t, 0, P.Head(p).counter) // State unchanged
return true
},
)(res)
}
func TestRight(t *testing.T) {
state := testState{counter: 5}
ctx := context.Background()
result := Right[testState](100)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 100, P.Tail(p))
assert.Equal(t, 5, P.Head(p).counter)
return p
})(res)
}
func TestLeft(t *testing.T) {
state := testState{counter: 10}
ctx := context.Background()
testErr := errors.New("test error")
result := Left[testState, int](testErr)
res := result(state)(ctx)()
assert.True(t, RES.IsLeft(res))
}
func TestMonadMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := MonadMap(
Of[testState](21),
N.Mul(2),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestMap(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := F.Pipe1(
Of[testState](21),
Map[testState](N.Mul(2)),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestMonadChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := MonadChain(
Of[testState](5),
func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("value: %d", x))
},
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value: 5", P.Tail(p))
return p
})(res)
}
func TestChain(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
result := F.Pipe1(
Of[testState](5),
Chain(func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("value: %d", x))
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value: 5", P.Tail(p))
return p
})(res)
}
func TestMonadAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
fab := Of[testState](N.Mul(2))
fa := Of[testState](21)
result := MonadAp(fab, fa)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestAp(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
fa := Of[testState](21)
result := F.Pipe1(
Of[testState](N.Mul(2)),
Ap[int](fa),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 42, P.Tail(p))
return p
})(res)
}
func TestFromIOResult(t *testing.T) {
state := testState{counter: 3}
ctx := context.Background()
ior := IOR.Of(55)
result := FromIOResult[testState](ior)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 55, P.Tail(p))
assert.Equal(t, 3, P.Head(p).counter)
return p
})(res)
}
func TestFromState(t *testing.T) {
initialState := testState{counter: 10}
ctx := context.Background()
// State computation that increments counter and returns it
stateComp := func(s testState) P.Pair[testState, int] {
newState := testState{counter: s.counter + 1}
return P.MakePair(newState, newState.counter)
}
result := FromState(stateComp)
res := result(initialState)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 11, P.Tail(p)) // Incremented value
assert.Equal(t, 11, P.Head(p).counter) // State updated
return p
})(res)
}
func TestFromIO(t *testing.T) {
state := testState{counter: 8}
ctx := context.Background()
ioVal := func() int { return 99 }
result := FromIO[testState](ioVal)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 99, P.Tail(p))
assert.Equal(t, 8, P.Head(p).counter)
return p
})(res)
}
func TestFromResult(t *testing.T) {
state := testState{counter: 12}
ctx := context.Background()
// Test Success case
resultSuccess := FromResult[testState](RES.Of(42))
resSuccess := resultSuccess(state)(ctx)()
assert.True(t, RES.IsRight(resSuccess))
// Test Error case
resultError := FromResult[testState](RES.Left[int](errors.New("error")))
resError := resultError(state)(ctx)()
assert.True(t, RES.IsLeft(resError))
}
func TestLocal(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "key", "value1")
// Create a computation that uses the context
comp := Asks(func(c context.Context) StateReaderIOResult[testState, string] {
val := c.Value("key").(string)
return Of[testState](val)
})
// Modify context before running computation
result := Local[testState, string](
func(c context.Context) context.Context {
return context.WithValue(c, "key", "value2")
},
)(comp)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "value2", P.Tail(p))
return p
})(res)
}
func TestAsks(t *testing.T) {
state := testState{counter: 0}
ctx := context.WithValue(context.Background(), "multiplier", 7)
result := Asks(func(c context.Context) StateReaderIOResult[testState, int] {
mult := c.Value("multiplier").(int)
return Of[testState](mult * 5)
})
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 35, P.Tail(p))
return p
})(res)
}
func TestFromResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
validate := func(x int) RES.Result[int] {
if x > 0 {
return RES.Of(x * 2)
}
return RES.Left[int](errors.New("negative"))
}
kleisli := FromResultK[testState](validate)
// Test with valid input
resultValid := kleisli(5)
resValid := resultValid(state)(ctx)()
assert.True(t, RES.IsRight(resValid))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 10, P.Tail(p))
return p
})(resValid)
// Test with invalid input
resultInvalid := kleisli(-5)
resInvalid := resultInvalid(state)(ctx)()
assert.True(t, RES.IsLeft(resInvalid))
}
func TestFromIOK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
ioFunc := func(x int) io.IO[int] {
return func() int { return x * 3 }
}
kleisli := FromIOK[testState](ioFunc)
result := kleisli(7)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 21, P.Tail(p))
return p
})(res)
}
func TestFromIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
iorFunc := func(x int) IOR.IOResult[int] {
if x > 0 {
return IOR.Of(x * 4)
}
return IOR.Left[int](errors.New("invalid"))
}
kleisli := FromIOResultK[testState](iorFunc)
result := kleisli(3)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 12, P.Tail(p))
return p
})(res)
}
func TestChainResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
validate := func(x int) RES.Result[string] {
if x > 0 {
return RES.Of(fmt.Sprintf("valid: %d", x))
}
return RES.Left[string](errors.New("invalid"))
}
result := F.Pipe1(
Of[testState](42),
ChainResultK[testState](validate),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "valid: 42", P.Tail(p))
return p
})(res)
}
func TestChainIOResultK(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
iorFunc := func(x int) IOR.IOResult[string] {
return IOR.Of(fmt.Sprintf("result: %d", x))
}
result := F.Pipe1(
Of[testState](100),
ChainIOResultK[testState](iorFunc),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "result: 100", P.Tail(p))
return p
})(res)
}
func TestDo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
type Result struct {
value int
}
result := Do[testState](Result{})
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] {
assert.Equal(t, 0, P.Tail(p).value)
return p
})(res)
}
func TestBindTo(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
type Result struct {
value int
}
result := F.Pipe1(
Of[testState](42),
BindTo[testState](func(v int) Result {
return Result{value: v}
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] {
assert.Equal(t, 42, P.Tail(p).value)
return p
})(res)
}
func TestStatefulComputation(t *testing.T) {
initialState := testState{counter: 0}
ctx := context.Background()
// Create a computation that modifies state
incrementAndGet := func(s testState) P.Pair[testState, int] {
newState := testState{counter: s.counter + 1}
return P.MakePair(newState, newState.counter)
}
// Chain multiple stateful operations
result := F.Pipe2(
FromState(incrementAndGet),
Chain(func(v1 int) StateReaderIOResult[testState, int] {
return FromState(incrementAndGet)
}),
Chain(func(v2 int) StateReaderIOResult[testState, int] {
return FromState(incrementAndGet)
}),
)
res := result(initialState)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] {
assert.Equal(t, 3, P.Tail(p)) // Last incremented value
assert.Equal(t, 3, P.Head(p).counter) // State updated three times
return p
})(res)
}
func TestErrorPropagation(t *testing.T) {
state := testState{counter: 0}
ctx := context.Background()
testErr := errors.New("test error")
// Chain operations where the second one fails
result := F.Pipe1(
Of[testState](42),
Chain(func(x int) StateReaderIOResult[testState, int] {
return Left[testState, int](testErr)
}),
)
res := result(state)(ctx)()
assert.True(t, RES.IsLeft(res))
}
func TestPointed(t *testing.T) {
p := Pointed[testState, int]()
assert.NotNil(t, p)
result := p.Of(42)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
}
func TestFunctor(t *testing.T) {
f := Functor[testState, int, string]()
assert.NotNil(t, f)
mapper := f.Map(func(x int) string { return fmt.Sprintf("%d", x) })
result := mapper(Of[testState](42))
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}
func TestApplicative(t *testing.T) {
a := Applicative[testState, int, string]()
assert.NotNil(t, a)
fab := Of[testState](func(x int) string { return fmt.Sprintf("%d", x) })
fa := Of[testState](42)
result := a.Ap(fa)(fab)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}
func TestMonad(t *testing.T) {
m := Monad[testState, int, string]()
assert.NotNil(t, m)
fa := m.Of(42)
result := m.Chain(func(x int) StateReaderIOResult[testState, string] {
return Of[testState](fmt.Sprintf("%d", x))
})(fa)
state := testState{counter: 0}
ctx := context.Background()
res := result(state)(ctx)()
assert.True(t, RES.IsRight(res))
RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] {
assert.Equal(t, "42", P.Tail(p))
return p
})(res)
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testing
import (
"context"
"testing"
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
ST "github.com/IBM/fp-go/v2/context/statereaderioresult"
EQ "github.com/IBM/fp-go/v2/eq"
L "github.com/IBM/fp-go/v2/internal/monad/testing"
P "github.com/IBM/fp-go/v2/pair"
RES "github.com/IBM/fp-go/v2/result"
)
// AssertLaws asserts the monad laws for the StateReaderIOResult monad
func AssertLaws[S, A, B, C any](t *testing.T,
eqs EQ.Eq[S],
eqa EQ.Eq[A],
eqb EQ.Eq[B],
eqc EQ.Eq[C],
ab func(A) B,
bc func(B) C,
s S,
ctx context.Context,
) func(a A) bool {
eqra := RIORES.Eq(RES.Eq(P.Eq(eqs, eqa)))(ctx)
eqrb := RIORES.Eq(RES.Eq(P.Eq(eqs, eqb)))(ctx)
eqrc := RIORES.Eq(RES.Eq(P.Eq(eqs, eqc)))(ctx)
fofc := ST.Pointed[S, C]()
fofaa := ST.Pointed[S, func(A) A]()
fofbc := ST.Pointed[S, func(B) C]()
fofabb := ST.Pointed[S, func(func(A) B) B]()
fmap := ST.Functor[S, func(B) C, func(func(A) B) func(A) C]()
fapabb := ST.Applicative[S, func(A) B, B]()
fapabac := ST.Applicative[S, func(A) B, func(A) C]()
maa := ST.Monad[S, A, A]()
mab := ST.Monad[S, A, B]()
mac := ST.Monad[S, A, C]()
mbc := ST.Monad[S, B, C]()
return L.MonadAssertLaws(t,
ST.Eq(eqra)(s),
ST.Eq(eqrb)(s),
ST.Eq(eqrc)(s),
fofc,
fofaa,
fofbc,
fofabb,
fmap,
fapabb,
fapabac,
maa,
mab,
mac,
mbc,
ab,
bc,
)
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testing
import (
"context"
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
)
func TestMonadLaws(t *testing.T) {
// some comparison
eqs := A.Eq(EQ.FromStrictEquals[string]())
eqa := EQ.FromStrictEquals[bool]()
eqb := EQ.FromStrictEquals[int]()
eqc := EQ.FromStrictEquals[string]()
ab := func(a bool) int {
if a {
return 1
}
return 0
}
bc := func(b int) string {
return fmt.Sprintf("value %d", b)
}
laws := AssertLaws(t, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), context.Background())
assert.True(t, laws(true))
assert.True(t, laws(false))
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package statereaderioresult
import (
RIORES "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/iso/lens"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/state"
)
type (
// Endomorphism represents a function from A to A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lens is an optic that focuses on a field of type A within a structure of type S.
Lens[S, A any] = lens.Lens[S, A]
// State represents a stateful computation that takes an initial state S and returns
// a pair of the new state S and a value A.
State[S, A any] = state.State[S, A]
// Pair represents a tuple of two values.
Pair[L, R any] = pair.Pair[L, R]
// Reader represents a computation that depends on an environment/context of type R
// and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// Result represents a value that can be either an error or a success value.
// This is specialized to use [error] as the error type.
Result[A any] = result.Result[A]
// IO represents a computation that performs side effects and produces a value of type A.
IO[A any] = io.IO[A]
// IOResult represents a computation that performs side effects and can fail with an error
// or succeed with a value A.
IOResult[A any] = ioresult.IOResult[A]
// ReaderIOResult represents a computation that depends on a context.Context,
// performs side effects, and can fail with an error or succeed with a value A.
ReaderIOResult[A any] = RIORES.ReaderIOResult[A]
// StateReaderIOResult represents a stateful computation that:
// - Takes an initial state S
// - Depends on a [context.Context]
// - Performs side effects (IO)
// - Can fail with an [error] or succeed with a value A
// - Returns a pair of the new state S and the result
//
// This is the main type of this package, combining State, Reader, IO, and Result monads.
// It is a specialization of StateReaderIOEither with:
// - Context type fixed to [context.Context]
// - Error type fixed to [error]
StateReaderIOResult[S, A any] = Reader[S, ReaderIOResult[Pair[S, A]]]
// Kleisli represents a Kleisli arrow - a function that takes a value A and returns
// a StateReaderIOResult computation producing B.
// This is used for monadic composition via Chain.
Kleisli[S, A, B any] = Reader[A, StateReaderIOResult[S, B]]
// Operator represents a function that transforms one StateReaderIOResult into another.
// This is commonly used for building composable operations via Map, Chain, etc.
Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]
)

67
v2/coverage.txt Normal file
View File

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

View File

@@ -249,7 +249,7 @@ func TestMultiTokenStringRepresentation(t *testing.T) {
// Benchmark tests
func BenchmarkMakeToken(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
MakeToken[int]("BenchToken")
}
}
@@ -259,13 +259,13 @@ func BenchmarkTokenUnerase(b *testing.B) {
value := any(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
token.Unerase(value)
}
}
func BenchmarkMakeMultiToken(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
MakeMultiToken[int]("BenchMulti")
}
}

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

@@ -4,16 +4,17 @@ import (
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
TST "github.com/IBM/fp-go/v2/internal/testing"
"github.com/stretchr/testify/assert"
)
func TestCompactArray(t *testing.T) {
ar := []Either[string, string]{
ar := A.From(
Of[string]("ok"),
Left[string]("err"),
Of[string]("ok"),
}
)
res := CompactArray(ar)
assert.Equal(t, 2, len(res))

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.
@@ -58,17 +59,23 @@ func FromIO[E any, IO ~func() A, A any](f IO) Either[E, A] {
//
// Example:
//
// fab := either.Right[error](func(x int) int { return x * 2 })
// fab := either.Right[error](N.Mul(2))
// 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)
}
@@ -81,12 +88,15 @@ func Ap[B, E, A any](fa Either[E, A]) Operator[E, func(A) B, B] {
//
// result := either.MonadMap(
// either.Right[error](21),
// func(x int) int { return x * 2 },
// N.Mul(2),
// ) // Right(42)
//
//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

@@ -20,6 +20,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
var (
@@ -33,21 +34,21 @@ var (
// Benchmark core constructors
func BenchmarkLeft(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Left[int](errBench)
}
}
func BenchmarkRight(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Right[error](42)
}
}
func BenchmarkOf(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Of[error](42)
}
}
@@ -57,7 +58,7 @@ func BenchmarkIsLeft(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchBool = IsLeft(left)
}
}
@@ -66,7 +67,7 @@ func BenchmarkIsRight(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchBool = IsRight(right)
}
}
@@ -75,10 +76,10 @@ func BenchmarkIsRight(b *testing.B) {
func BenchmarkMonadFold_Right(b *testing.B) {
right := Right[error](42)
onLeft := func(e error) int { return 0 }
onRight := func(a int) int { return a * 2 }
onRight := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = MonadFold(right, onLeft, onRight)
}
}
@@ -86,10 +87,10 @@ func BenchmarkMonadFold_Right(b *testing.B) {
func BenchmarkMonadFold_Left(b *testing.B) {
left := Left[int](errBench)
onLeft := func(e error) int { return 0 }
onRight := func(a int) int { return a * 2 }
onRight := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = MonadFold(left, onLeft, onRight)
}
}
@@ -98,11 +99,11 @@ func BenchmarkFold_Right(b *testing.B) {
right := Right[error](42)
folder := Fold(
func(e error) int { return 0 },
func(a int) int { return a * 2 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = folder(right)
}
}
@@ -111,11 +112,11 @@ func BenchmarkFold_Left(b *testing.B) {
left := Left[int](errBench)
folder := Fold(
func(e error) int { return 0 },
func(a int) int { return a * 2 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = folder(left)
}
}
@@ -125,7 +126,7 @@ func BenchmarkUnwrap_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = Unwrap(right)
}
}
@@ -134,7 +135,7 @@ func BenchmarkUnwrap_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = Unwrap(left)
}
}
@@ -143,7 +144,7 @@ func BenchmarkUnwrapError_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = UnwrapError(right)
}
}
@@ -152,7 +153,7 @@ func BenchmarkUnwrapError_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = UnwrapError(left)
}
}
@@ -160,40 +161,40 @@ func BenchmarkUnwrapError_Left(b *testing.B) {
// Benchmark functor operations
func BenchmarkMonadMap_Right(b *testing.B) {
right := Right[error](42)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadMap(right, mapper)
}
}
func BenchmarkMonadMap_Left(b *testing.B) {
left := Left[int](errBench)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadMap(left, mapper)
}
}
func BenchmarkMap_Right(b *testing.B) {
right := Right[error](42)
mapper := Map[error](func(a int) int { return a * 2 })
mapper := Map[error](N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = mapper(right)
}
}
func BenchmarkMap_Left(b *testing.B) {
left := Left[int](errBench)
mapper := Map[error](func(a int) int { return a * 2 })
mapper := Map[error](N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = mapper(left)
}
}
@@ -203,7 +204,7 @@ func BenchmarkMapLeft_Right(b *testing.B) {
mapper := MapLeft[int](error.Error)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(right)
}
}
@@ -213,7 +214,7 @@ func BenchmarkMapLeft_Left(b *testing.B) {
mapper := MapLeft[int](error.Error)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(left)
}
}
@@ -226,7 +227,7 @@ func BenchmarkBiMap_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(right)
}
}
@@ -239,7 +240,7 @@ func BenchmarkBiMap_Left(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(left)
}
}
@@ -250,7 +251,7 @@ func BenchmarkMonadChain_Right(b *testing.B) {
chainer := func(a int) Either[error, int] { return Right[error](a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadChain(right, chainer)
}
}
@@ -260,7 +261,7 @@ func BenchmarkMonadChain_Left(b *testing.B) {
chainer := func(a int) Either[error, int] { return Right[error](a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadChain(left, chainer)
}
}
@@ -270,7 +271,7 @@ func BenchmarkChain_Right(b *testing.B) {
chainer := Chain(func(a int) Either[error, int] { return Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(right)
}
}
@@ -280,7 +281,7 @@ func BenchmarkChain_Left(b *testing.B) {
chainer := Chain(func(a int) Either[error, int] { return Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(left)
}
}
@@ -290,7 +291,7 @@ func BenchmarkChainFirst_Right(b *testing.B) {
chainer := ChainFirst(func(a int) Either[error, string] { return Right[error]("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(right)
}
}
@@ -300,7 +301,7 @@ func BenchmarkChainFirst_Left(b *testing.B) {
chainer := ChainFirst(func(a int) Either[error, string] { return Right[error]("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(left)
}
}
@@ -309,7 +310,7 @@ func BenchmarkFlatten_Right(b *testing.B) {
nested := Right[error](Right[error](42))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Flatten(nested)
}
}
@@ -318,28 +319,28 @@ func BenchmarkFlatten_Left(b *testing.B) {
nested := Left[Either[error, int]](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Flatten(nested)
}
}
// Benchmark applicative operations
func BenchmarkMonadAp_RightRight(b *testing.B) {
fab := Right[error](func(a int) int { return a * 2 })
fab := Right[error](N.Mul(2))
fa := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadAp(fab, fa)
}
}
func BenchmarkMonadAp_RightLeft(b *testing.B) {
fab := Right[error](func(a int) int { return a * 2 })
fab := Right[error](N.Mul(2))
fa := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadAp(fab, fa)
}
}
@@ -349,18 +350,18 @@ func BenchmarkMonadAp_LeftRight(b *testing.B) {
fa := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadAp(fab, fa)
}
}
func BenchmarkAp_RightRight(b *testing.B) {
fab := Right[error](func(a int) int { return a * 2 })
fab := Right[error](N.Mul(2))
fa := Right[error](42)
ap := Ap[int](fa)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = ap(fab)
}
}
@@ -371,7 +372,7 @@ func BenchmarkAlt_RightRight(b *testing.B) {
alternative := Alt(func() Either[error, int] { return Right[error](99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = alternative(right)
}
}
@@ -381,7 +382,7 @@ func BenchmarkAlt_LeftRight(b *testing.B) {
alternative := Alt(func() Either[error, int] { return Right[error](99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = alternative(left)
}
}
@@ -391,7 +392,7 @@ func BenchmarkOrElse_Right(b *testing.B) {
recover := OrElse(func(e error) Either[error, int] { return Right[error](0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = recover(right)
}
}
@@ -401,7 +402,7 @@ func BenchmarkOrElse_Left(b *testing.B) {
recover := OrElse(func(e error) Either[error, int] { return Right[error](0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = recover(left)
}
}
@@ -410,7 +411,7 @@ func BenchmarkOrElse_Left(b *testing.B) {
func BenchmarkTryCatch_Success(b *testing.B) {
onThrow := func(err error) error { return err }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatch(42, nil, onThrow)
}
}
@@ -418,21 +419,21 @@ func BenchmarkTryCatch_Success(b *testing.B) {
func BenchmarkTryCatch_Error(b *testing.B) {
onThrow := func(err error) error { return err }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatch(0, errBench, onThrow)
}
}
func BenchmarkTryCatchError_Success(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatchError(42, nil)
}
}
func BenchmarkTryCatchError_Error(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatchError(0, errBench)
}
}
@@ -441,7 +442,7 @@ func BenchmarkSwap_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Swap(right)
}
}
@@ -450,7 +451,7 @@ func BenchmarkSwap_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Swap(left)
}
}
@@ -460,7 +461,7 @@ func BenchmarkGetOrElse_Right(b *testing.B) {
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = getter(right)
}
}
@@ -470,7 +471,7 @@ func BenchmarkGetOrElse_Left(b *testing.B) {
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = getter(left)
}
}
@@ -480,10 +481,10 @@ func BenchmarkPipeline_Map_Right(b *testing.B) {
right := Right[error](21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
right,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -492,10 +493,10 @@ func BenchmarkPipeline_Map_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
left,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -504,7 +505,7 @@ func BenchmarkPipeline_Chain_Right(b *testing.B) {
right := Right[error](21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
right,
Chain(func(x int) Either[error, int] { return Right[error](x * 2) }),
@@ -516,7 +517,7 @@ func BenchmarkPipeline_Chain_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
left,
Chain(func(x int) Either[error, int] { return Right[error](x * 2) }),
@@ -528,12 +529,12 @@ func BenchmarkPipeline_Complex_Right(b *testing.B) {
right := Right[error](10)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe3(
right,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
Chain(func(x int) Either[error, int] { return Right[error](x + 1) }),
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -542,12 +543,12 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe3(
left,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
Chain(func(x int) Either[error, int] { return Right[error](x + 1) }),
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -559,7 +560,7 @@ func BenchmarkMonadSequence2_RightRight(b *testing.B) {
f := func(a, b int) Either[error, int] { return Right[error](a + b) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadSequence2(e1, e2, f)
}
}
@@ -570,7 +571,7 @@ func BenchmarkMonadSequence2_LeftRight(b *testing.B) {
f := func(a, b int) Either[error, int] { return Right[error](a + b) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadSequence2(e1, e2, f)
}
}
@@ -582,7 +583,7 @@ func BenchmarkMonadSequence3_RightRightRight(b *testing.B) {
f := func(a, b, c int) Either[error, int] { return Right[error](a + b + c) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadSequence3(e1, e2, e3, f)
}
}
@@ -591,7 +592,7 @@ func BenchmarkMonadSequence3_RightRightRight(b *testing.B) {
func BenchmarkDo(b *testing.B) {
type State struct{ value int }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Do[error](State{})
}
}
@@ -609,7 +610,7 @@ func BenchmarkBind_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = binder(initial)
}
}
@@ -625,7 +626,7 @@ func BenchmarkLet_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = letter(initial)
}
}
@@ -635,7 +636,7 @@ func BenchmarkString_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchString = right.String()
}
}
@@ -644,7 +645,7 @@ func BenchmarkString_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchString = left.String()
}
}

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

View File

@@ -66,7 +66,7 @@ func TestUnwrapError(t *testing.T) {
func TestReduce(t *testing.T) {
s := S.Semigroup()
s := S.Semigroup
assert.Equal(t, "foobar", F.Pipe1(Right[string]("bar"), Reduce[string](s.Concat, "foo")))
assert.Equal(t, "foo", F.Pipe1(Left[string]("bar"), Reduce[string](s.Concat, "foo")))

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

@@ -46,7 +46,7 @@ func _log[E, A any](left func(string, ...any), right func(string, ...any), prefi
// result := F.Pipe2(
// either.Right[error](42),
// logger("Processing"),
// either.Map(func(x int) int { return x * 2 }),
// either.Map(N.Mul(2)),
// )
// // Logs: "Processing: 42"
// // result is Right(84)

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(N.Mul(2))(either.Right[error](21))
// // value is Right(42)
//
// // Ap applies wrapped functions (also fails fast)
// fn := either.Right[error](N.Add(1))
// result := m.Ap(value)(fn)
// // result is Right(43)
//
// Example - Real-world usage with error handling:
//
// m := either.Monad[error, User, SavedUser]()
//
// // Pipeline of operations that can fail
// result := m.Chain(func(user User) either.Either[error, SavedUser] {
// // Save to database
// return saveToDatabase(user)
// })(m.Chain(func(user User) either.Either[error, User] {
// // Validate user
// return validateUser(user)
// })(either.Right[error](inputUser)))
//
// // If any step fails, the error propagates immediately
//
// Type Parameters:
// - E: The error type (Left value)
// - A: The input value type (Right value)
// - B: The output value type after transformation
func Monad[E, A, B any]() monad.Monad[A, B, Either[E, A], Either[E, B], Either[E, func(A) B]] {
return &eitherMonad[E, A, B]{}
return &eitherMonad[E, A, B]{
eitherApplicative[E, A, B]{
Of[E, A],
Map[E, A, B],
Ap[B, E, A],
},
Chain[E, A, B],
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

144
v2/either/validation.go Normal file
View File

@@ -0,0 +1,144 @@
// 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
import (
F "github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
// MonadApV is the applicative validation functor that combines errors using a semigroup.
//
// Unlike the standard [MonadAp] which short-circuits on the first Left (error),
// MonadApV accumulates all errors using the provided semigroup's Concat operation.
// This is particularly useful for validation scenarios where you want to collect
// all validation errors rather than stopping at the first one.
//
// The function takes a semigroup for combining errors and returns a function that
// applies a wrapped function to a wrapped value, accumulating errors if both are Left.
//
// Behavior:
// - If both fab and fa are Left, combines their errors using sg.Concat
// - If only fab is Left, returns Left with fab's error
// - If only fa is Left, returns Left with fa's error
// - If both are Right, applies the function and returns Right with the result
//
// Type Parameters:
// - B: The result type after applying the function
// - E: The error type (must support the semigroup operation)
// - A: The input type to the function
//
// Parameters:
// - sg: A semigroup that defines how to combine two error values
//
// Returns:
// - A function that takes a wrapped function and a wrapped value, returning
// Either[E, B] with accumulated errors or the computed result
//
// Example:
//
// // Define a semigroup that concatenates error messages
// errorSemigroup := semigroup.MakeSemigroup(func(e1, e2 string) string {
// return e1 + "; " + e2
// })
//
// // Create the validation applicative
// applyV := either.MonadApV[int](errorSemigroup)
//
// // Both are errors - errors get combined
// fab := either.Left[func(int) int]("error1")
// fa := either.Left[int]("error2")
// result := applyV(fab, fa) // Left("error1; error2")
//
// // One error - returns that error
// fab2 := either.Right[string](N.Mul(2))
// fa2 := either.Left[int]("validation failed")
// result2 := applyV(fab2, fa2) // Left("validation failed")
//
// // Both success - applies function
// fab3 := either.Right[string](N.Mul(2))
// fa3 := either.Right[string](21)
// result3 := applyV(fab3, fa3) // Right(42)
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] {
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))
}
}
// ApV is the curried version of [MonadApV] that combines errors using a semigroup.
//
// This function provides a more convenient API for validation scenarios by currying
// the arguments. It first takes the value to validate, then returns a function that
// takes the validation function. This allows for a more natural composition style.
//
// Like [MonadApV], this accumulates all errors using the provided semigroup instead
// of short-circuiting on the first error. This is the key difference from the
// standard [Ap] function.
//
// Type Parameters:
// - B: The result type after applying the function
// - E: The error type (must support the semigroup operation)
// - A: The input type to the function
//
// Parameters:
// - sg: A semigroup that defines how to combine two error values
//
// Returns:
// - A function that takes a value Either[E, A] and returns an Operator that
// applies validation functions while accumulating errors
//
// Example:
//
// // Define a semigroup for combining validation errors
// type ValidationError struct {
// Errors []string
// }
// errorSemigroup := semigroup.MakeSemigroup(func(e1, e2 ValidationError) ValidationError {
// return ValidationError{Errors: append(e1.Errors, e2.Errors...)}
// })
//
// // Create validators
// validatePositive := func(x int) either.Either[ValidationError, int] {
// if x > 0 {
// return either.Right[ValidationError](x)
// }
// return either.Left[int](ValidationError{Errors: []string{"must be positive"}})
// }
//
// // Use ApV for validation
// applyValidation := either.ApV[int](errorSemigroup)
// value := either.Left[int](ValidationError{Errors: []string{"invalid input"}})
// validator := either.Left[func(int) int](ValidationError{Errors: []string{"invalid validator"}})
//
// result := applyValidation(value)(validator)
// // Left(ValidationError{Errors: []string{"invalid validator", "invalid input"}})
//
//go:inline
func ApV[B, A, E any](sg S.Semigroup[E]) func(Either[E, A]) Operator[E, func(A) B, B] {
apv := MonadApV[B, A](sg)
return func(e Either[E, A]) Operator[E, func(A) B, B] {
return F.Bind2nd(apv, e)
}
}

View File

@@ -0,0 +1,362 @@
// 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
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Both are Right - should apply function
fab := Right[string](N.Mul(2))
fa := Right[string](21)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string](42), result)
}
// 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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Both are Left - should combine errors
fab := Left[func(int) int]("error1")
fa := Left[int]("error2")
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Function is Left, value is Right - should return function's error
fab := Left[func(int) int]("function error")
fa := Right[string](21)
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("function error"), result)
}
// 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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Function is Right, value is Left - should return value's error
fab := Right[string](N.Mul(2))
fa := Left[int]("value error")
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("value error"), result)
}
// TestMonadApV_WithSliceSemigroup tests MonadApV with a slice-based semigroup
func TestMonadApV_WithSliceSemigroup(t *testing.T) {
// Create a semigroup that concatenates slices
sg := SG.MakeSemigroup(func(a, b []string) []string {
return append(a, b...)
})
// Create the validation applicative
applyV := MonadApV[string, string](sg)
// Both are Left with slice errors
fab := Left[func(string) string]([]string{"error1", "error2"})
fa := Left[string]([]string{"error3", "error4"})
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa errors + fab errors
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 := SG.MakeSemigroup(func(a, b string) string {
return a + " | " + b
})
// Create the validation applicative
applyV := MonadApV[string, int](sg)
// Test with a function that transforms the value
fab := Right[string](func(x int) string {
if x > 0 {
return "positive"
}
return "non-positive"
})
fa := Right[string](42)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("positive"), result)
}
// 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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Right - should apply function
fa := Right[string](21)
fab := Right[string](N.Mul(2))
result := applyV(fa)(fab)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string](42), result)
}
// 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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Left - should combine errors
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Function is Left, value is Right - should return function's error
fa := Right[string](21)
fab := Left[func(int) int]("function error")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("function error"), result)
}
// 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.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Function is Right, value is Left - should return value's error
fa := Left[int]("value error")
fab := Right[string](N.Mul(2))
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("value error"), result)
}
// TestApV_Composition tests ApV with function composition
func TestApV_Composition(t *testing.T) {
// Create a semigroup for string concatenation
sg := SG.MakeSemigroup(func(a, b string) string {
return a + " & " + b
})
// Create the validation applicative
applyV := ApV[string, int](sg)
// Test composition with pipe
fa := Right[string](10)
fab := Right[string](func(x int) string {
return F.Pipe1(x, func(n int) string {
if n >= 10 {
return "large"
}
return "small"
})
})
result := F.Pipe1(fa, applyV)(fab)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("large"), result)
}
// TestApV_WithStructSemigroup tests ApV with a custom struct semigroup
func TestApV_WithStructSemigroup(t *testing.T) {
type ValidationErrors struct {
Errors []string
}
// Create a semigroup that combines validation errors
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, int](sg)
// Both are Left with validation errors
fa := Left[int](ValidationErrors{Errors: []string{"field2: invalid"}})
fab := Left[func(int) int](ValidationErrors{Errors: []string{"field1: required"}})
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa errors + fab errors
expected := Left[int](ValidationErrors{
Errors: []string{"field1: required", "field2: invalid"},
})
assert.Equal(t, expected, result)
}
// TestApV_MultipleValidations tests ApV with multiple validation steps
func TestApV_MultipleValidations(t *testing.T) {
// Create a semigroup for string concatenation
sg := SG.MakeSemigroup(func(a, b string) string {
return a + ", " + b
})
// Create the validation applicative
applyV := ApV[int, int](sg)
// Simulate multiple validation failures
validation1 := Left[int]("age must be positive")
validation2 := Left[func(int) int]("name is required")
result := applyV(validation1)(validation2)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: validation1 error + validation2 error
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.IntersperseSemigroup(" + ")
// Create the validation applicative
applyV := MonadApV[string, int](sg)
// Function converts int to string
fab := Right[string](func(x int) string {
return F.Pipe1(x, func(n int) string {
if n == 0 {
return "zero"
} else if n > 0 {
return "positive"
}
return "negative"
})
})
fa := Right[string](-5)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("negative"), result)
}
// 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 := SG.First[string]()
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Left - should return first error
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(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 := SG.Last[string]()
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Left - should return last error
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// Last semigroup returns the last value, which is fa's error
assert.Equal(t, Left[int]("error2"), result)
}

406
v2/endomorphism/builder.go Normal file
View File

@@ -0,0 +1,406 @@
// 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 endomorphism
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/array"
)
// Build applies an endomorphism to the zero value of type A, effectively using
// the endomorphism as a builder pattern.
//
// # Endomorphism as Builder Pattern
//
// An endomorphism (a function from type A to type A) can be viewed as a builder pattern
// because it transforms a value of a type into another value of the same type. When you
// compose multiple endomorphisms together, you create a pipeline of transformations that
// build up a final value step by step.
//
// The Build function starts with the zero value of type A and applies the endomorphism
// to it, making it particularly useful for building complex values from scratch using
// a functional composition of transformations.
//
// # Builder Pattern Characteristics
//
// Traditional builder patterns have these characteristics:
// 1. Start with an initial (often empty) state
// 2. Apply a series of transformations/configurations
// 3. Return the final built object
//
// Endomorphisms provide the same pattern functionally:
// 1. Start with zero value: var a A
// 2. Apply composed endomorphisms: e(a)
// 3. Return the transformed value
//
// # Type Parameters
//
// - A: The type being built/transformed
//
// # Parameters
//
// - e: An endomorphism (or composition of endomorphisms) that transforms type A
//
// # Returns
//
// The result of applying the endomorphism to the zero value of type A
//
// # Example - Building a Configuration Object
//
// type Config struct {
// Host string
// Port int
// Timeout time.Duration
// Debug bool
// }
//
// // Define builder functions as endomorphisms
// withHost := func(host string) Endomorphism[Config] {
// return func(c Config) Config {
// c.Host = host
// return c
// }
// }
//
// withPort := func(port int) Endomorphism[Config] {
// return func(c Config) Config {
// c.Port = port
// return c
// }
// }
//
// withTimeout := func(d time.Duration) Endomorphism[Config] {
// return func(c Config) Config {
// c.Timeout = d
// return c
// }
// }
//
// withDebug := func(debug bool) Endomorphism[Config] {
// return func(c Config) Config {
// c.Debug = debug
// return c
// }
// }
//
// // Compose builders using monoid operations
// import M "github.com/IBM/fp-go/v2/monoid"
//
// configBuilder := M.ConcatAll(Monoid[Config]())(
// withHost("localhost"),
// withPort(8080),
// withTimeout(30 * time.Second),
// withDebug(true),
// )
//
// // Build the final configuration
// config := Build(configBuilder)
// // Result: Config{Host: "localhost", Port: 8080, Timeout: 30s, Debug: true}
//
// # Example - Building a String with Transformations
//
// import (
// "strings"
// M "github.com/IBM/fp-go/v2/monoid"
// )
//
// // Define string transformation endomorphisms
// appendHello := func(s string) string { return s + "Hello" }
// appendSpace := func(s string) string { return s + " " }
// appendWorld := func(s string) string { return s + "World" }
// toUpper := strings.ToUpper
//
// // Compose transformations
// stringBuilder := M.ConcatAll(Monoid[string]())(
// appendHello,
// appendSpace,
// appendWorld,
// toUpper,
// )
//
// // Build the final string from empty string
// result := Build(stringBuilder)
// // Result: "HELLO WORLD"
//
// # Example - Building a Slice with Operations
//
// type IntSlice []int
//
// appendValue := func(v int) Endomorphism[IntSlice] {
// return func(s IntSlice) IntSlice {
// return append(s, v)
// }
// }
//
// sortSlice := func(s IntSlice) IntSlice {
// sorted := make(IntSlice, len(s))
// copy(sorted, s)
// sort.Ints(sorted)
// return sorted
// }
//
// // Build a sorted slice
// sliceBuilder := M.ConcatAll(Monoid[IntSlice]())(
// appendValue(5),
// appendValue(2),
// appendValue(8),
// appendValue(1),
// sortSlice,
// )
//
// result := Build(sliceBuilder)
// // Result: IntSlice{1, 2, 5, 8}
//
// # Advantages of Endomorphism Builder Pattern
//
// 1. **Composability**: Builders can be composed using monoid operations
// 2. **Immutability**: Each transformation returns a new value (if implemented immutably)
// 3. **Type Safety**: The type system ensures all transformations work on the same type
// 4. **Reusability**: Individual builder functions can be reused and combined differently
// 5. **Testability**: Each transformation can be tested independently
// 6. **Declarative**: The composition clearly expresses the building process
//
// # Comparison with Traditional Builder Pattern
//
// Traditional OOP Builder:
//
// config := NewConfigBuilder().
// WithHost("localhost").
// WithPort(8080).
// WithTimeout(30 * time.Second).
// Build()
//
// Endomorphism Builder:
//
// config := Build(M.ConcatAll(Monoid[Config]())(
// withHost("localhost"),
// withPort(8080),
// withTimeout(30 * time.Second),
// ))
//
// Both achieve the same goal, but the endomorphism approach:
// - Uses pure functions instead of methods
// - Leverages algebraic properties (monoid) for composition
// - Allows for more flexible composition patterns
// - Integrates naturally with other functional programming constructs
func Build[A any](e Endomorphism[A]) A {
var a A
return e(a)
}
// ConcatAll combines multiple endomorphisms into a single endomorphism using composition.
//
// This function takes a slice of endomorphisms and combines them using the monoid's
// concat operation (which is composition). The resulting endomorphism, when applied,
// will execute all the input endomorphisms in RIGHT-TO-LEFT order (mathematical composition order).
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - ConcatAll([]Endomorphism{f, g, h}) creates an endomorphism that applies h, then g, then f
// - This is equivalent to f ∘ g ∘ h in mathematical notation
// - The last endomorphism in the slice is applied first
//
// If the slice is empty, returns the identity endomorphism.
//
// # Type Parameters
//
// - T: The type that the endomorphisms operate on
//
// # Parameters
//
// - es: A slice of endomorphisms to combine
//
// # Returns
//
// A single endomorphism that represents the composition of all input endomorphisms
//
// # Example - Basic Composition
//
// double := N.Mul(2)
// increment := N.Add(1)
// square := func(x int) int { return x * x }
//
// // Combine endomorphisms (RIGHT-TO-LEFT execution)
// combined := ConcatAll([]Endomorphism[int]{double, increment, square})
// result := combined(5)
// // Execution: square(5) = 25, increment(25) = 26, double(26) = 52
// // Result: 52
//
// # Example - Building with ConcatAll
//
// type Config struct {
// Host string
// Port int
// }
//
// withHost := func(host string) Endomorphism[Config] {
// return func(c Config) Config {
// c.Host = host
// return c
// }
// }
//
// withPort := func(port int) Endomorphism[Config] {
// return func(c Config) Config {
// c.Port = port
// return c
// }
// }
//
// // Combine configuration builders
// configBuilder := ConcatAll([]Endomorphism[Config]{
// withHost("localhost"),
// withPort(8080),
// })
//
// // Apply to zero value
// config := Build(configBuilder)
// // Result: Config{Host: "localhost", Port: 8080}
//
// # Example - Empty Slice
//
// // Empty slice returns identity
// identity := ConcatAll([]Endomorphism[int]{})
// result := identity(42) // Returns: 42
//
// # Relationship to Monoid
//
// ConcatAll is equivalent to using M.ConcatAll with the endomorphism Monoid:
//
// import M "github.com/IBM/fp-go/v2/monoid"
//
// // These are equivalent:
// result1 := ConcatAll(endomorphisms)
// result2 := M.ConcatAll(Monoid[T]())(endomorphisms)
//
// # Use Cases
//
// 1. **Pipeline Construction**: Build transformation pipelines from individual steps
// 2. **Configuration Building**: Combine multiple configuration setters
// 3. **Data Transformation**: Chain multiple data transformations
// 4. **Middleware Composition**: Combine middleware functions
// 5. **Validation Chains**: Compose multiple validation functions
func ConcatAll[T any](es []Endomorphism[T]) Endomorphism[T] {
return A.Reduce(es, MonadCompose[T], function.Identity[T])
}
// Reduce applies a slice of endomorphisms to the zero value of type T in LEFT-TO-RIGHT order.
//
// This function is a convenience wrapper that:
// 1. Starts with the zero value of type T
// 2. Applies each endomorphism in the slice from left to right
// 3. Returns the final transformed value
//
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
// - Reduce([]Endomorphism{f, g, h}) applies f first, then g, then h
// - This is the opposite of ConcatAll's RIGHT-TO-LEFT order
// - Each endomorphism receives the result of the previous one
//
// This is equivalent to: Build(ConcatAll(reverse(es))) but more efficient and clearer
// for left-to-right sequential application.
//
// # Type Parameters
//
// - T: The type being transformed
//
// # Parameters
//
// - es: A slice of endomorphisms to apply sequentially
//
// # Returns
//
// The final value after applying all endomorphisms to the zero value
//
// # Example - Sequential Transformations
//
// double := N.Mul(2)
// increment := N.Add(1)
// square := func(x int) int { return x * x }
//
// // Apply transformations LEFT-TO-RIGHT
// result := Reduce([]Endomorphism[int]{double, increment, square})
// // Execution: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
// // Result: 1
//
// // With a non-zero starting point, use a custom initial value:
// addTen := N.Add(10)
// result2 := Reduce([]Endomorphism[int]{addTen, double, increment})
// // Execution: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
// // Result: 21
//
// # Example - Building a String
//
// appendHello := func(s string) string { return s + "Hello" }
// appendSpace := func(s string) string { return s + " " }
// appendWorld := func(s string) string { return s + "World" }
//
// // Build string LEFT-TO-RIGHT
// result := Reduce([]Endomorphism[string]{
// appendHello,
// appendSpace,
// appendWorld,
// })
// // Execution: "" -> "Hello" -> "Hello " -> "Hello World"
// // Result: "Hello World"
//
// # Example - Configuration Building
//
// type Settings struct {
// Theme string
// FontSize int
// }
//
// withTheme := func(theme string) Endomorphism[Settings] {
// return func(s Settings) Settings {
// s.Theme = theme
// return s
// }
// }
//
// withFontSize := func(size int) Endomorphism[Settings] {
// return func(s Settings) Settings {
// s.FontSize = size
// return s
// }
// }
//
// // Build settings LEFT-TO-RIGHT
// settings := Reduce([]Endomorphism[Settings]{
// withTheme("dark"),
// withFontSize(14),
// })
// // Result: Settings{Theme: "dark", FontSize: 14}
//
// # Comparison with ConcatAll
//
// // ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
// endo := ConcatAll([]Endomorphism[int]{f, g, h})
// result1 := endo(value) // Applies h, then g, then f
//
// // Reduce: LEFT-TO-RIGHT application, returns final value
// result2 := Reduce([]Endomorphism[int]{f, g, h})
// // Applies f to zero, then g, then h
//
// # Use Cases
//
// 1. **Sequential Processing**: Apply transformations in order
// 2. **Pipeline Execution**: Execute a pipeline from start to finish
// 3. **Builder Pattern**: Build objects step by step
// 4. **State Machines**: Apply state transitions in sequence
// 5. **Data Flow**: Transform data through multiple stages
func Reduce[T any](es []Endomorphism[T]) T {
var t T
return A.Reduce(es, func(t T, e Endomorphism[T]) T { return e(t) }, t)
}

View File

@@ -0,0 +1,254 @@
// 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 endomorphism_test
import (
"fmt"
"time"
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/endomorphism"
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
)
// Example_build_basicUsage demonstrates basic usage of the Build function
// to construct a value from the zero value using endomorphisms.
func Example_build_basicUsage() {
// Define simple endomorphisms
addTen := N.Add(10)
double := N.Mul(2)
// Compose them using monoid (RIGHT-TO-LEFT execution)
// double is applied first, then addTen
builder := M.ConcatAll(endomorphism.Monoid[int]())(A.From(
addTen,
double,
))
// Build from zero value: 0 * 2 = 0, 0 + 10 = 10
result := endomorphism.Build(builder)
fmt.Println(result)
// Output: 10
}
// Example_build_configBuilder demonstrates using Build as a configuration builder pattern.
func Example_build_configBuilder() {
type Config struct {
Host string
Port int
Timeout time.Duration
Debug bool
}
// Define builder functions as endomorphisms
withHost := func(host string) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Host = host
return c
}
}
withPort := func(port int) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Port = port
return c
}
}
withTimeout := func(d time.Duration) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Timeout = d
return c
}
}
withDebug := func(debug bool) endomorphism.Endomorphism[Config] {
return func(c Config) Config {
c.Debug = debug
return c
}
}
// Compose builders using monoid
configBuilder := M.ConcatAll(endomorphism.Monoid[Config]())([]endomorphism.Endomorphism[Config]{
withHost("localhost"),
withPort(8080),
withTimeout(30 * time.Second),
withDebug(true),
})
// Build the configuration from zero value
config := endomorphism.Build(configBuilder)
fmt.Printf("Host: %s\n", config.Host)
fmt.Printf("Port: %d\n", config.Port)
fmt.Printf("Timeout: %v\n", config.Timeout)
fmt.Printf("Debug: %v\n", config.Debug)
// Output:
// Host: localhost
// Port: 8080
// Timeout: 30s
// Debug: true
}
// Example_build_stringBuilder demonstrates building a string using endomorphisms.
func Example_build_stringBuilder() {
// Define string transformation endomorphisms
appendHello := func(s string) string { return s + "Hello" }
appendSpace := func(s string) string { return s + " " }
appendWorld := func(s string) string { return s + "World" }
appendExclamation := func(s string) string { return s + "!" }
// Compose transformations (RIGHT-TO-LEFT execution)
stringBuilder := M.ConcatAll(endomorphism.Monoid[string]())([]endomorphism.Endomorphism[string]{
appendHello,
appendSpace,
appendWorld,
appendExclamation,
})
// Build the string from empty string
result := endomorphism.Build(stringBuilder)
fmt.Println(result)
// Output: !World Hello
}
// Example_build_personBuilder demonstrates building a complex struct using the builder pattern.
func Example_build_personBuilder() {
type Person struct {
FirstName string
LastName string
Age int
Email string
}
// Define builder functions
withFirstName := func(name string) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.FirstName = name
return p
}
}
withLastName := func(name string) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.LastName = name
return p
}
}
withAge := func(age int) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.Age = age
return p
}
}
withEmail := func(email string) endomorphism.Endomorphism[Person] {
return func(p Person) Person {
p.Email = email
return p
}
}
// Build a person
personBuilder := M.ConcatAll(endomorphism.Monoid[Person]())([]endomorphism.Endomorphism[Person]{
withFirstName("Alice"),
withLastName("Smith"),
withAge(30),
withEmail("alice.smith@example.com"),
})
person := endomorphism.Build(personBuilder)
fmt.Printf("%s %s, Age: %d, Email: %s\n",
person.FirstName, person.LastName, person.Age, person.Email)
// Output: Alice Smith, Age: 30, Email: alice.smith@example.com
}
// Example_build_conditionalBuilder demonstrates conditional building using endomorphisms.
func Example_build_conditionalBuilder() {
type Settings struct {
Theme string
FontSize int
AutoSave bool
Animations bool
}
withTheme := func(theme string) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.Theme = theme
return s
}
}
withFontSize := func(size int) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.FontSize = size
return s
}
}
withAutoSave := func(enabled bool) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.AutoSave = enabled
return s
}
}
withAnimations := func(enabled bool) endomorphism.Endomorphism[Settings] {
return func(s Settings) Settings {
s.Animations = enabled
return s
}
}
// Build settings conditionally
isDarkMode := true
isAccessibilityMode := true
// Note: Monoid executes RIGHT-TO-LEFT, so later items in the slice are applied first
// We need to add items in reverse order for the desired effect
builders := []endomorphism.Endomorphism[Settings]{}
if isAccessibilityMode {
builders = append(builders, withFontSize(18)) // Will be applied last (overrides)
builders = append(builders, withAnimations(false))
}
if isDarkMode {
builders = append(builders, withTheme("dark"))
} else {
builders = append(builders, withTheme("light"))
}
builders = append(builders, withAutoSave(true))
builders = append(builders, withFontSize(14)) // Will be applied first
settingsBuilder := M.ConcatAll(endomorphism.Monoid[Settings]())(builders)
settings := endomorphism.Build(settingsBuilder)
fmt.Printf("Theme: %s\n", settings.Theme)
fmt.Printf("FontSize: %d\n", settings.FontSize)
fmt.Printf("AutoSave: %v\n", settings.AutoSave)
fmt.Printf("Animations: %v\n", settings.Animations)
// Output:
// Theme: dark
// FontSize: 18
// AutoSave: true
// Animations: false
}

View File

@@ -36,8 +36,8 @@
// )
//
// // Define some endomorphisms
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
//
// // Compose them (RIGHT-TO-LEFT execution)
// composed := endomorphism.Compose(double, increment)
@@ -62,9 +62,9 @@
//
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(
// func(x int) int { return x * 2 }, // applied third
// func(x int) int { return x + 1 }, // applied second
// func(x int) int { return x * 3 }, // applied first
// N.Mul(2), // applied third
// N.Add(1), // applied second
// N.Mul(3), // applied first
// )
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
//
@@ -74,8 +74,8 @@
// MonadChain executes LEFT-TO-RIGHT, unlike Compose:
//
// // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
// f := func(x int) int { return x * 2 }
// g := func(x int) int { return x + 1 }
// f := N.Mul(2)
// g := N.Add(1)
// chained := endomorphism.MonadChain(f, g) // f first, then g
// result := chained(5) // (5 * 2) + 1 = 11
//
@@ -83,8 +83,8 @@
//
// The key difference between Compose and Chain/MonadChain is execution order:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
//
// // Compose: RIGHT-TO-LEFT (mathematical composition)
// composed := endomorphism.Compose(double, increment)

View File

@@ -37,8 +37,8 @@ import (
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
// // result(5) = double(increment(5)) = double(6) = 12
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
@@ -62,9 +62,9 @@ func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// applyIncrement := endomorphism.Ap(increment)
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// composed := applyIncrement(double) // double ∘ increment
// // composed(5) = double(increment(5)) = double(6) = 12
func Ap[A any](fa Endomorphism[A]) Operator[A] {
@@ -91,8 +91,8 @@ func Ap[A any](fa Endomorphism[A]) Operator[A] {
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
//
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
// composed := endomorphism.MonadCompose(double, increment)
@@ -123,8 +123,8 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
// mapped := endomorphism.MonadMap(double, increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
@@ -151,9 +151,9 @@ func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// composeWithIncrement := endomorphism.Compose(increment)
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
//
// // Composes double with increment (RIGHT-TO-LEFT: increment first, then double)
// composed := composeWithIncrement(double)
@@ -186,9 +186,9 @@ func Compose[A any](g Endomorphism[A]) Operator[A] {
//
// Example:
//
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// mapDouble := endomorphism.Map(double)
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// mapped := mapDouble(increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func Map[A any](f Endomorphism[A]) Operator[A] {
@@ -215,8 +215,8 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
//
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
// chained := endomorphism.MonadChain(double, increment)
@@ -243,7 +243,7 @@ func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
//
// Example:
//
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// log := func(x int) int { fmt.Println(x); return x }
// chained := endomorphism.MonadChainFirst(double, log)
// result := chained(5) // Prints 10, returns 10
@@ -269,7 +269,7 @@ func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[
//
// log := func(x int) int { fmt.Println(x); return x }
// chainLog := endomorphism.ChainFirst(log)
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// chained := chainLog(double)
// result := chained(5) // Prints 10, returns 10
func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
@@ -294,9 +294,9 @@ func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// increment := N.Add(1)
// chainWithIncrement := endomorphism.Chain(increment)
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
//
// // Chains double (first) with increment (second)
// chained := chainWithIncrement(double)
@@ -304,3 +304,85 @@ func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
func Chain[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChain, f)
}
// Flatten collapses a nested endomorphism into a single endomorphism.
//
// Given an endomorphism that transforms endomorphisms (Endomorphism[Endomorphism[A]]),
// Flatten produces a simple endomorphism by applying the outer transformation to the
// identity function. This is the monadic join operation for the Endomorphism monad.
//
// The function applies the nested endomorphism to Identity[A] to extract the inner
// endomorphism, effectively "flattening" the two layers into one.
//
// Type Parameters:
// - A: The type being transformed by the endomorphisms
//
// Parameters:
// - mma: A nested endomorphism that transforms endomorphisms
//
// Returns:
// - An endomorphism that applies the transformation directly to values of type A
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// // An endomorphism that wraps another endomorphism
// addThenDouble := func(endo Endomorphism[Counter]) Endomorphism[Counter] {
// return func(c Counter) Counter {
// c = endo(c) // Apply the input endomorphism
// c.Value = c.Value * 2 // Then double
// return c
// }
// }
//
// flattened := Flatten(addThenDouble)
// result := flattened(Counter{Value: 5}) // Counter{Value: 10}
func Flatten[A any](mma Endomorphism[Endomorphism[A]]) Endomorphism[A] {
return mma(function.Identity[A])
}
// Join performs self-application of a function that produces endomorphisms.
//
// Given a function that takes a value and returns an endomorphism of that same type,
// Join creates an endomorphism that applies the value to itself through the function.
// This operation is also known as the W combinator (warbler) in combinatory logic,
// or diagonal application.
//
// The resulting endomorphism evaluates f(a)(a), applying the same value a to both
// the function f and the resulting endomorphism.
//
// Type Parameters:
// - A: The type being transformed
//
// Parameters:
// - f: A function that takes a value and returns an endomorphism of that type
//
// Returns:
// - An endomorphism that performs self-application: f(a)(a)
//
// Example:
//
// type Point struct {
// X, Y int
// }
//
// // Create an endomorphism based on the input point
// scaleBy := func(p Point) Endomorphism[Point] {
// return func(p2 Point) Point {
// return Point{
// X: p2.X * p.X,
// Y: p2.Y * p.Y,
// }
// }
// }
//
// selfScale := Join(scaleBy)
// result := selfScale(Point{X: 3, Y: 4}) // Point{X: 9, Y: 16}
func Join[A any](f Kleisli[A]) Endomorphism[A] {
return func(a A) A {
return f(a)(a)
}
}

View File

@@ -19,6 +19,7 @@ import (
"testing"
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
@@ -204,8 +205,8 @@ func TestCompose(t *testing.T) {
// TestMonadComposeVsCompose demonstrates the relationship between MonadCompose and Compose
func TestMonadComposeVsCompose(t *testing.T) {
double := func(x int) int { return x * 2 }
increment := func(x int) int { return x + 1 }
double := N.Mul(2)
increment := N.Add(1)
// MonadCompose takes both functions at once
monadComposed := MonadCompose(double, increment)
@@ -448,7 +449,7 @@ func TestOperatorType(t *testing.T) {
func BenchmarkCompose(b *testing.B) {
composed := MonadCompose(double, increment)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = composed(5)
}
}
@@ -456,8 +457,8 @@ func BenchmarkCompose(b *testing.B) {
// BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid
// TestComposeVsChain demonstrates the key difference between Compose and Chain
func TestComposeVsChain(t *testing.T) {
double := func(x int) int { return x * 2 }
increment := func(x int) int { return x + 1 }
double := N.Mul(2)
increment := N.Add(1)
// Compose executes RIGHT-TO-LEFT
// Compose(double, increment) means: increment first, then double
@@ -499,7 +500,7 @@ func BenchmarkMonoidConcatAll(b *testing.B) {
monoid := Monoid[int]()
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = combined(5)
}
}
@@ -509,7 +510,7 @@ func BenchmarkChain(b *testing.B) {
chainWithIncrement := Chain(increment)
chained := chainWithIncrement(double)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = chained(5)
}
}
@@ -704,7 +705,7 @@ func TestApEqualsCompose(t *testing.T) {
// TestChainFirst tests the ChainFirst operation
func TestChainFirst(t *testing.T) {
double := func(x int) int { return x * 2 }
double := N.Mul(2)
// Track side effect
var sideEffect int
@@ -721,3 +722,352 @@ func TestChainFirst(t *testing.T) {
// But side effect should have been executed with double's result
assert.Equal(t, 10, sideEffect, "ChainFirst should execute second function for effect")
}
// TestBuild tests the Build function
func TestBuild(t *testing.T) {
t.Run("build with single transformation", func(t *testing.T) {
// Build applies endomorphism to zero value
result := Build(double)
assert.Equal(t, 0, result, "Build(double) on zero value should be 0")
})
t.Run("build with composed transformations", func(t *testing.T) {
// Create a builder that starts from zero and applies transformations
builder := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
N.Add(10),
N.Mul(2),
N.Add(5),
})
result := Build(builder)
// RIGHT-TO-LEFT: 0 + 5 = 5, 5 * 2 = 10, 10 + 10 = 20
assert.Equal(t, 20, result, "Build should apply composed transformations to zero value")
})
t.Run("build with identity", func(t *testing.T) {
result := Build(Identity[int]())
assert.Equal(t, 0, result, "Build(identity) should return zero value")
})
t.Run("build string from empty", func(t *testing.T) {
builder := M.ConcatAll(Monoid[string]())([]Endomorphism[string]{
func(s string) string { return s + "Hello" },
func(s string) string { return s + " " },
func(s string) string { return s + "World" },
})
result := Build(builder)
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
assert.Equal(t, "World Hello", result, "Build should work with strings")
})
t.Run("build struct with builder pattern", func(t *testing.T) {
type Config struct {
Host string
Port int
}
withHost := func(host string) Endomorphism[Config] {
return func(c Config) Config {
c.Host = host
return c
}
}
withPort := func(port int) Endomorphism[Config] {
return func(c Config) Config {
c.Port = port
return c
}
}
builder := M.ConcatAll(Monoid[Config]())([]Endomorphism[Config]{
withHost("localhost"),
withPort(8080),
})
result := Build(builder)
assert.Equal(t, "localhost", result.Host, "Build should set Host")
assert.Equal(t, 8080, result.Port, "Build should set Port")
})
t.Run("build slice with operations", func(t *testing.T) {
type IntSlice []int
appendValue := func(v int) Endomorphism[IntSlice] {
return func(s IntSlice) IntSlice {
return append(s, v)
}
}
builder := M.ConcatAll(Monoid[IntSlice]())([]Endomorphism[IntSlice]{
appendValue(1),
appendValue(2),
appendValue(3),
})
result := Build(builder)
// RIGHT-TO-LEFT: append 3, append 2, append 1
assert.Equal(t, IntSlice{3, 2, 1}, result, "Build should construct slice")
})
}
// TestBuildAsBuilderPattern demonstrates using Build as a builder pattern
func TestBuildAsBuilderPattern(t *testing.T) {
type Person struct {
Name string
Age int
Email string
Active bool
}
// Define builder functions
withName := func(name string) Endomorphism[Person] {
return func(p Person) Person {
p.Name = name
return p
}
}
withAge := func(age int) Endomorphism[Person] {
return func(p Person) Person {
p.Age = age
return p
}
}
withEmail := func(email string) Endomorphism[Person] {
return func(p Person) Person {
p.Email = email
return p
}
}
withActive := func(active bool) Endomorphism[Person] {
return func(p Person) Person {
p.Active = active
return p
}
}
// Build a person using the builder pattern
personBuilder := M.ConcatAll(Monoid[Person]())([]Endomorphism[Person]{
withName("Alice"),
withAge(30),
withEmail("alice@example.com"),
withActive(true),
})
person := Build(personBuilder)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
assert.Equal(t, "alice@example.com", person.Email)
assert.True(t, person.Active)
}
// TestConcatAll tests the ConcatAll function
func TestConcatAll(t *testing.T) {
t.Run("concat all with multiple endomorphisms", func(t *testing.T) {
// ConcatAll executes RIGHT-TO-LEFT
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
result := combined(5)
// RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 52, result, "ConcatAll should execute right-to-left")
})
t.Run("concat all with empty slice", func(t *testing.T) {
// Empty slice should return identity
identity := ConcatAll([]Endomorphism[int]{})
result := identity(42)
assert.Equal(t, 42, result, "ConcatAll with empty slice should return identity")
})
t.Run("concat all with single endomorphism", func(t *testing.T) {
combined := ConcatAll([]Endomorphism[int]{double})
result := combined(5)
assert.Equal(t, 10, result, "ConcatAll with single endomorphism should apply it")
})
t.Run("concat all with two endomorphisms", func(t *testing.T) {
// RIGHT-TO-LEFT: increment first, then double
combined := ConcatAll([]Endomorphism[int]{double, increment})
result := combined(5)
assert.Equal(t, 12, result, "ConcatAll should execute right-to-left: (5 + 1) * 2 = 12")
})
t.Run("concat all with strings", func(t *testing.T) {
appendHello := func(s string) string { return s + "Hello" }
appendSpace := func(s string) string { return s + " " }
appendWorld := func(s string) string { return s + "World" }
// RIGHT-TO-LEFT execution
combined := ConcatAll([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
result := combined("")
// RIGHT-TO-LEFT: "" + "World" = "World", "World" + " " = "World ", "World " + "Hello" = "World Hello"
assert.Equal(t, "World Hello", result, "ConcatAll should work with strings")
})
t.Run("concat all for building structs", func(t *testing.T) {
type Config struct {
Host string
Port int
}
withHost := func(host string) Endomorphism[Config] {
return func(c Config) Config {
c.Host = host
return c
}
}
withPort := func(port int) Endomorphism[Config] {
return func(c Config) Config {
c.Port = port
return c
}
}
combined := ConcatAll([]Endomorphism[Config]{
withHost("localhost"),
withPort(8080),
})
result := combined(Config{})
assert.Equal(t, "localhost", result.Host)
assert.Equal(t, 8080, result.Port)
})
t.Run("concat all is equivalent to monoid ConcatAll", func(t *testing.T) {
endos := []Endomorphism[int]{double, increment, square}
result1 := ConcatAll(endos)(5)
result2 := M.ConcatAll(Monoid[int]())(endos)(5)
assert.Equal(t, result1, result2, "ConcatAll should be equivalent to M.ConcatAll(Monoid())")
})
}
// TestReduce tests the Reduce function
func TestReduce(t *testing.T) {
t.Run("reduce with multiple endomorphisms", func(t *testing.T) {
// Reduce executes LEFT-TO-RIGHT starting from zero value
result := Reduce([]Endomorphism[int]{double, increment, square})
// LEFT-TO-RIGHT: 0 -> double(0) = 0 -> increment(0) = 1 -> square(1) = 1
assert.Equal(t, 1, result, "Reduce should execute left-to-right from zero value")
})
t.Run("reduce with empty slice", func(t *testing.T) {
// Empty slice should return zero value
result := Reduce([]Endomorphism[int]{})
assert.Equal(t, 0, result, "Reduce with empty slice should return zero value")
})
t.Run("reduce with single endomorphism", func(t *testing.T) {
addTen := N.Add(10)
result := Reduce([]Endomorphism[int]{addTen})
// 0 + 10 = 10
assert.Equal(t, 10, result, "Reduce with single endomorphism should apply it to zero")
})
t.Run("reduce with sequential transformations", func(t *testing.T) {
addTen := N.Add(10)
// LEFT-TO-RIGHT: 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
result := Reduce([]Endomorphism[int]{addTen, double, increment})
assert.Equal(t, 21, result, "Reduce should apply transformations left-to-right")
})
t.Run("reduce with strings", func(t *testing.T) {
appendHello := func(s string) string { return s + "Hello" }
appendSpace := func(s string) string { return s + " " }
appendWorld := func(s string) string { return s + "World" }
// LEFT-TO-RIGHT execution
result := Reduce([]Endomorphism[string]{appendHello, appendSpace, appendWorld})
// "" -> "Hello" -> "Hello " -> "Hello World"
assert.Equal(t, "Hello World", result, "Reduce should work with strings left-to-right")
})
t.Run("reduce for building structs", func(t *testing.T) {
type Settings struct {
Theme string
FontSize int
}
withTheme := func(theme string) Endomorphism[Settings] {
return func(s Settings) Settings {
s.Theme = theme
return s
}
}
withFontSize := func(size int) Endomorphism[Settings] {
return func(s Settings) Settings {
s.FontSize = size
return s
}
}
// LEFT-TO-RIGHT application
result := Reduce([]Endomorphism[Settings]{
withTheme("dark"),
withFontSize(14),
})
assert.Equal(t, "dark", result.Theme)
assert.Equal(t, 14, result.FontSize)
})
t.Run("reduce is equivalent to Build(ConcatAll(reverse))", func(t *testing.T) {
addTen := N.Add(10)
endos := []Endomorphism[int]{addTen, double, increment}
// Reduce applies left-to-right
result1 := Reduce(endos)
// Reverse and use ConcatAll (which is right-to-left)
reversed := []Endomorphism[int]{increment, double, addTen}
result2 := Build(ConcatAll(reversed))
assert.Equal(t, result1, result2, "Reduce should be equivalent to Build(ConcatAll(reverse))")
})
}
// TestConcatAllVsReduce demonstrates the difference between ConcatAll and Reduce
func TestConcatAllVsReduce(t *testing.T) {
addTen := N.Add(10)
endos := []Endomorphism[int]{addTen, double, increment}
// ConcatAll: RIGHT-TO-LEFT composition, returns endomorphism
concatResult := ConcatAll(endos)(5)
// 5 -> increment(5) = 6 -> double(6) = 12 -> addTen(12) = 22
// Reduce: LEFT-TO-RIGHT application, returns value from zero
reduceResult := Reduce(endos)
// 0 -> addTen(0) = 10 -> double(10) = 20 -> increment(20) = 21
assert.NotEqual(t, concatResult, reduceResult, "ConcatAll and Reduce should produce different results")
assert.Equal(t, 22, concatResult, "ConcatAll should execute right-to-left on input value")
assert.Equal(t, 21, reduceResult, "Reduce should execute left-to-right from zero value")
}
// TestReduceWithBuild demonstrates using Reduce vs Build with ConcatAll
func TestReduceWithBuild(t *testing.T) {
addFive := N.Add(5)
multiplyByThree := N.Mul(3)
endos := []Endomorphism[int]{addFive, multiplyByThree}
// Reduce: LEFT-TO-RIGHT from zero
reduceResult := Reduce(endos)
// 0 -> addFive(0) = 5 -> multiplyByThree(5) = 15
assert.Equal(t, 15, reduceResult)
// Build with ConcatAll: RIGHT-TO-LEFT from zero
buildResult := Build(ConcatAll(endos))
// 0 -> multiplyByThree(0) = 0 -> addFive(0) = 5
assert.Equal(t, 5, buildResult)
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
}

10
v2/endomorphism/from.go Normal file
View File

@@ -0,0 +1,10 @@
package endomorphism
import (
"github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
func FromSemigroup[A any](s S.Semigroup[A]) Kleisli[A] {
return function.Bind2of2(s.Concat)
}

View File

@@ -35,7 +35,7 @@ import (
//
// Example:
//
// myFunc := func(x int) int { return x * 2 }
// myFunc := N.Mul(2)
// endo := endomorphism.Of(myFunc)
func Of[F ~func(A) A, A any](f F) Endomorphism[A] {
return f
@@ -75,7 +75,7 @@ func Unwrap[F ~func(A) A, A any](f Endomorphism[A]) F {
// result := id(42) // Returns: 42
//
// // Identity is neutral for composition
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// composed := endomorphism.Compose(id, double)
// // composed behaves exactly like double
func Identity[A any]() Endomorphism[A] {
@@ -103,8 +103,8 @@ func Identity[A any]() Endomorphism[A] {
// import S "github.com/IBM/fp-go/v2/semigroup"
//
// sg := endomorphism.Semigroup[int]()
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
//
// // Combine using the semigroup (RIGHT-TO-LEFT execution)
// combined := sg.Concat(double, increment)
@@ -139,8 +139,8 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// import M "github.com/IBM/fp-go/v2/monoid"
//
// monoid := endomorphism.Monoid[int]()
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
// square := func(x int) int { return x * x }
//
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)

View File

@@ -29,8 +29,8 @@ type (
// Example:
//
// // Simple endomorphisms on integers
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// double := N.Mul(2)
// increment := N.Add(1)
//
// // Both are endomorphisms of type Endomorphism[int]
// var f endomorphism.Endomorphism[int] = double

View File

@@ -23,6 +23,7 @@ import (
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -266,7 +267,7 @@ func TestEither(t *testing.T) {
erased := Erase(42)
result := F.Pipe1(
SafeUnerase[int](erased),
E.Map[error](func(x int) int { return x * 2 }),
E.Map[error](N.Mul(2)),
)
assert.True(t, E.IsRight(result))

View File

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

View File

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

View File

@@ -80,7 +80,6 @@ import (
A "github.com/IBM/fp-go/v2/array"
B "github.com/IBM/fp-go/v2/bytes"
E "github.com/IBM/fp-go/v2/either"
ENDO "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
C "github.com/IBM/fp-go/v2/http/content"
@@ -91,16 +90,17 @@ import (
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
)
type (
Builder struct {
method O.Option[string]
method Option[string]
url string
headers http.Header
body O.Option[E.Either[error, []byte]]
body Option[Result[[]byte]]
query url.Values
}
@@ -117,19 +117,19 @@ var (
// Monoid is the [M.Monoid] for the [Endomorphism]
Monoid = ENDO.Monoid[*Builder]()
// Url is a [L.Lens] for the URL
// Url is a [Lens] for the URL
//
// Deprecated: use [URL] instead
Url = L.MakeLensRef((*Builder).GetURL, (*Builder).SetURL)
// URL is a [L.Lens] for the URL
// URL is a [Lens] for the URL
URL = L.MakeLensRef((*Builder).GetURL, (*Builder).SetURL)
// Method is a [L.Lens] for the HTTP method
// Method is a [Lens] for the HTTP method
Method = L.MakeLensRef((*Builder).GetMethod, (*Builder).SetMethod)
// Body is a [L.Lens] for the request body
// Body is a [Lens] for the request body
Body = L.MakeLensRef((*Builder).GetBody, (*Builder).SetBody)
// Headers is a [L.Lens] for the complete set of request headers
// Headers is a [Lens] for the complete set of request headers
Headers = L.MakeLensRef((*Builder).GetHeaders, (*Builder).SetHeaders)
// Query is a [L.Lens] for the set of query parameters
// Query is a [Lens] for the set of query parameters
Query = L.MakeLensRef((*Builder).GetQuery, (*Builder).SetQuery)
rawQuery = L.MakeLensRef(getRawQuery, setRawQuery)
@@ -139,11 +139,11 @@ var (
setHeader = F.Bind2of3((*Builder).SetHeader)
noHeader = O.None[string]()
noBody = O.None[E.Either[error, []byte]]()
noBody = O.None[Result[[]byte]]()
noQueryArg = O.None[string]()
parseURL = E.Eitherize1(url.Parse)
parseQuery = E.Eitherize1(url.ParseQuery)
parseURL = result.Eitherize1(url.Parse)
parseQuery = result.Eitherize1(url.ParseQuery)
// WithQuery creates a [Endomorphism] for a complete set of query parameters
WithQuery = Query.Set
@@ -159,12 +159,12 @@ var (
WithHeaders = Headers.Set
// WithBody creates a [Endomorphism] for a request body
WithBody = F.Flow2(
O.Of[E.Either[error, []byte]],
O.Of[Result[[]byte]],
Body.Set,
)
// WithBytes creates a [Endomorphism] for a request body using bytes
WithBytes = F.Flow2(
E.Of[error, []byte],
result.Of[[]byte],
WithBody,
)
// WithContentType adds the [H.ContentType] header
@@ -202,7 +202,7 @@ var (
)
// bodyAsBytes returns a []byte with a fallback to the empty array
bodyAsBytes = O.Fold(B.Empty, E.Fold(F.Ignore1of1[error](B.Empty), F.Identity[[]byte]))
bodyAsBytes = O.Fold(B.Empty, result.Fold(F.Ignore1of1[error](B.Empty), F.Identity[[]byte]))
)
func setRawQuery(u *url.URL, raw string) *url.URL {
@@ -223,35 +223,35 @@ func (builder *Builder) clone() *Builder {
// GetTargetUrl constructs a full URL with query parameters on top of the provided URL string
//
// Deprecated: use [GetTargetURL] instead
func (builder *Builder) GetTargetUrl() E.Either[error, string] {
func (builder *Builder) GetTargetUrl() Result[string] {
return builder.GetTargetURL()
}
// GetTargetURL constructs a full URL with query parameters on top of the provided URL string
func (builder *Builder) GetTargetURL() E.Either[error, string] {
func (builder *Builder) GetTargetURL() Result[string] {
// construct the final URL
return F.Pipe3(
builder,
Url.Get,
parseURL,
E.Chain(F.Flow4(
result.Chain(F.Flow4(
T.Replicate2[*url.URL],
T.Map2(
F.Flow2(
F.Curry2(setRawQuery),
E.Of[error, func(string) *url.URL],
result.Of[func(string) *url.URL],
),
F.Flow3(
rawQuery.Get,
parseQuery,
E.Map[error](F.Flow2(
result.Map(F.Flow2(
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
(url.Values).Encode,
)),
),
),
T.Tupled2(E.MonadAp[*url.URL, error, string]),
E.Map[error]((*url.URL).String),
T.Tupled2(result.MonadAp[*url.URL, string]),
result.Map((*url.URL).String),
)),
)
}
@@ -285,7 +285,7 @@ func (builder *Builder) SetQuery(query url.Values) *Builder {
return builder
}
func (builder *Builder) GetBody() O.Option[E.Either[error, []byte]] {
func (builder *Builder) GetBody() Option[Result[[]byte]] {
return builder.body
}
@@ -310,7 +310,7 @@ func (builder *Builder) SetHeaders(headers http.Header) *Builder {
return builder
}
func (builder *Builder) SetBody(body O.Option[E.Either[error, []byte]]) *Builder {
func (builder *Builder) SetBody(body Option[Result[[]byte]]) *Builder {
builder.body = body
return builder
}
@@ -325,7 +325,7 @@ func (builder *Builder) DelHeader(name string) *Builder {
return builder
}
func (builder *Builder) GetHeader(name string) O.Option[string] {
func (builder *Builder) GetHeader(name string) Option[string] {
return F.Pipe2(
name,
builder.headers.Get,
@@ -342,8 +342,8 @@ func (builder *Builder) GetHash() string {
return MakeHash(builder)
}
// Header returns a [L.Lens] for a single header
func Header(name string) L.Lens[*Builder, O.Option[string]] {
// Header returns a [Lens] for a single header
func Header(name string) Lens[*Builder, Option[string]] {
get := getHeader(name)
set := F.Bind1of2(setHeader(name))
del := F.Flow2(
@@ -351,7 +351,7 @@ func Header(name string) L.Lens[*Builder, O.Option[string]] {
LZ.Map(delHeader(name)),
)
return L.MakeLens(get, func(b *Builder, value O.Option[string]) *Builder {
return L.MakeLens(get, func(b *Builder, value Option[string]) *Builder {
cpy := b.clone()
return F.Pipe1(
value,
@@ -392,8 +392,8 @@ func WithJSON[T any](data T) Endomorphism {
)
}
// QueryArg is a [L.Lens] for the first value of a query argument
func QueryArg(name string) L.Lens[*Builder, O.Option[string]] {
// QueryArg is a [Lens] for the first value of a query argument
func QueryArg(name string) Lens[*Builder, Option[string]] {
return F.Pipe1(
Query,
L.Compose[*Builder](FM.AtValue(name)),

13
v2/http/builder/type.go Normal file
View File

@@ -0,0 +1,13 @@
package builder
import (
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
Lens[S, T any] = lens.Lens[S, T]
)

View File

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

View File

@@ -0,0 +1,226 @@
# Idiomatic Package Review Summary
**Date:** 2025-11-26
**Reviewer:** Code Review Assistant
## Overview
This document summarizes the comprehensive review of the `idiomatic` package and its subpackages, including documentation fixes, additions, and test coverage analysis.
## Documentation Improvements
### 1. Main Package (`idiomatic/`)
-**Status:** Documentation is comprehensive and well-structured
- **File:** `doc.go` (505 lines)
- **Quality:** Excellent - includes overview, performance comparisons, usage examples, and best practices
### 2. Option Package (`idiomatic/option/`)
-**Fixed:** Added missing copyright headers to `types.go` and `function.go`
-**Fixed:** Added comprehensive documentation for type aliases in `types.go`
-**Fixed:** Enhanced function documentation in `function.go` with examples
-**Fixed:** Added missing documentation for `FromZero`, `FromNonZero`, and `FromEq` functions
- **Files Updated:**
- `types.go` - Added copyright header and type documentation
- `function.go` - Added copyright header and improved function docs
- `option.go` - Enhanced documentation for utility functions
### 3. Result Package (`idiomatic/result/`)
-**Fixed:** Added missing copyright header to `function.go`
-**Fixed:** Enhanced function documentation with examples
- **Files Updated:**
- `function.go` - Added copyright header and improved documentation
- `types.go` - Already had good documentation
### 4. IOResult Package (`idiomatic/ioresult/`)
-**Status:** Documentation is comprehensive
- **File:** `doc.go` (198 lines)
- **Quality:** Excellent - includes detailed explanations of IO operations, lazy evaluation, and side effects
### 5. ReaderIOResult Package (`idiomatic/readerioresult/`)
-**Created:** New `doc.go` file (96 lines)
-**Fixed:** Added comprehensive type documentation to `types.go`
- **New Documentation Includes:**
- Package overview and use cases
- Basic usage examples
- Composition patterns
- Error handling strategies
- Relationship to other monads
### 6. ReaderResult Package (`idiomatic/readerresult/`)
-**Fixed:** Added comprehensive type documentation to `types.go`
- **Existing:** `doc.go` already present (178 lines) with excellent documentation
## Test Coverage Analysis
### Option Package Tests
**File:** `idiomatic/option/option_test.go`
**Existing Coverage:**
-`IsNone` - Tested
-`IsSome` - Tested
-`Map` - Tested
-`Ap` - Tested
-`Chain` - Tested
-`ChainTo` - Comprehensive tests with multiple scenarios
**Missing Tests (Commented Out):**
- ⚠️ `Flatten` - Test commented out
- ⚠️ `Fold` - Test commented out
- ⚠️ `FromPredicate` - Test commented out
- ⚠️ `Alt` - Test commented out
**Recommendations:**
1. Uncomment and fix the commented-out tests
2. Add tests for:
- `FromZero`
- `FromNonZero`
- `FromEq`
- `FromNillable`
- `MapTo`
- `GetOrElse`
- `ChainFirst`
- `Reduce`
- `Filter`
- `Flap`
- `ToString`
### Result Package Tests
**File:** `idiomatic/result/either_test.go`
**Existing Coverage:**
-`IsLeft` - Tested
-`IsRight` - Tested
-`Map` - Tested
-`Ap` - Tested
-`Alt` - Tested
-`ChainFirst` - Tested
-`ChainOptionK` - Tested
-`FromOption` - Tested
-`ToString` - Tested
**Missing Tests:**
- ⚠️ `Of` - Not explicitly tested
- ⚠️ `BiMap` - Not tested
- ⚠️ `MapTo` - Not tested
- ⚠️ `MapLeft` - Not tested
- ⚠️ `Chain` - Not tested
- ⚠️ `ChainTo` - Not tested
- ⚠️ `ToOption` - Not tested
- ⚠️ `FromError` - Not tested
- ⚠️ `ToError` - Not tested
- ⚠️ `Fold` - Not tested
- ⚠️ `FromPredicate` - Not tested
- ⚠️ `FromNillable` - Not tested
- ⚠️ `GetOrElse` - Not tested
- ⚠️ `Reduce` - Not tested
- ⚠️ `OrElse` - Not tested
- ⚠️ `ToType` - Not tested
- ⚠️ `Memoize` - Not tested
- ⚠️ `Flap` - Not tested
### IOResult Package Tests
**File:** `idiomatic/ioresult/monad_test.go`
**Existing Coverage:****EXCELLENT**
- ✅ Comprehensive monad law tests (left identity, right identity, associativity)
- ✅ Functor law tests (composition, identity)
- ✅ Pointed, Functor, and Monad interface tests
- ✅ Parallel vs Sequential execution tests
- ✅ Integration tests with complex pipelines
- ✅ Error handling scenarios
**Status:** This package has exemplary test coverage and can serve as a model for other packages.
### ReaderIOResult Package
**Status:** ⚠️ **NO TESTS FOUND**
**Recommendations:**
Create comprehensive test suite covering:
- Basic construction and execution
- Map, Chain, Ap operations
- Error handling
- Environment dependency injection
- Integration with IOResult
### ReaderResult Package
**Files:** Multiple test files exist
- `array_test.go`
- `bind_test.go`
- `curry_test.go`
- `from_test.go`
- `monoid_test.go`
- `reader_test.go`
- `sequence_test.go`
**Status:** ✅ Good coverage exists
## Subpackages Review
### Packages Requiring Review:
1. **idiomatic/option/number/** - Needs documentation and test review
2. **idiomatic/option/testing/** - Contains disabled test files (`laws_test._go`, `laws._go`)
3. **idiomatic/result/exec/** - Needs review
4. **idiomatic/result/http/** - Needs review
5. **idiomatic/result/testing/** - Contains disabled test files
6. **idiomatic/ioresult/exec/** - Needs review
7. **idiomatic/ioresult/file/** - Needs review
8. **idiomatic/ioresult/http/** - Needs review
9. **idiomatic/ioresult/http/builder/** - Needs review
10. **idiomatic/ioresult/testing/** - Needs review
## Priority Recommendations
### High Priority
1. **Enable Commented Tests:** Uncomment and fix tests in `option/option_test.go`
2. **Add Missing Option Tests:** Create tests for all untested functions in option package
3. **Add Missing Result Tests:** Create comprehensive test suite for result package
4. **Create ReaderIOResult Tests:** This package has no tests at all
### Medium Priority
5. **Review Subpackages:** Systematically review exec, file, http, and testing subpackages
6. **Enable Testing Package Tests:** Investigate why `laws_test._go` files are disabled
### Low Priority
7. **Benchmark Tests:** Consider adding benchmark tests for performance-critical operations
8. **Property-Based Tests:** Consider adding property-based tests using testing/quick
## Files Modified in This Review
1. `idiomatic/option/types.go` - Added copyright and documentation
2. `idiomatic/option/function.go` - Added copyright and enhanced docs
3. `idiomatic/option/option.go` - Enhanced function documentation
4. `idiomatic/result/function.go` - Added copyright and enhanced docs
5. `idiomatic/readerioresult/doc.go` - **CREATED NEW FILE**
6. `idiomatic/readerioresult/types.go` - Added comprehensive type docs
7. `idiomatic/readerresult/types.go` - Added comprehensive type docs
## Summary Statistics
- **Packages Reviewed:** 6 main packages
- **Documentation Files Created:** 1 (readerioresult/doc.go)
- **Files Modified:** 7
- **Lines of Documentation Added:** ~150+
- **Test Coverage Status:**
- ✅ Excellent: ioresult
- ✅ Good: readerresult
- ⚠️ Needs Improvement: option, result
- ⚠️ Missing: readerioresult
## Next Steps
1. Create missing unit tests for option package functions
2. Create missing unit tests for result package functions
3. Create complete test suite for readerioresult package
4. Review and document subpackages (exec, file, http, testing, number)
5. Investigate and potentially enable disabled test files in testing subpackages
6. Consider adding integration tests that demonstrate real-world usage patterns
## Conclusion
The idiomatic package has excellent documentation at the package level, with comprehensive explanations of concepts, usage patterns, and performance characteristics. The main areas for improvement are:
1. **Test Coverage:** Several functions lack unit tests, particularly in option and result packages
2. **Subpackage Documentation:** Some subpackages need documentation review
3. **Disabled Tests:** Some test files are disabled and should be investigated
The IOResult package serves as an excellent example of comprehensive testing, including monad law verification and integration tests. This approach should be replicated across other packages.

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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