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

Compare commits

...

69 Commits

Author SHA1 Message Date
Dr. Carsten Leue
739b6a284c fix: better slog based logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 17:52:57 +01:00
Dr. Carsten Leue
ba10d8d314 doc: fix docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 13:00:03 +01:00
Dr. Carsten Leue
3d6c419185 fix: add better logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 12:49:44 +01:00
Dr. Carsten Leue
3f4b6292e4 fix: optimize Traverse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 21:35:05 +01:00
Dr. Carsten Leue
b1704b6d26 fix: implement TraverseReader
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 17:51:13 +01:00
Dr. Carsten Leue
ffdfd218f8 fix: implement Flip for Reader
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 11:04:49 +01:00
Dr. Carsten Leue
34826d8c52 fix: Ask and add tests to retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 16:47:53 +01:00
Dr. Carsten Leue
24c0519cc7 fix: try to unify type signatures
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 16:31:21 +01:00
Dr. Carsten Leue
ff48d8953e fix: implement some missing methods in reader io
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 13:50:25 +01:00
Dr. Carsten Leue
d739c9b277 fix: add doc to readerio
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-03 18:13:59 +01:00
Dr. Carsten Leue
f0054431a5 fix: add logging to readerio 2025-12-03 18:07:06 +01:00
Carsten Leue
1a89ec3df7 fix: implement Sequence for Pair
Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
2025-11-28 11:22:23 +01:00
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
Dr. Carsten Leue
d586428cb0 fix: examples
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:05:57 +01:00
Dr. Carsten Leue
d2dbce6e8b fix: improve lens handling
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 18:23:57 +01:00
Dr. Carsten Leue
6f7ec0768d fix: improve lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 17:28:20 +01:00
Dr. Carsten Leue
ca813b673c fix: better tests and doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 16:24:12 +01:00
Dr. Carsten Leue
af271e7d10 fix: better endo and lens
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 15:03:55 +01:00
Dr. Carsten Leue
567315a31c fix: make a distinction between Chain and Compose for endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 13:51:00 +01:00
Dr. Carsten Leue
311ed55f06 fix: add Read method to Readers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 11:59:20 +01:00
Dr. Carsten Leue
23333ce52c doc: improve doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 11:08:18 +01:00
Dr. Carsten Leue
eb7fc9f77b fix: better tests for Lazy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 10:46:07 +01:00
Dr. Carsten Leue
fd0550e71b fix: better test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 10:35:53 +01:00
Dr. Carsten Leue
13063bbd88 fix: doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 16:36:12 +01:00
Dr. Carsten Leue
4f8a557072 fix: simplify type hints
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 15:24:45 +01:00
Dr. Carsten Leue
a4e790ac3d fix: improve bind
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 13:05:55 +01:00
Dr. Carsten Leue
1af6501cd8 fix: add bind variations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 12:42:14 +01:00
Dr. Carsten Leue
600521b220 fix: refactor
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 11:01:49 +01:00
Dr. Carsten Leue
62fcd186a3 fix: introcuce readeioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-10 18:44:14 +01:00
748 changed files with 106580 additions and 7701 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.

574
v2/DESIGN.md Normal file
View File

@@ -0,0 +1,574 @@
# Design Decisions
This document explains the key design decisions and principles behind fp-go's API design.
## Table of Contents
- [Data Last Principle](#data-last-principle)
- [Kleisli and Operator Types](#kleisli-and-operator-types)
- [Monadic Operations Comparison](#monadic-operations-comparison)
- [Type Parameter Ordering](#type-parameter-ordering)
- [Generic Type Aliases](#generic-type-aliases)
## Data Last Principle
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
### What is "Data Last"?
In the "data last" style, functions are structured so that:
1. Configuration parameters come first
2. The data to be transformed comes last
This is the opposite of the traditional object-oriented style where the data (receiver) comes first.
### Why "Data Last"?
The "data last" principle enables:
1. **Natural Currying**: Functions can be partially applied to create specialized transformations
2. **Function Composition**: Operations can be composed before applying them to data
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
4. **Reusability**: Create reusable transformation pipelines
### Examples
#### Basic Transformation
```go
// Data last style (fp-go)
double := array.Map(number.Mul(2))
result := double([]int{1, 2, 3}) // [2, 4, 6]
// Compare with data first style (traditional)
result := array.Map([]int{1, 2, 3}, number.Mul(2))
```
#### Function Composition
```go
import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
// Create a pipeline of transformations
pipeline := F.Flow3(
A.Filter(func(x int) bool { return x > 0 }), // Keep positive numbers
A.Map(N.Mul(2)), // Double each number
A.Reduce(func(acc, x int) int { return acc + x }, 0), // Sum them up
)
// Apply the pipeline to different data
result1 := pipeline([]int{-1, 2, 3, -4, 5}) // (2 + 3 + 5) * 2 = 20
result2 := pipeline([]int{1, 2, 3}) // (1 + 2 + 3) * 2 = 12
```
#### Partial Application
```go
import (
O "github.com/IBM/fp-go/v2/option"
)
// Create specialized functions by partial application
getOrZero := O.GetOrElse(func() int { return 0 })
getOrEmpty := O.GetOrElse(func() string { return "" })
// Use them with different data
value1 := getOrZero(O.Some(42)) // 42
value2 := getOrZero(O.None[int]()) // 0
text1 := getOrEmpty(O.Some("hello")) // "hello"
text2 := getOrEmpty(O.None[string]()) // ""
```
#### Building Reusable Transformations
```go
import (
E "github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
)
// Create a reusable validation pipeline
type User struct {
Name string
Email string
Age int
}
validateAge := E.FromPredicate(
func(u User) bool { return u.Age >= 18 },
func(u User) error { return errors.New("must be 18 or older") },
)
validateEmail := E.FromPredicate(
func(u User) bool { return strings.Contains(u.Email, "@") },
func(u User) error { return errors.New("invalid email") },
)
// Compose validators
validateUser := F.Flow2(
validateAge,
E.Chain(validateEmail),
)
// Apply to different users
result1 := validateUser(User{Name: "Alice", Email: "alice@example.com", Age: 25})
result2 := validateUser(User{Name: "Bob", Email: "invalid", Age: 30})
```
#### Monadic Operations
```go
import (
O "github.com/IBM/fp-go/v2/option"
)
// Data last enables clean monadic chains
parseAndDouble := F.Flow2(
O.FromPredicate(func(s string) bool { return s != "" }),
O.Chain(func(s string) O.Option[int] {
n, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(n * 2)
}),
)
result1 := parseAndDouble("21") // Some(42)
result2 := parseAndDouble("") // None
result3 := parseAndDouble("abc") // None
```
### Monadic vs Non-Monadic Forms
fp-go provides two forms for most operations:
1. **Curried form** (data last): Returns a function that can be composed
2. **Monadic form** (data first): Takes all parameters at once
```go
// Curried form - data last, returns a function
Map[A, B any](f func(A) B) func(Option[A]) Option[B]
// Monadic form - data first, direct execution
MonadMap[A, B any](fa Option[A], f func(A) B) Option[B]
```
**When to use each:**
- **Curried form**: When building pipelines, composing functions, or creating reusable transformations
- **Monadic form**: When you have all parameters available and want direct execution
```go
// Curried form - building a pipeline
transform := F.Flow3(
O.Map(strings.ToUpper),
O.Filter(func(s string) bool { return len(s) > 3 }),
O.GetOrElse(func() string { return "DEFAULT" }),
)
result := transform(O.Some("hello"))
// Monadic form - direct execution
result := O.MonadMap(O.Some("hello"), strings.ToUpper)
```
### Further Reading on Data-Last Pattern
The data-last currying pattern is well-documented in the functional programming community:
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
## Kleisli and Operator Types
fp-go uses consistent type aliases across all monads to make code more recognizable and composable. These types provide a common vocabulary that works across different monadic contexts.
### Type Definitions
```go
// Kleisli arrow - a function that returns a monadic value
type Kleisli[A, B any] = func(A) M[B]
// Operator - a function that transforms a monadic value
type Operator[A, B any] = func(M[A]) M[B]
```
Where `M` represents the specific monad (Option, Either, IO, etc.).
### Why These Types Matter
1. **Consistency**: The same type names appear across all monads
2. **Recognizability**: Experienced functional programmers immediately understand the intent
3. **Composability**: Functions with these types compose naturally
4. **Documentation**: Type signatures clearly communicate the operation's behavior
### Examples Across Monads
#### Option Monad
```go
// option/option.go
type Kleisli[A, B any] = func(A) Option[B]
type Operator[A, B any] = func(Option[A]) Option[B]
// Chain uses Kleisli
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
// Map returns an Operator
func Map[A, B any](f func(A) B) Operator[A, B]
```
#### Either Monad
```go
// either/either.go
type Kleisli[E, A, B any] = func(A) Either[E, B]
type Operator[E, A, B any] = func(Either[E, A]) Either[E, B]
// Chain uses Kleisli
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B]
// Map returns an Operator
func Map[E, A, B any](f func(A) B) Operator[E, A, B]
```
#### IO Monad
```go
// io/io.go
type Kleisli[A, B any] = func(A) IO[B]
type Operator[A, B any] = func(IO[A]) IO[B]
// Chain uses Kleisli
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
// Map returns an Operator
func Map[A, B any](f func(A) B) Operator[A, B]
```
#### Array (List Monad)
```go
// array/array.go
type Kleisli[A, B any] = func(A) []B
type Operator[A, B any] = func([]A) []B
// Chain uses Kleisli
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B]
// Map returns an Operator
func Map[A, B any](f func(A) B) Operator[A, B]
```
### Pattern Recognition
Once you learn these patterns in one monad, you can apply them to all monads:
```go
// The pattern is always the same, just the monad changes
// Option
validateAge := option.Chain(func(user User) option.Option[User] {
if user.Age >= 18 {
return option.Some(user)
}
return option.None[User]()
})
// Either
validateAge := either.Chain(func(user User) either.Either[error, User] {
if user.Age >= 18 {
return either.Right[error](user)
}
return either.Left[User](errors.New("too young"))
})
// IO
validateAge := io.Chain(func(user User) io.IO[User] {
return io.Of(user) // Always succeeds in IO
})
// Array
validateAge := array.Chain(func(user User) []User {
if user.Age >= 18 {
return []User{user}
}
return []User{} // Empty array = failure
})
```
### Composing Kleisli Arrows
Kleisli arrows compose naturally using monadic composition:
```go
import (
O "github.com/IBM/fp-go/v2/option"
F "github.com/IBM/fp-go/v2/function"
)
// Define Kleisli arrows
parseAge := func(s string) O.Option[int] {
n, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(n)
}
validateAge := func(age int) O.Option[int] {
if age >= 18 {
return O.Some(age)
}
return O.None[int]()
}
formatAge := func(age int) O.Option[string] {
return O.Some(fmt.Sprintf("Age: %d", age))
}
// Compose them using Flow and Chain
pipeline := F.Flow3(
parseAge,
O.Chain(validateAge),
O.Chain(formatAge),
)
result := pipeline("25") // Some("Age: 25")
result := pipeline("15") // None (too young)
result := pipeline("abc") // None (parse error)
```
### Building Reusable Operators
Operators can be created once and reused across your codebase:
```go
import (
E "github.com/IBM/fp-go/v2/either"
)
// Create reusable operators
type ValidationError struct {
Field string
Message string
}
// Reusable validation operators
validateNonEmpty := E.Chain(func(s string) E.Either[ValidationError, string] {
if s == "" {
return E.Left[string](ValidationError{
Field: "input",
Message: "cannot be empty",
})
}
return E.Right[ValidationError](s)
})
validateEmail := E.Chain(func(s string) E.Either[ValidationError, string] {
if !strings.Contains(s, "@") {
return E.Left[string](ValidationError{
Field: "email",
Message: "invalid format",
})
}
return E.Right[ValidationError](s)
})
// Compose operators
validateEmailInput := F.Flow2(
validateNonEmpty,
validateEmail,
)
// Use across your application
result1 := validateEmailInput(E.Right[ValidationError]("user@example.com"))
result2 := validateEmailInput(E.Right[ValidationError](""))
result3 := validateEmailInput(E.Right[ValidationError]("invalid"))
```
### Benefits of Consistent Naming
1. **Cross-monad understanding**: Learn once, apply everywhere
2. **Easier refactoring**: Changing monads requires minimal code changes
3. **Better tooling**: IDEs can provide better suggestions
4. **Team communication**: Shared vocabulary across the team
5. **Library integration**: Third-party libraries follow the same patterns
### Identity Monad - The Simplest Case
The Identity monad shows these types in their simplest form:
```go
// identity/doc.go
type Operator[A, B any] = func(A) B
// In Identity, there's no wrapping, so:
// - Kleisli[A, B] is just func(A) B
// - Operator[A, B] is just func(A) B
// They're the same because Identity adds no context
```
This demonstrates that these type aliases represent fundamental functional programming concepts, not just arbitrary naming conventions.
## Monadic Operations Comparison
fp-go's monadic operations are inspired by functional programming languages and libraries. Here's how they compare:
| fp-go | fp-ts | Haskell | Scala | Description |
|-------|-------|---------|-------|-------------|
| `Map` | `map` | `fmap` | `map` | Functor mapping - transforms the value inside a context |
| `Chain` | `chain` | `>>=` (bind) | `flatMap` | Monadic bind - chains computations that return wrapped values |
| `Ap` | `ap` | `<*>` | `ap` | Applicative apply - applies a wrapped function to a wrapped value |
| `Of` | `of` | `return`/`pure` | `pure` | Lifts a pure value into a monadic context |
| `Fold` | `fold` | `either` | `fold` | Eliminates the context by providing handlers for each case |
| `Filter` | `filter` | `mfilter` | `filter` | Keeps values that satisfy a predicate |
| `Flatten` | `flatten` | `join` | `flatten` | Removes one level of nesting |
| `ChainFirst` | `chainFirst` | `>>` (then) | `tap` | Chains for side effects, keeping the original value |
| `Alt` | `alt` | `<\|>` | `orElse` | Provides an alternative value if the first fails |
| `GetOrElse` | `getOrElse` | `fromMaybe` | `getOrElse` | Extracts the value or provides a default |
| `FromPredicate` | `fromPredicate` | `guard` | `filter` | Creates a monadic value based on a predicate |
| `Sequence` | `sequence` | `sequence` | `sequence` | Transforms a collection of effects into an effect of a collection |
| `Traverse` | `traverse` | `traverse` | `traverse` | Maps and sequences in one operation |
| `Reduce` | `reduce` | `foldl` | `foldLeft` | Folds a structure from left to right |
| `ReduceRight` | `reduceRight` | `foldr` | `foldRight` | Folds a structure from right to left |
### Key Differences from Other Languages
#### Naming Conventions
- **Go conventions**: fp-go uses PascalCase for exported functions (e.g., `Map`, `Chain`) following Go's naming conventions
- **Type parameters first**: Non-inferrable type parameters come first (e.g., `Ap[B, E, A any]`)
- **Monadic prefix**: Direct execution forms use the `Monad` prefix (e.g., `MonadMap`, `MonadChain`)
#### Type System
```go
// fp-go (explicit type parameters when needed)
result := option.Map(transform)(value)
result := option.Map[string, int](transform)(value) // explicit when inference fails
// Haskell (type inference)
result = fmap transform value
// Scala (type inference with method syntax)
result = value.map(transform)
// fp-ts (TypeScript type inference)
const result = pipe(value, map(transform))
```
#### Currying
```go
// fp-go - explicit currying with data last
double := array.Map(number.Mul(2))
result := double(numbers)
// Haskell - automatic currying
double = fmap (*2)
result = double numbers
// Scala - method syntax
result = numbers.map(_ * 2)
```
## Type Parameter Ordering
fp-go v2 uses a specific ordering for type parameters to maximize type inference:
### Rule: Non-Inferrable Parameters First
Type parameters that **cannot be inferred** from function arguments come first. This allows the Go compiler to infer as many types as possible.
```go
// Ap - B cannot be inferred from arguments, so it comes first
func Ap[B, E, A any](fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B]
// Usage - only B needs to be specified
result := either.Ap[string](value)(funcInEither)
```
### Examples
```go
// Map - all types can be inferred from arguments
func Map[E, A, B any](f func(A) B) func(Either[E, A]) Either[E, B]
// Usage - no type parameters needed
result := either.Map(transform)(value)
// Chain - all types can be inferred
func Chain[E, A, B any](f func(A) Either[E, B]) func(Either[E, A]) Either[E, B]
// Usage - no type parameters needed
result := either.Chain(validator)(value)
// Of - E cannot be inferred, comes first
func Of[E, A any](value A) Either[E, A]
// Usage - only E needs to be specified
result := either.Of[error](42)
```
### Benefits
1. **Less verbose code**: Most operations don't require explicit type parameters
2. **Better IDE support**: Type inference provides better autocomplete
3. **Clearer intent**: Only specify types that can't be inferred
## Generic Type Aliases
fp-go v2 leverages Go 1.24's generic type aliases for cleaner type definitions:
```go
// V2 - using generic type alias (requires Go 1.24+)
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
// V1 - using type definition (Go 1.18+)
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
```
### Benefits
1. **True aliases**: The type is interchangeable with its definition
2. **No namespace imports needed**: Can use types directly without package prefixes
3. **Simpler codebase**: Eliminates the need for `generic` subpackages
4. **Better composability**: Types compose more naturally
### Migration Pattern
```go
// Define project-wide aliases once
package types
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/ioresult"
)
type Option[A any] = option.Option[A]
type Result[A any] = result.Result[A]
type IOResult[A any] = ioresult.IOResult[A]
// Use throughout your codebase
package myapp
import "myproject/types"
func process(input string) types.Result[types.Option[int]] {
// implementation
}
```
---
For more information, see:
- [README.md](./README.md) - Overview and quick start
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
- [Samples](./samples/) - Practical examples

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

@@ -2,25 +2,152 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go/v2.svg)](https://pkg.go.dev/github.com/IBM/fp-go/v2)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main&flag=v2)](https://coveralls.io/github/IBM/fp-go?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/IBM/fp-go/v2)](https://goreportcard.com/report/github.com/IBM/fp-go/v2)
Version 2 of fp-go leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
**fp-go** is a comprehensive functional programming library for Go, bringing type-safe functional patterns inspired by [fp-ts](https://gcanti.github.io/fp-ts/) to the Go ecosystem. Version 2 leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
## 📚 Table of Contents
- [Overview](#-overview)
- [Features](#-features)
- [Requirements](#-requirements)
- [Breaking Changes](#-breaking-changes)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Breaking Changes](#️-breaking-changes)
- [Key Improvements](#-key-improvements)
- [Migration Guide](#-migration-guide)
- [Installation](#-installation)
- [What's New](#-whats-new)
- [Documentation](#-documentation)
- [Contributing](#-contributing)
- [License](#-license)
## 🎯 Overview
fp-go brings the power of functional programming to Go with:
- **Type-safe abstractions** - Monads, Functors, Applicatives, and more
- **Composable operations** - Build complex logic from simple, reusable functions
- **Error handling** - Elegant error management with `Either`, `Result`, and `IOEither`
- **Lazy evaluation** - Control when and how computations execute
- **Optics** - Powerful lens, prism, and traversal operations for immutable data manipulation
## ✨ Features
- 🔒 **Type Safety** - Leverage Go's generics for compile-time guarantees
- 🧩 **Composability** - Chain operations naturally with functional composition
- 📦 **Rich Type System** - `Option`, `Either`, `Result`, `IO`, `Reader`, and more
- 🎯 **Practical** - Designed for real-world Go applications
- 🚀 **Performance** - Zero-cost abstractions where possible
- 📖 **Well-documented** - Comprehensive API documentation and examples
- 🧪 **Battle-tested** - Extensive test coverage
## 🔧 Requirements
- **Go 1.24 or later** (for generic type alias support)
## ⚠️ Breaking Changes
## 📦 Installation
### 1. Generic Type Aliases
```bash
go get github.com/IBM/fp-go/v2
```
## 🚀 Quick Start
### Working with Option
```go
package main
import (
"fmt"
"github.com/IBM/fp-go/v2/option"
)
func main() {
// Create an Option
some := option.Some(42)
none := option.None[int]()
// Map over values
doubled := option.Map(N.Mul(2))(some)
fmt.Println(option.GetOrElse(0)(doubled)) // Output: 84
// Chain operations
result := option.Chain(func(x int) option.Option[string] {
if x > 0 {
return option.Some(fmt.Sprintf("Positive: %d", x))
}
return option.None[string]()
})(some)
fmt.Println(option.GetOrElse("No value")(result)) // Output: Positive: 42
}
```
### Error Handling with Result
```go
package main
import (
"errors"
"fmt"
"github.com/IBM/fp-go/v2/result"
)
func divide(a, b int) result.Result[int] {
if b == 0 {
return result.Error[int](errors.New("division by zero"))
}
return result.Ok(a / b)
}
func main() {
res := divide(10, 2)
// Pattern match on the result
result.Fold(
func(err error) { fmt.Println("Error:", err) },
func(val int) { fmt.Println("Result:", val) },
)(res)
// Output: Result: 5
// Or use GetOrElse for a default value
value := result.GetOrElse(0)(divide(10, 0))
fmt.Println("Value:", value) // Output: Value: 0
}
```
### Composing IO Operations
```go
package main
import (
"fmt"
"github.com/IBM/fp-go/v2/io"
)
func main() {
// Define pure IO operations
readInput := io.MakeIO(func() string {
return "Hello, fp-go!"
})
// Transform the result
uppercase := io.Map(func(s string) string {
return fmt.Sprintf(">>> %s <<<", s)
})(readInput)
// Execute the IO operation
result := uppercase()
fmt.Println(result) // Output: >>> Hello, fp-go! <<<
}
```
### From V1 to V2
#### 1. Generic Type Aliases
V2 uses [generic type aliases](https://github.com/golang/go/issues/46477) which require Go 1.24+. This is the most significant change and enables cleaner type definitions.
@@ -34,7 +161,7 @@ type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
```
### 2. Generic Type Parameter Ordering
#### 2. Generic Type Parameter Ordering
Type parameters that **cannot** be inferred from function arguments now come first, improving type inference.
@@ -52,7 +179,7 @@ func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, fu
This change allows the Go compiler to infer more types automatically, reducing the need for explicit type parameters.
### 3. Pair Monad Semantics
#### 3. Pair Monad Semantics
Monadic operations for `Pair` now operate on the **second argument** to align with the [Haskell definition](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html).
@@ -60,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:**
@@ -70,6 +197,36 @@ pair := MakePair(1, "hello")
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
```
#### 4. Endomorphism Compose Semantics
The `Compose` function for endomorphisms now follows **mathematical function composition** (right-to-left execution), aligning with standard functional programming conventions.
**V1:**
```go
// Compose executed left-to-right
double := N.Mul(2)
increment := N.Add(1)
composed := Compose(double, increment)
result := composed(5) // (5 * 2) + 1 = 11
```
**V2:**
```go
// Compose executes RIGHT-TO-LEFT (mathematical composition)
double := N.Mul(2)
increment := N.Add(1)
composed := Compose(double, increment)
result := composed(5) // (5 + 1) * 2 = 12
// Use MonadChain for LEFT-TO-RIGHT execution
chained := MonadChain(double, increment)
result2 := chained(5) // (5 * 2) + 1 = 11
```
**Key Difference:**
- `Compose(f, g)` now means `f ∘ g`, which applies `g` first, then `f` (right-to-left)
- `MonadChain(f, g)` applies `f` first, then `g` (left-to-right)
## ✨ Key Improvements
### 1. Simplified Type Declarations
@@ -91,16 +248,16 @@ func processData(input string) ET.Either[error, OPT.Option[int]] {
**V2 Approach:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/option"
)
// Define type aliases once
type Either[A any] = either.Either[error, A]
type Result[A any] = result.Result[A]
type Option[A any] = option.Option[A]
// Use them throughout your codebase
func processData(input string) Either[Option[int]] {
func processData(input string) Result[Option[int]] {
// implementation
}
```
@@ -211,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):**
@@ -230,20 +387,14 @@ Create project-wide type aliases for common patterns:
package myapp
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
)
type Either[A any] = either.Either[error, A]
type Result[A any] = result.Result[A]
type Option[A any] = option.Option[A]
type IOEither[A any] = ioeither.IOEither[error, A]
```
## 📦 Installation
```bash
go get github.com/IBM/fp-go/v2
type IOResult[A any] = ioresult.IOResult[A]
```
## 🆕 What's New
@@ -269,7 +420,7 @@ import (
func process() IOET.IOEither[error, string] {
return IOEG.Map[error, int, string](
func(x int) string { return fmt.Sprintf("%d", x) },
strconv.Itoa,
)(fetchData())
}
```
@@ -277,25 +428,37 @@ func process() IOET.IOEither[error, string] {
**V2 Simplified Example:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/ioeither"
"strconv"
"github.com/IBM/fp-go/v2/ioresult"
)
type IOEither[A any] = ioeither.IOEither[error, A]
type IOResult[A any] = ioresult.IOResult[A]
func process() IOEither[string] {
return ioeither.Map(
func(x int) string { return fmt.Sprintf("%d", x) },
func process() IOResult[string] {
return ioresult.Map(
strconv.Itoa,
)(fetchData())
}
```
## 📚 Additional Resources
## 📚 Documentation
- [Main README](../README.md) - Core concepts and design philosophy
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)
- [Code Samples](../samples/)
- [Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
- **[Code Samples](./samples/)** - Practical examples and use cases
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
### Core Modules
- **Option** - Represent optional values without nil
- **Either** - Type-safe error handling with left/right values
- **Result** - Simplified Either with error as left type
- **IO** - Lazy evaluation and side effect management
- **IOEither** - Combine IO with error handling
- **Reader** - Dependency injection pattern
- **ReaderIOEither** - Combine Reader, IO, and Either for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
## 🤔 Should I Migrate?
@@ -310,10 +473,25 @@ func process() IOEither[string] {
- ⚠️ Migration effort outweighs benefits for your project
- ⚠️ You need stability in production (V2 is newer)
## 🤝 Contributing
Contributions are welcome! Here's how you can help:
1. **Report bugs** - Open an issue with a clear description and reproduction steps
2. **Suggest features** - Share your ideas for improvements
3. **Submit PRs** - Fix bugs or add features (please discuss major changes first)
4. **Improve docs** - Help make the documentation clearer and more comprehensive
Please read our contribution guidelines before submitting pull requests.
## 🐛 Issues and Feedback
Found a bug or have a suggestion? Please [open an issue](https://github.com/IBM/fp-go/issues) on GitHub.
## 📄 License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
This project is licensed under the Apache License 2.0. See the [LICENSE](https://github.com/IBM/fp-go/blob/main/LICENSE) file for details.
---
**Made with ❤️ by IBM**

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 {
return G.Map[[]A, []B, A, B](f)
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,8 +277,8 @@ 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 {
return G.MonadChain[[]A, []B](fa, f)
func MonadChain[A, B any](fa []A, f Kleisli[A, B]) []B {
return G.MonadChain(fa, f)
}
// Chain applies a function that returns an array to each element and flattens the results.
@@ -290,8 +290,8 @@ 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 {
return G.Chain[[]A, []B](f)
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return G.Chain[[]A](f)
}
// MonadAp applies an array of functions to an array of values, producing all combinations.
@@ -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)
}
@@ -314,21 +314,21 @@ func Ap[B, A any](fa []A) func([]func(A) B) []B {
//
//go:inline
func Match[A, B any](onEmpty func() B, onNonEmpty func([]A) B) func([]A) B {
return G.Match[[]A](onEmpty, onNonEmpty)
return G.Match(onEmpty, onNonEmpty)
}
// MatchLeft performs pattern matching on an array, calling onEmpty if empty or onNonEmpty with head and tail if not.
//
//go:inline
func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A) B {
return G.MatchLeft[[]A](onEmpty, onNonEmpty)
return G.MatchLeft(onEmpty, onNonEmpty)
}
// Tail returns all elements except the first, wrapped in an Option.
// 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) {
@@ -390,7 +390,7 @@ func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
// Intercalate inserts a separator between elements and concatenates them using a Monoid.
func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A {
return func(middle A) func([]A) A {
return Match(m.Empty, F.Flow2(Intersperse(middle), ConcatAll[A](m)))
return Match(m.Empty, F.Flow2(Intersperse(middle), ConcatAll(m)))
}
}
@@ -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.
@@ -519,20 +519,20 @@ func Push[A any](a A) EM.Endomorphism[[]A] {
//
//go:inline
func MonadFlap[B, A any](fab []func(A) B, a A) []B {
return G.MonadFlap[func(A) B, []func(A) B, []B, A, B](fab, a)
return G.MonadFlap[func(A) B, []func(A) B, []B](fab, a)
}
// Flap applies a value to an array of functions, producing an array of results.
// This is the curried version.
//
//go:inline
func Flap[B, A any](a A) func([]func(A) B) []B {
return G.Flap[func(A) B, []func(A) B, []B, A, B](a)
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

@@ -97,7 +97,7 @@ func TestAp(t *testing.T) {
utils.Double,
utils.Triple,
},
Ap[int, int]([]int{1, 2, 3}),
Ap[int]([]int{1, 2, 3}),
),
)
}

View File

@@ -34,7 +34,7 @@ import (
func Do[S any](
empty S,
) []S {
return G.Do[[]S, S](empty)
return G.Do[[]S](empty)
}
// Bind attaches the result of a computation to a context S1 to produce a context S2.
@@ -56,9 +56,9 @@ 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 {
return G.Bind[[]S1, []S2, []T, S1, S2, T](setter, f)
f Kleisli[S1, T],
) Operator[S1, S2] {
return G.Bind[[]S1, []S2](setter, f)
}
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
@@ -79,8 +79,8 @@ 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 {
return G.Let[[]S1, []S2, S1, S2, T](setter, f)
) Operator[S1, S2] {
return G.Let[[]S1, []S2](setter, f)
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
@@ -101,8 +101,8 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func([]S1) []S2 {
return G.LetTo[[]S1, []S2, S1, S2, T](setter, b)
) Operator[S1, S2] {
return G.LetTo[[]S1, []S2](setter, b)
}
// BindTo initializes a new state S1 from a value T.
@@ -120,8 +120,8 @@ func LetTo[S1, S2, T any](
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) func([]T) []S1 {
return G.BindTo[[]S1, []T, S1, T](setter)
) Operator[T, S1] {
return G.BindTo[[]S1, []T](setter)
}
// ApS attaches a value to a context S1 to produce a context S2 by considering
@@ -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 {
return G.ApS[[]S1, []S2, []T, S1, S2, T](setter, fa)
) 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

@@ -87,6 +87,6 @@ func Example_sort() {
// [abc klm zyx]
// [zyx klm abc]
// [None[int] Some[int](42) Some[int](1337)]
// [{c {false 0}} {b {true 10}} {d {true 10}} {a {true 30}}]
// [{c {0 false}} {b {10 true}} {d {10 true}} {a {30 true}}]
}

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,31 +25,33 @@ 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 {
return func(as GA) B {
return MonadReduce[GA](as, f, initial)
return MonadReduce(as, f, initial)
}
}
func ReduceWithIndex[GA ~[]A, A, B any](f func(int, B, A) B, initial B) func(GA) B {
return func(as GA) B {
return MonadReduceWithIndex[GA](as, f, initial)
return MonadReduceWithIndex(as, f, initial)
}
}
func ReduceRight[GA ~[]A, A, B any](f func(A, B) B, initial B) func(GA) B {
return func(as GA) B {
return MonadReduceRight[GA](as, f, initial)
return MonadReduceRight(as, f, initial)
}
}
func ReduceRightWithIndex[GA ~[]A, A, B any](f func(int, A, B) B, initial B) func(GA) B {
return func(as GA) B {
return MonadReduceRightWithIndex[GA](as, f, initial)
return MonadReduceRightWithIndex(as, f, initial)
}
}
@@ -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

@@ -22,19 +22,19 @@ import (
type arrayMonad[A, B any, GA ~[]A, GB ~[]B, GAB ~[]func(A) B] struct{}
func (o *arrayMonad[A, B, GA, GB, GAB]) Of(a A) GA {
return Of[GA, A](a)
return Of[GA](a)
}
func (o *arrayMonad[A, B, GA, GB, GAB]) Map(f func(A) B) func(GA) GB {
return Map[GA, GB, A, B](f)
return Map[GA, GB](f)
}
func (o *arrayMonad[A, B, GA, GB, GAB]) Chain(f func(A) GB) func(GA) GB {
return Chain[GA, GB, A, B](f)
return Chain[GA](f)
}
func (o *arrayMonad[A, B, GA, GB, GAB]) Ap(fa GA) func(GAB) GB {
return Ap[GB, GAB, GA, B, A](fa)
return Ap[GB, GAB](fa)
}
// Monad implements the monadic operations for an array

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

@@ -97,11 +97,11 @@ func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
}
func MonadChain[A, B any](fa NonEmptyArray[A], f func(a A) NonEmptyArray[B]) NonEmptyArray[B] {
return G.MonadChain[NonEmptyArray[A], NonEmptyArray[B]](fa, f)
return G.MonadChain(fa, f)
}
func Chain[A, B any](f func(A) NonEmptyArray[B]) func(NonEmptyArray[A]) NonEmptyArray[B] {
return G.Chain[NonEmptyArray[A], NonEmptyArray[B]](f)
return G.Chain[NonEmptyArray[A]](f)
}
func MonadAp[B, A any](fab NonEmptyArray[func(A) B], fa NonEmptyArray[A]) NonEmptyArray[B] {

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 {
return G.SortBy[[]T, []O.Ord[T]](ord)
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

@@ -18,7 +18,7 @@ import (
//
//go:inline
func StrictUniq[A comparable](as []A) []A {
return G.StrictUniq[[]A](as)
return G.StrictUniq(as)
}
// Uniq converts an array of arbitrary items into an array of unique items
@@ -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)
}

View File

@@ -36,7 +36,7 @@ import (
//
//go:inline
func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
return G.ZipWith[[]A, []B, []C, FCT](fa, fb, f)
return G.ZipWith[[]A, []B, []C](fa, fb, f)
}
// Zip takes two arrays and returns an array of corresponding pairs (tuples).
@@ -79,5 +79,5 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
//
//go:inline
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
return G.Unzip[[]A, []B, []T.Tuple2[A, B]](cs)
return G.Unzip[[]A, []B](cs)
}

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

@@ -0,0 +1,710 @@
// 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.
//
// # Data Last Principle
//
// This package follows the "data last" functional programming principle, where
// the data being operated on comes as the last parameter in a chain of function
// applications. This design enables several powerful functional programming patterns:
//
// 1. **Partial Application**: You can create reusable assertion functions by providing
// configuration parameters first, leaving the data and testing context for later.
//
// 2. **Function Composition**: Assertions can be composed and combined before being
// applied to actual data.
//
// 3. **Point-Free Style**: You can pass assertion functions around without immediately
// providing the data they operate on.
//
// The general pattern is:
//
// assert.Function(config)(data)(testingContext)
// ↑ ↑ ↑
// expected actual *testing.T (always last)
//
// For single-parameter assertions:
//
// assert.Function(data)(testingContext)
// ↑ ↑
// actual *testing.T (always last)
//
// Examples of "data last" in action:
//
// // Multi-parameter: expected value → actual value → testing context
// assert.Equal(42)(result)(t)
// assert.ArrayContains(3)(numbers)(t)
//
// // Single-parameter: data → testing context
// assert.NoError(err)(t)
// assert.ArrayNotEmpty(arr)(t)
//
// // Partial application - create reusable assertions
// isPositive := assert.That(func(n int) bool { return n > 0 })
// // Later, apply to different values:
// isPositive(42)(t) // Passes
// isPositive(-5)(t) // Fails
//
// // Composition - combine assertions before applying data
// validateUser := func(u User) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.Equal("Alice")(u.Name),
// assert.That(func(age int) bool { return age >= 18 })(u.Age),
// })
// }
// validateUser(user)(t)
//
// 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.
//
// This function follows the "data last" principle - you provide the expected value first,
// then the actual value, and finally the testing.T context.
//
// Example:
//
// func TestNotEqual(t *testing.T) {
// value := 42
// assert.NotEqual(10)(value)(t) // Passes: 42 != 10
// assert.NotEqual(42)(value)(t) // Fails: 42 == 42
// }
func NotEqual[T any](expected T) Kleisli[T] {
return wrap1(assert.NotEqual, expected)
}
// Equal tests if the expected and the actual values are equal.
//
// This is one of the most commonly used assertions. It follows the "data last" principle -
// you provide the expected value first, then the actual value, and finally the testing.T context.
//
// Example:
//
// func TestEqual(t *testing.T) {
// result := 2 + 2
// assert.Equal(4)(result)(t) // Passes
//
// name := "Alice"
// assert.Equal("Alice")(name)(t) // Passes
//
// // Can be composed with other assertions
// user := User{Name: "Bob", Age: 30}
// assertions := assert.AllOf([]assert.Reader{
// assert.Equal("Bob")(user.Name),
// assert.Equal(30)(user.Age),
// })
// assertions(t)
// }
func Equal[T any](expected T) Kleisli[T] {
return wrap1(assert.Equal, expected)
}
// ArrayNotEmpty checks if an array is not empty.
//
// Example:
//
// func TestArrayNotEmpty(t *testing.T) {
// numbers := []int{1, 2, 3}
// assert.ArrayNotEmpty(numbers)(t) // Passes
//
// empty := []int{}
// assert.ArrayNotEmpty(empty)(t) // Fails
// }
func ArrayNotEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, arr)
}
}
// RecordNotEmpty checks if a map is not empty.
//
// Example:
//
// func TestRecordNotEmpty(t *testing.T) {
// config := map[string]int{"timeout": 30, "retries": 3}
// assert.RecordNotEmpty(config)(t) // Passes
//
// empty := map[string]int{}
// assert.RecordNotEmpty(empty)(t) // Fails
// }
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.
//
// Example:
//
// func TestStringNotEmpty(t *testing.T) {
// message := "Hello, World!"
// assert.StringNotEmpty(message)(t) // Passes
//
// empty := ""
// assert.StringNotEmpty(empty)(t) // Fails
// }
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.
//
// Example:
//
// func TestArrayLength(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// assert.ArrayLength[int](5)(numbers)(t) // Passes
// assert.ArrayLength[int](3)(numbers)(t) // Fails
// }
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.
//
// Example:
//
// func TestRecordLength(t *testing.T) {
// config := map[string]string{"host": "localhost", "port": "8080"}
// assert.RecordLength[string, string](2)(config)(t) // Passes
// assert.RecordLength[string, string](3)(config)(t) // Fails
// }
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.
//
// Example:
//
// func TestStringLength(t *testing.T) {
// message := "Hello"
// assert.StringLength[any, any](5)(message)(t) // Passes
// assert.StringLength[any, any](10)(message)(t) // Fails
// }
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.
//
// This is commonly used to assert that operations complete successfully.
//
// Example:
//
// func TestNoError(t *testing.T) {
// err := doSomething()
// assert.NoError(err)(t) // Passes if err is nil
//
// // Can be used with result types
// result := result.TryCatch(func() (int, error) {
// return 42, nil
// })
// assert.Success(result)(t) // Uses NoError internally
// }
func NoError(err error) Reader {
return func(t *testing.T) bool {
return assert.NoError(t, err)
}
}
// Error validates that there is an error.
//
// This is used to assert that operations fail as expected.
//
// Example:
//
// func TestError(t *testing.T) {
// err := validateInput("")
// assert.Error(err)(t) // Passes if err is not nil
//
// err2 := validateInput("valid")
// assert.Error(err2)(t) // Fails if err2 is nil
// }
func Error(err error) Reader {
return func(t *testing.T) bool {
return assert.Error(t, err)
}
}
// Success checks if a [Result] represents success.
//
// This is a convenience function for testing Result types from the fp-go library.
//
// Example:
//
// func TestSuccess(t *testing.T) {
// res := result.Of[int](42)
// assert.Success(res)(t) // Passes
//
// failedRes := result.Error[int](errors.New("failed"))
// assert.Success(failedRes)(t) // Fails
// }
func Success[T any](res Result[T]) Reader {
return NoError(result.ToError(res))
}
// Failure checks if a [Result] represents failure.
//
// This is a convenience function for testing Result types from the fp-go library.
//
// Example:
//
// func TestFailure(t *testing.T) {
// res := result.Error[int](errors.New("something went wrong"))
// assert.Failure(res)(t) // Passes
//
// successRes := result.Of[int](42)
// assert.Failure(successRes)(t) // Fails
// }
func Failure[T any](res Result[T]) Reader {
return Error(result.ToError(res))
}
// ArrayContains tests if a value is contained in an array.
//
// Example:
//
// func TestArrayContains(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// assert.ArrayContains(3)(numbers)(t) // Passes
// assert.ArrayContains(10)(numbers)(t) // Fails
//
// names := []string{"Alice", "Bob", "Charlie"}
// assert.ArrayContains("Bob")(names)(t) // Passes
// }
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.
//
// Example:
//
// func TestContainsKey(t *testing.T) {
// config := map[string]int{"timeout": 30, "retries": 3}
// assert.ContainsKey[int]("timeout")(config)(t) // Passes
// assert.ContainsKey[int]("maxSize")(config)(t) // Fails
// }
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.
//
// Example:
//
// func TestNotContainsKey(t *testing.T) {
// config := map[string]int{"timeout": 30, "retries": 3}
// assert.NotContainsKey[int]("maxSize")(config)(t) // Passes
// assert.NotContainsKey[int]("timeout")(config)(t) // Fails
// }
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.
//
// This is a powerful function that allows you to create custom assertions using predicates.
//
// Example:
//
// func TestThat(t *testing.T) {
// // Test if a number is positive
// isPositive := func(n int) bool { return n > 0 }
// assert.That(isPositive)(42)(t) // Passes
// assert.That(isPositive)(-5)(t) // Fails
//
// // Test if a string is uppercase
// isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
// assert.That(isUppercase)("HELLO")(t) // Passes
// assert.That(isUppercase)("Hello")(t) // Fails
//
// // Can be combined with Local for property testing
// type User struct { Age int }
// ageIsAdult := assert.Local(func(u User) int { return u.Age })(
// assert.That(func(age int) bool { return age >= 18 }),
// )
// user := User{Age: 25}
// ageIsAdult(user)(t) // Passes
// }
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"
E "github.com/IBM/fp-go/v2/either"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
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) func(actual T) E.Either[error, T] {
return func(actual T) E.Either[error, T] {
ok := wrapped(t, expected, actual)
if ok {
return E.Of[error](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 E.Left[T](errTest)
}
}
})
// NotEqual tests if the expected and the actual values are not equal
func NotEqual[T any](t *testing.T, expected T) func(actual T) E.Either[error, 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) func(actual T) E.Either[error, 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) func(actual []T) E.Either[error, []T] {
return func(actual []T) E.Either[error, []T] {
ok := assert.Len(t, actual, expected)
if ok {
return E.Of[error](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 E.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) func(actual E.Either[error, T]) E.Either[error, T] {
return func(actual E.Either[error, T]) E.Either[error, T] {
return E.MonadFold(actual, func(e error) E.Either[error, T] {
assert.NoError(t, e)
return E.Left[T](e)
}, func(value T) E.Either[error, T] {
assert.NoError(t, nil)
return E.Right[error](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) func(actual []T) E.Either[error, []T] {
return func(actual []T) E.Either[error, []T] {
ok := assert.Contains(t, actual, expected)
if ok {
return E.Of[error](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 E.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) func(actual map[K]T) E.Either[error, map[K]T] {
return func(actual map[K]T) E.Either[error, map[K]T] {
ok := assert.Contains(t, actual, expected)
if ok {
return E.Of[error](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 E.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) func(actual map[K]T) E.Either[error, map[K]T] {
return func(actual map[K]T) E.Either[error, map[K]T] {
ok := assert.NotContains(t, actual, expected)
if ok {
return E.Of[error](actual)
}
return E.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")
}
})
}

235
v2/assert/example_test.go Normal file
View File

@@ -0,0 +1,235 @@
// 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_test
import (
"errors"
"strings"
"testing"
"github.com/IBM/fp-go/v2/assert"
"github.com/IBM/fp-go/v2/result"
)
// Example_basicAssertions demonstrates basic equality and inequality assertions
func Example_basicAssertions() {
// This would be in a real test function
var t *testing.T // placeholder for example
// Basic equality
value := 42
assert.Equal(42)(value)(t)
// String equality
name := "Alice"
assert.Equal("Alice")(name)(t)
// Inequality
assert.NotEqual(10)(value)(t)
}
// Example_arrayAssertions demonstrates array-related assertions
func Example_arrayAssertions() {
var t *testing.T // placeholder for example
numbers := []int{1, 2, 3, 4, 5}
// Check array is not empty
assert.ArrayNotEmpty(numbers)(t)
// Check array length
assert.ArrayLength[int](5)(numbers)(t)
// Check array contains a value
assert.ArrayContains(3)(numbers)(t)
}
// Example_mapAssertions demonstrates map-related assertions
func Example_mapAssertions() {
var t *testing.T // placeholder for example
config := map[string]int{
"timeout": 30,
"retries": 3,
"maxSize": 1000,
}
// Check map is not empty
assert.RecordNotEmpty(config)(t)
// Check map length
assert.RecordLength[string, int](3)(config)(t)
// Check map contains key
assert.ContainsKey[int]("timeout")(config)(t)
// Check map does not contain key
assert.NotContainsKey[int]("unknown")(config)(t)
}
// Example_errorAssertions demonstrates error-related assertions
func Example_errorAssertions() {
var t *testing.T // placeholder for example
// Assert no error
err := doSomethingSuccessful()
assert.NoError(err)(t)
// Assert error exists
err2 := doSomethingThatFails()
assert.Error(err2)(t)
}
// Example_resultAssertions demonstrates Result type assertions
func Example_resultAssertions() {
var t *testing.T // placeholder for example
// Assert success
successResult := result.Of[int](42)
assert.Success(successResult)(t)
// Assert failure
failureResult := result.Left[int](errors.New("something went wrong"))
assert.Failure(failureResult)(t)
}
// Example_predicateAssertions demonstrates custom predicate assertions
func Example_predicateAssertions() {
var t *testing.T // placeholder for example
// Test if a number is positive
isPositive := func(n int) bool { return n > 0 }
assert.That(isPositive)(42)(t)
// Test if a string is uppercase
isUppercase := func(s string) bool { return s == strings.ToUpper(s) }
assert.That(isUppercase)("HELLO")(t)
// Test if a number is even
isEven := func(n int) bool { return n%2 == 0 }
assert.That(isEven)(10)(t)
}
// Example_allOf demonstrates combining multiple assertions
func Example_allOf() {
var t *testing.T // placeholder for example
type User struct {
Name string
Age int
Active bool
}
user := User{Name: "Alice", Age: 30, Active: true}
// Combine multiple assertions
assertions := assert.AllOf([]assert.Reader{
assert.Equal("Alice")(user.Name),
assert.Equal(30)(user.Age),
assert.Equal(true)(user.Active),
})
assertions(t)
}
// Example_runAll demonstrates running named test cases
func Example_runAll() {
var t *testing.T // placeholder for example
testcases := map[string]assert.Reader{
"addition": assert.Equal(4)(2 + 2),
"multiplication": assert.Equal(6)(2 * 3),
"subtraction": assert.Equal(1)(3 - 2),
"division": assert.Equal(2)(10 / 5),
}
assert.RunAll(testcases)(t)
}
// Example_local demonstrates focusing assertions on specific properties
func Example_local() {
var t *testing.T // placeholder for 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)
}
// Example_composableAssertions demonstrates building complex assertions
func Example_composableAssertions() {
var t *testing.T // placeholder for example
type Config struct {
Host string
Port int
Timeout int
Retries int
}
config := Config{
Host: "localhost",
Port: 8080,
Timeout: 30,
Retries: 3,
}
// Create focused assertions for each field
validHost := assert.Local(func(c Config) string { return c.Host })(
assert.StringNotEmpty,
)
validPort := assert.Local(func(c Config) int { return c.Port })(
assert.That(func(p int) bool { return p > 0 && p < 65536 }),
)
validTimeout := assert.Local(func(c Config) int { return c.Timeout })(
assert.That(func(t int) bool { return t > 0 }),
)
validRetries := assert.Local(func(c Config) int { return c.Retries })(
assert.That(func(r int) bool { return r >= 0 }),
)
// Combine all assertions
validConfig := assert.AllOf([]assert.Reader{
validHost(config),
validPort(config),
validTimeout(config),
validRetries(config),
})
validConfig(t)
}
// Helper functions for examples
func doSomethingSuccessful() error {
return nil
}
func doSomethingThatFails() error {
return errors.New("operation failed")
}

22
v2/assert/types.go Normal file
View File

@@ -0,0 +1,22 @@
package assert
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]
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

@@ -53,7 +53,7 @@ func MakeBounded[T any](o ord.Ord[T], t, b T) Bounded[T] {
// Clamp returns a function that clamps against the bounds defined in the bounded type
func Clamp[T any](b Bounded[T]) func(T) T {
return ord.Clamp[T](b)(b.Bottom(), b.Top())
return ord.Clamp(b)(b.Bottom(), b.Top())
}
// Reverse reverses the ordering and swaps the bounds

7
v2/builder/builder.go Normal file
View File

@@ -0,0 +1,7 @@
package builder
type (
Builder[T any] interface {
Build() Result[T]
}
)

12
v2/builder/prism.go Normal file
View File

@@ -0,0 +1,12 @@
package builder
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/result"
)
// 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.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
}

15
v2/builder/types.go Normal file
View File

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

View File

@@ -15,14 +15,163 @@
package bytes
// Empty returns an empty byte slice.
//
// This function returns the identity element for the byte slice Monoid,
// which is an empty byte slice. It's useful as a starting point for
// building byte slices or as a default value.
//
// Returns:
// - An empty byte slice ([]byte{})
//
// Properties:
// - Empty() is the identity element for Monoid.Concat
// - Monoid.Concat(Empty(), x) == x
// - Monoid.Concat(x, Empty()) == x
//
// Example - Basic usage:
//
// empty := Empty()
// fmt.Println(len(empty)) // 0
//
// Example - As identity element:
//
// data := []byte("hello")
// result1 := Monoid.Concat(Empty(), data) // []byte("hello")
// result2 := Monoid.Concat(data, Empty()) // []byte("hello")
//
// Example - Building byte slices:
//
// // Start with empty and build up
// buffer := Empty()
// buffer = Monoid.Concat(buffer, []byte("Hello"))
// buffer = Monoid.Concat(buffer, []byte(" "))
// buffer = Monoid.Concat(buffer, []byte("World"))
// // buffer: []byte("Hello World")
//
// See also:
// - Monoid.Empty(): Alternative way to get empty byte slice
// - ConcatAll(): For concatenating multiple byte slices
func Empty() []byte {
return Monoid.Empty()
}
// ToString converts a byte slice to a string.
//
// This function performs a direct conversion from []byte to string.
// The conversion creates a new string with a copy of the byte data.
//
// Parameters:
// - a: The byte slice to convert
//
// Returns:
// - A string containing the same data as the byte slice
//
// Performance Note:
//
// This conversion allocates a new string. For performance-critical code
// that needs to avoid allocations, consider using unsafe.String (Go 1.20+)
// or working directly with byte slices.
//
// Example - Basic conversion:
//
// bytes := []byte("hello")
// str := ToString(bytes)
// fmt.Println(str) // "hello"
//
// Example - Converting binary data:
//
// // ASCII codes for "Hello"
// data := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
// str := ToString(data)
// fmt.Println(str) // "Hello"
//
// Example - Empty byte slice:
//
// empty := Empty()
// str := ToString(empty)
// fmt.Println(str == "") // true
//
// Example - UTF-8 encoded text:
//
// utf8Bytes := []byte("Hello, 世界")
// str := ToString(utf8Bytes)
// fmt.Println(str) // "Hello, 世界"
//
// Example - Round-trip conversion:
//
// original := "test string"
// bytes := []byte(original)
// result := ToString(bytes)
// fmt.Println(original == result) // true
//
// See also:
// - []byte(string): For converting string to byte slice
// - Size(): For getting the length of a byte slice
func ToString(a []byte) string {
return string(a)
}
// Size returns the number of bytes in a byte slice.
//
// This function returns the length of the byte slice, which is the number
// of bytes it contains. This is equivalent to len(as) but provided as a
// named function for use in functional composition.
//
// Parameters:
// - as: The byte slice to measure
//
// Returns:
// - The number of bytes in the slice
//
// Example - Basic usage:
//
// data := []byte("hello")
// size := Size(data)
// fmt.Println(size) // 5
//
// Example - Empty slice:
//
// empty := Empty()
// size := Size(empty)
// fmt.Println(size) // 0
//
// Example - Binary data:
//
// binary := []byte{0x01, 0x02, 0x03, 0x04}
// size := Size(binary)
// fmt.Println(size) // 4
//
// Example - UTF-8 encoded text:
//
// // Note: Size returns byte count, not character count
// utf8 := []byte("Hello, 世界")
// byteCount := Size(utf8)
// fmt.Println(byteCount) // 13 (not 9 characters)
//
// Example - Using in functional composition:
//
// import "github.com/IBM/fp-go/v2/array"
//
// slices := [][]byte{
// []byte("a"),
// []byte("bb"),
// []byte("ccc"),
// }
//
// // Map to get sizes
// sizes := array.Map(Size)(slices)
// // sizes: []int{1, 2, 3}
//
// Example - Checking if slice is empty:
//
// data := []byte("test")
// isEmpty := Size(data) == 0
// fmt.Println(isEmpty) // false
//
// See also:
// - len(): Built-in function for getting slice length
// - ToString(): For converting byte slice to string
func Size(as []byte) int {
return len(as)
}

View File

@@ -187,6 +187,299 @@ func TestOrd(t *testing.T) {
})
}
// TestOrdProperties tests mathematical properties of Ord
func TestOrdProperties(t *testing.T) {
t.Run("reflexivity: x == x", func(t *testing.T) {
testCases := [][]byte{
[]byte{},
[]byte("a"),
[]byte("test"),
[]byte{0x01, 0x02, 0x03},
}
for _, tc := range testCases {
assert.Equal(t, 0, Ord.Compare(tc, tc),
"Compare(%v, %v) should be 0", tc, tc)
assert.True(t, Ord.Equals(tc, tc),
"Equals(%v, %v) should be true", tc, tc)
}
})
t.Run("antisymmetry: if x <= y and y <= x then x == y", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abc")},
{[]byte{}, []byte{}},
{[]byte{0x01}, []byte{0x01}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
if cmp1 <= 0 && cmp2 <= 0 {
assert.True(t, Ord.Equals(tc.a, tc.b),
"If %v <= %v and %v <= %v, they should be equal", tc.a, tc.b, tc.b, tc.a)
}
}
})
t.Run("transitivity: if x <= y and y <= z then x <= z", func(t *testing.T) {
x := []byte("a")
y := []byte("b")
z := []byte("c")
cmpXY := Ord.Compare(x, y)
cmpYZ := Ord.Compare(y, z)
cmpXZ := Ord.Compare(x, z)
if cmpXY <= 0 && cmpYZ <= 0 {
assert.True(t, cmpXZ <= 0,
"If %v <= %v and %v <= %v, then %v <= %v", x, y, y, z, x, z)
}
})
t.Run("totality: either x <= y or y <= x", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abd")},
{[]byte("xyz"), []byte("abc")},
{[]byte{}, []byte("a")},
{[]byte{0x01}, []byte{0x02}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
assert.True(t, cmp1 <= 0 || cmp2 <= 0,
"Either %v <= %v or %v <= %v must be true", tc.a, tc.b, tc.b, tc.a)
}
})
}
// TestEdgeCases tests edge cases and boundary conditions
func TestEdgeCases(t *testing.T) {
t.Run("very large byte slices", func(t *testing.T) {
large := make([]byte, 1000000)
for i := range large {
large[i] = byte(i % 256)
}
size := Size(large)
assert.Equal(t, 1000000, size)
str := ToString(large)
assert.Equal(t, 1000000, len(str))
})
t.Run("concatenating many slices", func(t *testing.T) {
slices := make([][]byte, 100)
for i := range slices {
slices[i] = []byte{byte(i)}
}
result := ConcatAll(slices...)
assert.Equal(t, 100, Size(result))
})
t.Run("null bytes in slice", func(t *testing.T) {
data := []byte{0x00, 0x01, 0x00, 0x02}
size := Size(data)
assert.Equal(t, 4, size)
str := ToString(data)
assert.Equal(t, 4, len(str))
})
t.Run("comparing slices with null bytes", func(t *testing.T) {
a := []byte{0x00, 0x01}
b := []byte{0x00, 0x02}
assert.Equal(t, -1, Ord.Compare(a, b))
})
}
// TestMonoidConcatPerformance tests concatenation performance characteristics
func TestMonoidConcatPerformance(t *testing.T) {
t.Run("ConcatAll vs repeated Concat", func(t *testing.T) {
slices := [][]byte{
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
}
// Using ConcatAll
result1 := ConcatAll(slices...)
// Using repeated Concat
result2 := Monoid.Empty()
for _, s := range slices {
result2 = Monoid.Concat(result2, s)
}
assert.Equal(t, result1, result2)
assert.Equal(t, []byte("abcde"), result1)
})
}
// TestRoundTrip tests round-trip conversions
func TestRoundTrip(t *testing.T) {
t.Run("string to bytes to string", func(t *testing.T) {
original := "Hello, World! 世界"
bytes := []byte(original)
result := ToString(bytes)
assert.Equal(t, original, result)
})
t.Run("bytes to string to bytes", func(t *testing.T) {
original := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
str := ToString(original)
result := []byte(str)
assert.Equal(t, original, result)
})
}
// TestConcatAllVariadic tests ConcatAll with various argument counts
func TestConcatAllVariadic(t *testing.T) {
t.Run("zero arguments", func(t *testing.T) {
result := ConcatAll()
assert.Equal(t, []byte{}, result)
})
t.Run("one argument", func(t *testing.T) {
result := ConcatAll([]byte("test"))
assert.Equal(t, []byte("test"), result)
})
t.Run("two arguments", func(t *testing.T) {
result := ConcatAll([]byte("hello"), []byte("world"))
assert.Equal(t, []byte("helloworld"), result)
})
t.Run("many arguments", func(t *testing.T) {
result := ConcatAll(
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
[]byte("f"),
[]byte("g"),
[]byte("h"),
[]byte("i"),
[]byte("j"),
)
assert.Equal(t, []byte("abcdefghij"), result)
})
}
// Benchmark tests
func BenchmarkToString(b *testing.B) {
data := []byte("Hello, World!")
b.Run("small", func(b *testing.B) {
for b.Loop() {
_ = ToString(data)
}
})
b.Run("large", func(b *testing.B) {
large := make([]byte, 10000)
for i := range large {
large[i] = byte(i % 256)
}
b.ResetTimer()
for b.Loop() {
_ = ToString(large)
}
})
}
func BenchmarkSize(b *testing.B) {
data := []byte("Hello, World!")
for b.Loop() {
_ = Size(data)
}
}
func BenchmarkMonoidConcat(b *testing.B) {
a := []byte("Hello")
c := []byte(" World")
b.Run("small slices", func(b *testing.B) {
for b.Loop() {
_ = Monoid.Concat(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
b.ResetTimer()
for b.Loop() {
_ = Monoid.Concat(large1, large2)
}
})
}
func BenchmarkConcatAll(b *testing.B) {
slices := [][]byte{
[]byte("Hello"),
[]byte(" "),
[]byte("World"),
[]byte("!"),
}
b.Run("few slices", func(b *testing.B) {
for b.Loop() {
_ = ConcatAll(slices...)
}
})
b.Run("many slices", func(b *testing.B) {
many := make([][]byte, 100)
for i := range many {
many[i] = []byte{byte(i)}
}
b.ResetTimer()
for b.Loop() {
_ = ConcatAll(many...)
}
})
}
func BenchmarkOrdCompare(b *testing.B) {
a := []byte("abc")
c := []byte("abd")
b.Run("equal", func(b *testing.B) {
for b.Loop() {
_ = Ord.Compare(a, a)
}
})
b.Run("different", func(b *testing.B) {
for b.Loop() {
_ = Ord.Compare(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
large2[9999] = 1
b.ResetTimer()
for b.Loop() {
_ = Ord.Compare(large1, large2)
}
})
}
// Example tests
func ExampleEmpty() {
empty := Empty()
@@ -219,3 +512,17 @@ func ExampleConcatAll() {
// Output:
}
func ExampleMonoid_concat() {
result := Monoid.Concat([]byte("Hello"), []byte(" World"))
println(string(result)) // Hello World
// Output:
}
func ExampleOrd_compare() {
cmp := Ord.Compare([]byte("abc"), []byte("abd"))
println(cmp) // -1 (abc < abd)
// Output:
}

4
v2/bytes/coverage.out Normal file
View File

@@ -0,0 +1,4 @@
mode: set
github.com/IBM/fp-go/v2/bytes/bytes.go:55.21,57.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:111.32,113.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:175.26,177.2 1 1

View File

@@ -23,12 +23,219 @@ import (
)
var (
// monoid for byte arrays
// Monoid is the Monoid instance for byte slices.
//
// This Monoid combines byte slices through concatenation, with an empty
// byte slice as the identity element. It satisfies the monoid laws:
//
// Identity laws:
// - Monoid.Concat(Monoid.Empty(), x) == x (left identity)
// - Monoid.Concat(x, Monoid.Empty()) == x (right identity)
//
// Associativity law:
// - Monoid.Concat(Monoid.Concat(a, b), c) == Monoid.Concat(a, Monoid.Concat(b, c))
//
// Operations:
// - Empty(): Returns an empty byte slice []byte{}
// - Concat(a, b []byte): Concatenates two byte slices
//
// Example - Basic concatenation:
//
// result := Monoid.Concat([]byte("Hello"), []byte(" World"))
// // result: []byte("Hello World")
//
// Example - Identity element:
//
// empty := Monoid.Empty()
// data := []byte("test")
// result1 := Monoid.Concat(empty, data) // []byte("test")
// result2 := Monoid.Concat(data, empty) // []byte("test")
//
// Example - Building byte buffers:
//
// buffer := Monoid.Empty()
// buffer = Monoid.Concat(buffer, []byte("Line 1\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 2\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 3\n"))
//
// Example - Associativity:
//
// a := []byte("a")
// b := []byte("b")
// c := []byte("c")
// left := Monoid.Concat(Monoid.Concat(a, b), c) // []byte("abc")
// right := Monoid.Concat(a, Monoid.Concat(b, c)) // []byte("abc")
// // left == right
//
// See also:
// - ConcatAll: For concatenating multiple byte slices at once
// - Empty(): Convenience function for getting empty byte slice
Monoid = A.Monoid[byte]()
// ConcatAll concatenates all bytes
// ConcatAll efficiently concatenates multiple byte slices into a single slice.
//
// This function takes a variadic number of byte slices and combines them
// into a single byte slice. It pre-allocates the exact amount of memory
// needed, making it more efficient than repeated concatenation.
//
// Parameters:
// - slices: Zero or more byte slices to concatenate
//
// Returns:
// - A new byte slice containing all input slices concatenated in order
//
// Performance:
//
// ConcatAll is more efficient than using Monoid.Concat repeatedly because
// it calculates the total size upfront and allocates memory once, avoiding
// multiple allocations and copies.
//
// Example - Basic usage:
//
// result := ConcatAll(
// []byte("Hello"),
// []byte(" "),
// []byte("World"),
// )
// // result: []byte("Hello World")
//
// Example - Empty input:
//
// result := ConcatAll()
// // result: []byte{}
//
// Example - Single slice:
//
// result := ConcatAll([]byte("test"))
// // result: []byte("test")
//
// Example - Building protocol messages:
//
// import "encoding/binary"
//
// header := []byte{0x01, 0x02}
// length := make([]byte, 4)
// binary.BigEndian.PutUint32(length, 100)
// payload := []byte("data")
// footer := []byte{0xFF}
//
// message := ConcatAll(header, length, payload, footer)
//
// Example - With empty slices:
//
// result := ConcatAll(
// []byte("a"),
// []byte{},
// []byte("b"),
// []byte{},
// []byte("c"),
// )
// // result: []byte("abc")
//
// Example - Building CSV line:
//
// fields := [][]byte{
// []byte("John"),
// []byte("Doe"),
// []byte("30"),
// }
// separator := []byte(",")
//
// // Interleave fields with separators
// parts := [][]byte{
// fields[0], separator,
// fields[1], separator,
// fields[2],
// }
// line := ConcatAll(parts...)
// // line: []byte("John,Doe,30")
//
// See also:
// - Monoid.Concat: For concatenating exactly two byte slices
// - bytes.Join: Standard library function for joining with separator
ConcatAll = A.ArrayConcatAll[byte]
// Ord implements the default ordering on bytes
// Ord is the Ord instance for byte slices providing lexicographic ordering.
//
// This Ord instance compares byte slices lexicographically (dictionary order),
// comparing bytes from left to right until a difference is found or one slice
// ends. It uses the standard library's bytes.Compare and bytes.Equal functions.
//
// Comparison rules:
// - Compares byte-by-byte from left to right
// - First differing byte determines the order
// - Shorter slice is less than longer slice if all bytes match
// - Empty slice is less than any non-empty slice
//
// Operations:
// - Compare(a, b []byte) int: Returns -1 if a < b, 0 if a == b, 1 if a > b
// - Equals(a, b []byte) bool: Returns true if slices are equal
//
// Example - Basic comparison:
//
// cmp := Ord.Compare([]byte("abc"), []byte("abd"))
// // cmp: -1 (abc < abd)
//
// cmp = Ord.Compare([]byte("xyz"), []byte("abc"))
// // cmp: 1 (xyz > abc)
//
// cmp = Ord.Compare([]byte("test"), []byte("test"))
// // cmp: 0 (equal)
//
// Example - Length differences:
//
// cmp := Ord.Compare([]byte("ab"), []byte("abc"))
// // cmp: -1 (shorter is less)
//
// cmp = Ord.Compare([]byte("abc"), []byte("ab"))
// // cmp: 1 (longer is greater)
//
// Example - Empty slices:
//
// cmp := Ord.Compare([]byte{}, []byte("a"))
// // cmp: -1 (empty is less)
//
// cmp = Ord.Compare([]byte{}, []byte{})
// // cmp: 0 (both empty)
//
// Example - Equality check:
//
// equal := Ord.Equals([]byte("test"), []byte("test"))
// // equal: true
//
// equal = Ord.Equals([]byte("test"), []byte("Test"))
// // equal: false (case-sensitive)
//
// Example - Sorting byte slices:
//
// import "github.com/IBM/fp-go/v2/array"
//
// data := [][]byte{
// []byte("zebra"),
// []byte("apple"),
// []byte("mango"),
// }
//
// sorted := array.Sort(Ord)(data)
// // sorted: [[]byte("apple"), []byte("mango"), []byte("zebra")]
//
// Example - Binary data comparison:
//
// cmp := Ord.Compare([]byte{0x01, 0x02}, []byte{0x01, 0x03})
// // cmp: -1 (0x02 < 0x03)
//
// Example - Finding minimum:
//
// import O "github.com/IBM/fp-go/v2/ord"
//
// a := []byte("xyz")
// b := []byte("abc")
// min := O.Min(Ord)(a, b)
// // min: []byte("abc")
//
// See also:
// - bytes.Compare: Standard library comparison function
// - bytes.Equal: Standard library equality function
// - array.Sort: For sorting slices using an Ord instance
Ord = O.MakeOrd(bytes.Compare, bytes.Equal)
)

View File

@@ -53,17 +53,20 @@ 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
type fieldInfo struct {
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
IsComparable bool // true if the type is comparable (can use ==)
}
// templateData holds data for template rendering
@@ -74,64 +77,95 @@ 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}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}}
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{- 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}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}}
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{- 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}}
{{- if .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
lens{{.Name}} := L.MakeLens(
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}}
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
{{- end}}
return {{.Name}}Lenses{
return {{.Name}}Lenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
),
{{.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 .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
{{- end}}
{{- end}}
return {{.Name}}RefLenses{
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- if .IsComparable}}
lens{{.Name}} := L.MakeLensStrict(
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- else}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
),
lens{{.Name}} := L.MakeLensRef(
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}}
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
{{- 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}}
}
@@ -257,6 +291,259 @@ func isPointerType(expr ast.Expr) bool {
return ok
}
// isComparableType checks if a type expression represents a comparable type.
// Comparable types in Go include:
// - Basic types (bool, numeric types, string)
// - Pointer types
// - Channel types
// - Interface types
// - Structs where all fields are comparable
// - Arrays where the element type is comparable
//
// Non-comparable types include:
// - Slices
// - Maps
// - Functions
//
// 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
// Known non-comparable built-in types
if name == "error" {
// error is an interface, which is comparable
return true
}
// Most basic types and named types are comparable
// We can't determine if a custom type is comparable without type checking,
// so we assume it is (conservative approach)
return true
case *ast.StarExpr:
// Pointer types are always comparable
return true
case *ast.ArrayType:
// Arrays are comparable if their element type is comparable
if t.Len == nil {
// This is a slice (no length), slices are not comparable
return false
}
// Fixed-size array, check element type
return isComparableType(t.Elt, typeParams)
case *ast.MapType:
// Maps are not comparable
return false
case *ast.FuncType:
// Functions are not comparable
return false
case *ast.InterfaceType:
// Interface types are comparable
return true
case *ast.StructType:
// Structs are comparable if all fields are comparable
// We can't easily determine this without full type information,
// so we conservatively return false for struct literals
return false
case *ast.SelectorExpr:
// Qualified identifier (e.g., pkg.Type)
// We can't determine comparability without type information
// Check for known non-comparable types from standard library
if ident, ok := t.X.(*ast.Ident); ok {
pkgName := ident.Name
typeName := t.Sel.Name
// Check for known non-comparable types
if pkgName == "context" && typeName == "Context" {
// context.Context is an interface, which is comparable
return true
}
// For other qualified types, we assume they're comparable
// This is a conservative approach
}
return true
case *ast.IndexExpr, *ast.IndexListExpr:
// Generic types - we can't determine comparability without type information
// For common generic types, we can make educated guesses
var baseExpr ast.Expr
if idx, ok := t.(*ast.IndexExpr); ok {
baseExpr = idx.X
} else if idxList, ok := t.(*ast.IndexListExpr); ok {
baseExpr = idxList.X
}
if sel, ok := baseExpr.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok {
pkgName := ident.Name
typeName := sel.Sel.Name
// Check for known non-comparable generic types
if pkgName == "option" && typeName == "Option" {
// Option types are not comparable (they contain a slice internally)
return false
}
if pkgName == "either" && typeName == "Either" {
// Either types are not comparable
return false
}
}
}
// For other generic types, conservatively assume not comparable
log.Printf("Not comparable type: %v\n", t)
return false
case *ast.ChanType:
// Channel types are comparable
return true
default:
// Unknown type, conservatively assume not comparable
return false
}
}
// 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()
@@ -320,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 {
@@ -331,6 +636,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
typeName := getTypeName(field.Type)
isOptional := false
baseType := typeName
isComparable := false
// Check if field is optional:
// 1. Pointer types are always optional
@@ -344,6 +650,11 @@ func parseFile(filename string) ([]structInfo, string, error) {
isOptional = true
}
// 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, typeParamsMap)
// log.Printf("field %s, type: %v, isComparable: %b\n", name, field.Type, isComparable)
// Extract imports from this field's type
fieldImports := make(map[string]string)
extractImports(field.Type, fieldImports)
@@ -356,20 +667,24 @@ func parseFile(filename string) ([]structInfo, string, error) {
}
fields = append(fields, fieldInfo{
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
IsComparable: isComparable,
})
}
}
}
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,
})
}
@@ -469,8 +784,8 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
// Standard fp-go imports always needed
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\tI \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// Add additional imports collected from field types
for importPath, alias := range allImports {

View File

@@ -168,6 +168,91 @@ func TestIsPointerType(t *testing.T) {
}
}
func TestIsComparableType(t *testing.T) {
tests := []struct {
name string
code string
expected bool
}{
{
name: "basic type - string",
code: "type T struct { F string }",
expected: true,
},
{
name: "basic type - int",
code: "type T struct { F int }",
expected: true,
},
{
name: "basic type - bool",
code: "type T struct { F bool }",
expected: true,
},
{
name: "pointer type",
code: "type T struct { F *string }",
expected: true,
},
{
name: "slice type - not comparable",
code: "type T struct { F []string }",
expected: false,
},
{
name: "map type - not comparable",
code: "type T struct { F map[string]int }",
expected: false,
},
{
name: "array type - comparable if element is",
code: "type T struct { F [5]int }",
expected: true,
},
{
name: "interface type",
code: "type T struct { F interface{} }",
expected: true,
},
{
name: "channel type",
code: "type T struct { F chan int }",
expected: true,
},
{
name: "function type - not comparable",
code: "type T struct { F func() }",
expected: false,
},
{
name: "struct literal - conservatively not comparable",
code: "type T struct { F struct{ X int } }",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := isComparableType(fieldType, map[string]string{})
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasOmitEmpty(t *testing.T) {
tests := []struct {
name string
@@ -337,6 +422,167 @@ type Config struct {
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
}
func TestParseFileWithComparableTypes(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type TypeTest struct {
Name string
Age int
Pointer *string
Slice []string
Map map[string]int
Channel chan 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 TypeTest struct
typeTest := structs[0]
assert.Equal(t, "TypeTest", typeTest.Name)
assert.Len(t, typeTest.Fields, 6)
// Name - string is comparable
assert.Equal(t, "Name", typeTest.Fields[0].Name)
assert.Equal(t, "string", typeTest.Fields[0].TypeName)
assert.False(t, typeTest.Fields[0].IsOptional)
assert.True(t, typeTest.Fields[0].IsComparable, "string should be comparable")
// Age - int is comparable
assert.Equal(t, "Age", typeTest.Fields[1].Name)
assert.Equal(t, "int", typeTest.Fields[1].TypeName)
assert.False(t, typeTest.Fields[1].IsOptional)
assert.True(t, typeTest.Fields[1].IsComparable, "int should be comparable")
// Pointer - pointer is optional, IsComparable not checked for optional fields
assert.Equal(t, "Pointer", typeTest.Fields[2].Name)
assert.Equal(t, "*string", typeTest.Fields[2].TypeName)
assert.True(t, typeTest.Fields[2].IsOptional)
// Slice - not comparable
assert.Equal(t, "Slice", typeTest.Fields[3].Name)
assert.Equal(t, "[]string", typeTest.Fields[3].TypeName)
assert.False(t, typeTest.Fields[3].IsOptional)
assert.False(t, typeTest.Fields[3].IsComparable, "slice should not be comparable")
// Map - not comparable
assert.Equal(t, "Map", typeTest.Fields[4].Name)
assert.Equal(t, "map[string]int", typeTest.Fields[4].TypeName)
assert.False(t, typeTest.Fields[4].IsOptional)
assert.False(t, typeTest.Fields[4].IsComparable, "map should not be comparable")
// Channel - comparable (note: getTypeName returns "any" for channel types, but isComparableType correctly identifies them)
assert.Equal(t, "Channel", typeTest.Fields[5].Name)
assert.Equal(t, "any", typeTest.Fields[5].TypeName) // getTypeName doesn't handle chan types specifically
assert.False(t, typeTest.Fields[5].IsOptional)
assert.True(t, typeTest.Fields[5].IsComparable, "channel should be comparable")
}
func TestLensRefTemplatesWithComparable(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Age", TypeName: "int", IsOptional: false, IsComparable: true},
{Name: "Data", TypeName: "[]byte", IsOptional: false, IsComparable: false},
{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: false},
},
}
// Test constructor template for RefLenses
var constructorBuf bytes.Buffer
err := constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
// Check that MakeLensStrict is used for comparable types in RefLenses
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
// Name field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(",
"comparable field Name should use MakeLensStrict in RefLenses")
// Age field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(",
"comparable field Age should use MakeLensStrict in RefLenses")
// Data field - not comparable, should use MakeLensRef
assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(",
"non-comparable field Data should use MakeLensRef in RefLenses")
}
func TestGenerateLensHelpersWithComparable(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type TestStruct struct {
Name string
Count int
Data []byte
}
`
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 in RefLenses
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
// Name and Count are comparable, should use MakeLensStrict
assert.Contains(t, contentStr, "L.MakeLensStrict",
"comparable fields should use MakeLensStrict in RefLenses")
// Data is not comparable (slice), should use MakeLensRef
assert.Contains(t, contentStr, "L.MakeLensRef",
"non-comparable fields should use MakeLensRef in RefLenses")
// Verify the pattern appears for Name field (comparable)
namePattern := "lensName := L.MakeLensStrict("
assert.Contains(t, contentStr, namePattern,
"Name field should use MakeLensStrict")
// Verify the pattern appears for Data field (not comparable)
dataPattern := "lensData := L.MakeLensRef("
assert.Contains(t, contentStr, dataPattern,
"Data field should use MakeLensRef")
}
func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
@@ -373,11 +619,11 @@ type TestStruct struct {
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLens")
assert.Contains(t, contentStr, "MakeTestStructLens")
assert.Contains(t, contentStr, "TestStructLenses")
assert.Contains(t, contentStr, "MakeTestStructLenses")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
assert.Contains(t, contentStr, "I.FromZero")
assert.Contains(t, contentStr, "IO.FromZero")
}
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
@@ -411,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},
},
}
@@ -424,7 +670,9 @@ func TestLensTemplates(t *testing.T) {
structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]")
assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]")
// Test constructor template
var constructorBuf bytes.Buffer
@@ -434,19 +682,21 @@ func TestLensTemplates(t *testing.T) {
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
assert.Contains(t, constructorStr, "return TestStructLenses{")
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
assert.Contains(t, constructorStr, "I.FromZero")
assert.Contains(t, constructorStr, "Name: lensName,")
assert.Contains(t, constructorStr, "NameO: lensNameO,")
assert.Contains(t, constructorStr, "Value: lensValue,")
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
assert.Contains(t, constructorStr, "IO.FromZero")
}
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
},
}
@@ -458,9 +708,13 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Pointer LO.LensO[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]", "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]", "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]")
// Test constructor template
var constructorBuf bytes.Buffer
@@ -469,9 +723,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()")
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()")
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()")
assert.Contains(t, constructorStr, "IO.FromZero[string]()")
assert.Contains(t, constructorStr, "IO.FromZero[int]()")
assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
}
func TestLensCommandFlags(t *testing.T) {
@@ -480,7 +734,7 @@ func TestLensCommandFlags(t *testing.T) {
assert.Equal(t, "lens", cmd.Name)
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")
// Check flags
assert.Len(t, cmd.Flags, 3)
@@ -501,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")
}

View File

@@ -27,7 +27,7 @@ import (
func TestMap(t *testing.T) {
fa := Make[string, int]("foo")
assert.Equal(t, fa, F.Pipe1(fa, Map[string, int](utils.Double)))
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
}
func TestOf(t *testing.T) {

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

@@ -13,20 +13,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package ioeither
package ioresult
import (
"context"
"github.com/IBM/fp-go/v2/either"
IOE "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/result"
)
// withContext wraps an existing IOEither and performs a context check for cancellation before delegating
func WithContext[A any](ctx context.Context, ma IOE.IOEither[error, A]) IOE.IOEither[error, A] {
return func() either.Either[error, A] {
if err := context.Cause(ctx); err != nil {
return either.Left[A](err)
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
return ma()
}

View File

@@ -0,0 +1,11 @@
package ioresult
import (
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
)
type (
IOResult[T any] = ioresult.IOResult[T]
Result[T any] = result.Result[T]
)

View File

@@ -0,0 +1,16 @@
package readerio
import (
RIO "github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func Bracket[
A, B, ANY any](
acquire ReaderIO[A],
use Kleisli[A, B],
release func(A, B) ReaderIO[ANY],
) ReaderIO[B] {
return RIO.Bracket(acquire, use, release)
}

View File

@@ -0,0 +1,20 @@
package readerio
import (
"context"
"github.com/IBM/fp-go/v2/reader"
RIO "github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
return RIO.SequenceReader(ma)
}
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderIO[A]) Kleisli[R, B] {
return RIO.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,29 @@
package readerio
import (
"context"
"log/slog"
"github.com/IBM/fp-go/v2/logging"
)
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) Kleisli[A, A] {
return func(a A) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
// logger
logger := cb(ctx)
return func() A {
logger.LogAttrs(ctx, logLevel, message, slog.Any("value", a))
return a
}
}
}
}
//go:inline
func SLog[A any](message string) Kleisli[A, A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}

View File

@@ -0,0 +1,755 @@
// 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 readerio
import (
"context"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/reader"
RIO "github.com/IBM/fp-go/v2/readerio"
)
const (
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
useParallel = true
)
// MonadMap transforms the success value of a [ReaderIO] using the provided function.
// If the computation fails, the error is propagated unchanged.
//
// Parameters:
// - fa: The ReaderIO to transform
// - f: The transformation function
//
// Returns a new ReaderIO with the transformed value.
//
//go:inline
func MonadMap[A, B any](fa ReaderIO[A], f func(A) B) ReaderIO[B] {
return RIO.MonadMap(fa, f)
}
// Map transforms the success value of a [ReaderIO] using the provided function.
// This is the curried version of [MonadMap], useful for composition.
//
// Parameters:
// - f: The transformation function
//
// Returns a function that transforms a ReaderIO.
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return RIO.Map[context.Context](f)
}
// MonadMapTo replaces the success value of a [ReaderIO] with a constant value.
// If the computation fails, the error is propagated unchanged.
//
// Parameters:
// - fa: The ReaderIO to transform
// - b: The constant value to use
//
// Returns a new ReaderIO with the constant value.
//
//go:inline
func MonadMapTo[A, B any](fa ReaderIO[A], b B) ReaderIO[B] {
return RIO.MonadMapTo(fa, b)
}
// MapTo replaces the success value of a [ReaderIO] with a constant value.
// This is the curried version of [MonadMapTo].
//
// Parameters:
// - b: The constant value to use
//
// Returns a function that transforms a ReaderIO.
//
//go:inline
func MapTo[A, B any](b B) Operator[A, B] {
return RIO.MapTo[context.Context, A](b)
}
// MonadChain sequences two [ReaderIO] computations, where the second depends on the result of the first.
// If the first computation fails, the second is not executed.
//
// Parameters:
// - ma: The first ReaderIO
// - f: Function that produces the second ReaderIO based on the first's result
//
// Returns a new ReaderIO representing the sequenced computation.
//
//go:inline
func MonadChain[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[B] {
return RIO.MonadChain(ma, f)
}
// Chain sequences two [ReaderIO] computations, where the second depends on the result of the first.
// This is the curried version of [MonadChain], useful for composition.
//
// Parameters:
// - f: Function that produces the second ReaderIO based on the first's result
//
// Returns a function that sequences ReaderIO computations.
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return RIO.Chain(f)
}
// MonadChainFirst sequences two [ReaderIO] computations but returns the result of the first.
// The second computation is executed for its side effects only.
//
// Parameters:
// - ma: The first ReaderIO
// - f: Function that produces the second ReaderIO
//
// Returns a ReaderIO with the result of the first computation.
//
//go:inline
func MonadChainFirst[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[A] {
return RIO.MonadChainFirst(ma, f)
}
// MonadTap executes a side-effect computation but returns the original value.
// This is an alias for [MonadChainFirst] and is useful for operations like logging
// or validation that should not affect the main computation flow.
//
// Parameters:
// - ma: The ReaderIO to tap
// - f: Function that produces a side-effect ReaderIO
//
// Returns a ReaderIO with the original value after executing the side effect.
//
//go:inline
func MonadTap[A, B any](ma ReaderIO[A], f Kleisli[A, B]) ReaderIO[A] {
return RIO.MonadTap(ma, f)
}
// ChainFirst sequences two [ReaderIO] computations but returns the result of the first.
// This is the curried version of [MonadChainFirst].
//
// Parameters:
// - f: Function that produces the second ReaderIO
//
// Returns a function that sequences ReaderIO computations.
//
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIO.ChainFirst(f)
}
// Tap executes a side-effect computation but returns the original value.
// This is the curried version of [MonadTap], an alias for [ChainFirst].
//
// Parameters:
// - f: Function that produces a side-effect ReaderIO
//
// Returns a function that taps ReaderIO computations.
//
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIO.Tap(f)
}
// Of creates a [ReaderIO] that always succeeds with the given value.
// This is the same as [Right] and represents the monadic return operation.
//
// Parameters:
// - a: The value to wrap
//
// Returns a ReaderIO that always succeeds with the given value.
//
//go:inline
func Of[A any](a A) ReaderIO[A] {
return RIO.Of[context.Context](a)
}
// MonadApPar implements parallel applicative application for [ReaderIO].
// It executes the function and value computations in parallel where possible,
// potentially improving performance for independent operations.
//
// Parameters:
// - fab: ReaderIO containing a function
// - fa: ReaderIO containing a value
//
// Returns a ReaderIO with the function applied to the value.
//
//go:inline
func MonadApPar[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
return RIO.MonadApPar(fab, fa)
}
// MonadAp implements applicative application for [ReaderIO].
// By default, it uses parallel execution ([MonadApPar]) but can be configured to use
// sequential execution ([MonadApSeq]) via the useParallel constant.
//
// Parameters:
// - fab: ReaderIO containing a function
// - fa: ReaderIO containing a value
//
// Returns a ReaderIO with the function applied to the value.
//
//go:inline
func MonadAp[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
// dispatch to the configured version
if useParallel {
return MonadApPar(fab, fa)
}
return MonadApSeq(fab, fa)
}
// MonadApSeq implements sequential applicative application for [ReaderIO].
// It executes the function computation first, then the value computation.
//
// Parameters:
// - fab: ReaderIO containing a function
// - fa: ReaderIO containing a value
//
// Returns a ReaderIO with the function applied to the value.
//
//go:inline
func MonadApSeq[B, A any](fab ReaderIO[func(A) B], fa ReaderIO[A]) ReaderIO[B] {
return RIO.MonadApSeq(fab, fa)
}
// Ap applies a function wrapped in a [ReaderIO] to a value wrapped in a ReaderIO.
// This is the curried version of [MonadAp], using the default execution mode.
//
// Parameters:
// - fa: ReaderIO containing a value
//
// Returns a function that applies a ReaderIO function to the value.
//
//go:inline
func Ap[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
return RIO.Ap[B](fa)
}
// ApSeq applies a function wrapped in a [ReaderIO] to a value sequentially.
// This is the curried version of [MonadApSeq].
//
// Parameters:
// - fa: ReaderIO containing a value
//
// Returns a function that applies a ReaderIO function to the value sequentially.
//
//go:inline
func ApSeq[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadApSeq[B, A], fa)
}
// ApPar applies a function wrapped in a [ReaderIO] to a value in parallel.
// This is the curried version of [MonadApPar].
//
// Parameters:
// - fa: ReaderIO containing a value
//
// Returns a function that applies a ReaderIO function to the value in parallel.
//
//go:inline
func ApPar[B, A any](fa ReaderIO[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadApPar[B, A], fa)
}
// Ask returns a [ReaderIO] that provides access to the context.
// This is useful for accessing the [context.Context] within a computation.
//
// Returns a ReaderIO that produces the context.
//
//go:inline
func Ask() ReaderIO[context.Context] {
return RIO.Ask[context.Context]()
}
// FromIO converts an [IO] into a [ReaderIO].
// The IO computation always succeeds, so it's wrapped in Right.
//
// Parameters:
// - t: The IO to convert
//
// Returns a ReaderIO that executes the IO and wraps the result in Right.
//
//go:inline
func FromIO[A any](t IO[A]) ReaderIO[A] {
return RIO.FromIO[context.Context](t)
}
// FromReader converts a [Reader] into a [ReaderIO].
// The Reader computation is lifted into the IO context, allowing it to be
// composed with other ReaderIO operations.
//
// Parameters:
// - t: The Reader to convert
//
// Returns a ReaderIO that executes the Reader and wraps the result in IO.
//
//go:inline
func FromReader[A any](t Reader[context.Context, A]) ReaderIO[A] {
return RIO.FromReader(t)
}
// FromLazy converts a [Lazy] computation into a [ReaderIO].
// The Lazy computation always succeeds, so it's wrapped in Right.
// This is an alias for [FromIO] since Lazy and IO have the same structure.
//
// Parameters:
// - t: The Lazy computation to convert
//
// Returns a ReaderIO that executes the Lazy computation and wraps the result in Right.
//
//go:inline
func FromLazy[A any](t Lazy[A]) ReaderIO[A] {
return RIO.FromIO[context.Context](t)
}
// MonadChainIOK chains a function that returns an [IO] into a [ReaderIO] computation.
// The IO computation always succeeds, so it's wrapped in Right.
//
// Parameters:
// - ma: The ReaderIO to chain from
// - f: Function that produces an IO
//
// Returns a new ReaderIO with the chained IO computation.
//
//go:inline
func MonadChainIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[B] {
return RIO.MonadChainIOK(ma, f)
}
// ChainIOK chains a function that returns an [IO] into a [ReaderIO] computation.
// This is the curried version of [MonadChainIOK].
//
// Parameters:
// - f: Function that produces an IO
//
// Returns a function that chains the IO-returning function.
//
//go:inline
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
return RIO.ChainIOK[context.Context](f)
}
// MonadChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// The IO computation is executed for its side effects only.
//
// Parameters:
// - ma: The ReaderIO to chain from
// - f: Function that produces an IO
//
// Returns a ReaderIO with the original value after executing the IO.
//
//go:inline
func MonadChainFirstIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[A] {
return RIO.MonadChainFirstIOK(ma, f)
}
// MonadTapIOK chains a function that returns an [IO] but keeps the original value.
// This is an alias for [MonadChainFirstIOK] and is useful for side effects like logging.
//
// Parameters:
// - ma: The ReaderIO to tap
// - f: Function that produces an IO for side effects
//
// Returns a ReaderIO with the original value after executing the IO.
//
//go:inline
func MonadTapIOK[A, B any](ma ReaderIO[A], f func(A) IO[B]) ReaderIO[A] {
return RIO.MonadTapIOK(ma, f)
}
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// This is the curried version of [MonadChainFirstIOK].
//
// Parameters:
// - f: Function that produces an IO
//
// Returns a function that chains the IO-returning function.
//
//go:inline
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIO.ChainFirstIOK[context.Context](f)
}
// TapIOK chains a function that returns an [IO] but keeps the original value.
// This is the curried version of [MonadTapIOK], an alias for [ChainFirstIOK].
//
// Parameters:
// - f: Function that produces an IO for side effects
//
// Returns a function that taps with IO-returning functions.
//
//go:inline
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIO.TapIOK[context.Context](f)
}
// Defer creates a [ReaderIO] by lazily generating a new computation each time it's executed.
// This is useful for creating computations that should be re-evaluated on each execution.
//
// Parameters:
// - gen: Lazy generator function that produces a ReaderIO
//
// Returns a ReaderIO that generates a fresh computation on each execution.
//
//go:inline
func Defer[A any](gen Lazy[ReaderIO[A]]) ReaderIO[A] {
return RIO.Defer(gen)
}
// Memoize computes the value of the provided [ReaderIO] monad lazily but exactly once.
// The context used to compute the value is the context of the first call, so do not use this
// method if the value has a functional dependency on the content of the context.
//
// Parameters:
// - rdr: The ReaderIO to memoize
//
// Returns a ReaderIO that caches its result after the first execution.
//
//go:inline
func Memoize[A any](rdr ReaderIO[A]) ReaderIO[A] {
return RIO.Memoize(rdr)
}
// Flatten converts a nested [ReaderIO] into a flat [ReaderIO].
// This is equivalent to [MonadChain] with the identity function.
//
// Parameters:
// - rdr: The nested ReaderIO to flatten
//
// Returns a flattened ReaderIO.
//
//go:inline
func Flatten[A any](rdr ReaderIO[ReaderIO[A]]) ReaderIO[A] {
return RIO.Flatten(rdr)
}
// MonadFlap applies a value to a function wrapped in a [ReaderIO].
// This is the reverse of [MonadAp], useful in certain composition scenarios.
//
// Parameters:
// - fab: ReaderIO containing a function
// - a: The value to apply to the function
//
// Returns a ReaderIO with the function applied to the value.
//
//go:inline
func MonadFlap[B, A any](fab ReaderIO[func(A) B], a A) ReaderIO[B] {
return RIO.MonadFlap(fab, a)
}
// Flap applies a value to a function wrapped in a [ReaderIO].
// This is the curried version of [MonadFlap].
//
// Parameters:
// - a: The value to apply to the function
//
// Returns a function that applies the value to a ReaderIO function.
//
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return RIO.Flap[context.Context, B](a)
}
// MonadChainReaderK chains a [ReaderIO] with a function that returns a [Reader].
// The Reader is lifted into the ReaderIO context, allowing composition of
// Reader and ReaderIO operations.
//
// Parameters:
// - ma: The ReaderIO to chain from
// - f: Function that produces a Reader
//
// Returns a new ReaderIO with the chained Reader computation.
//
//go:inline
func MonadChainReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[B] {
return RIO.MonadChainReaderK(ma, f)
}
// ChainReaderK chains a [ReaderIO] with a function that returns a [Reader].
// This is the curried version of [MonadChainReaderK].
//
// Parameters:
// - f: Function that produces a Reader
//
// Returns a function that chains Reader-returning functions.
//
//go:inline
func ChainReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIO.ChainReaderK(f)
}
// MonadChainFirstReaderK chains a function that returns a [Reader] but keeps the original value.
// The Reader computation is executed for its side effects only.
//
// Parameters:
// - ma: The ReaderIO to chain from
// - f: Function that produces a Reader
//
// Returns a ReaderIO with the original value after executing the Reader.
//
//go:inline
func MonadChainFirstReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[A] {
return RIO.MonadChainFirstReaderK(ma, f)
}
// MonadTapReaderK chains a function that returns a [Reader] but keeps the original value.
// This is an alias for [MonadChainFirstReaderK] and is useful for side effects.
//
// Parameters:
// - ma: The ReaderIO to tap
// - f: Function that produces a Reader for side effects
//
// Returns a ReaderIO with the original value after executing the Reader.
//
//go:inline
func MonadTapReaderK[A, B any](ma ReaderIO[A], f reader.Kleisli[context.Context, A, B]) ReaderIO[A] {
return RIO.MonadTapReaderK(ma, f)
}
// ChainFirstReaderK chains a function that returns a [Reader] but keeps the original value.
// This is the curried version of [MonadChainFirstReaderK].
//
// Parameters:
// - f: Function that produces a Reader
//
// Returns a function that chains Reader-returning functions while preserving the original value.
//
//go:inline
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIO.ChainFirstReaderK(f)
}
// TapReaderK chains a function that returns a [Reader] but keeps the original value.
// This is the curried version of [MonadTapReaderK], an alias for [ChainFirstReaderK].
//
// Parameters:
// - f: Function that produces a Reader for side effects
//
// Returns a function that taps with Reader-returning functions.
//
//go:inline
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIO.TapReaderK(f)
}
// Read executes a [ReaderIO] with a given context, returning the resulting [IO].
// This is useful for providing the context dependency and obtaining an IO action
// that can be executed later.
//
// Parameters:
// - r: The context to provide to the ReaderIO
//
// Returns a function that converts a ReaderIO into an IO by applying the context.
//
//go:inline
func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
return RIO.Read[A](r)
}
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerio.FromReader(func(ctx context.Context) string {
// if user := ctx.Value(userKey); user != nil {
// return user.(string)
// }
// return "unknown"
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
return func() A {
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}
// WithTimeout adds a timeout to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderIO is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{} after 5s timeout
//
// Successful Example:
//
// quickFetch := readerio.Of(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{Value: "quick"}
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](deadline),
// )
// data := result(context.Background())() // Returns Data{} if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](laterDeadline),
// )
// data := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
})
}

View File

@@ -0,0 +1,502 @@
// 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 readerio
import (
"context"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
G "github.com/IBM/fp-go/v2/io"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
func TestMonadMap(t *testing.T) {
rio := Of(5)
doubled := MonadMap(rio, N.Mul(2))
result := doubled(context.Background())()
assert.Equal(t, 10, result)
}
func TestMap(t *testing.T) {
g := F.Pipe1(
Of(1),
Map(utils.Double),
)
assert.Equal(t, 2, g(context.Background())())
}
func TestMonadMapTo(t *testing.T) {
rio := Of(42)
replaced := MonadMapTo(rio, "constant")
result := replaced(context.Background())()
assert.Equal(t, "constant", result)
}
func TestMapTo(t *testing.T) {
result := F.Pipe1(
Of(42),
MapTo[int]("constant"),
)
assert.Equal(t, "constant", result(context.Background())())
}
func TestMonadChain(t *testing.T) {
rio1 := Of(5)
result := MonadChain(rio1, func(n int) ReaderIO[int] {
return Of(n * 3)
})
assert.Equal(t, 15, result(context.Background())())
}
func TestChain(t *testing.T) {
result := F.Pipe1(
Of(5),
Chain(func(n int) ReaderIO[int] {
return Of(n * 3)
}),
)
assert.Equal(t, 15, result(context.Background())())
}
func TestMonadChainFirst(t *testing.T) {
sideEffect := 0
rio := Of(42)
result := MonadChainFirst(rio, func(n int) ReaderIO[string] {
sideEffect = n
return Of("side effect")
})
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestChainFirst(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of(42),
ChainFirst(func(n int) ReaderIO[string] {
sideEffect = n
return Of("side effect")
}),
)
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadTap(t *testing.T) {
sideEffect := 0
rio := Of(42)
result := MonadTap(rio, func(n int) ReaderIO[func()] {
sideEffect = n
return Of(func() {})
})
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestTap(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of(42),
Tap(func(n int) ReaderIO[func()] {
sideEffect = n
return Of(func() {})
}),
)
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestOf(t *testing.T) {
rio := Of(100)
result := rio(context.Background())()
assert.Equal(t, 100, result)
}
func TestMonadAp(t *testing.T) {
fabIO := Of(N.Mul(2))
faIO := Of(5)
result := MonadAp(fabIO, faIO)
assert.Equal(t, 10, result(context.Background())())
}
func TestAp(t *testing.T) {
g := F.Pipe1(
Of(utils.Double),
Ap[int](Of(1)),
)
assert.Equal(t, 2, g(context.Background())())
}
func TestMonadApSeq(t *testing.T) {
fabIO := Of(N.Add(10))
faIO := Of(5)
result := MonadApSeq(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
}
func TestApSeq(t *testing.T) {
g := F.Pipe1(
Of(N.Add(10)),
ApSeq[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
}
func TestMonadApPar(t *testing.T) {
fabIO := Of(N.Add(10))
faIO := Of(5)
result := MonadApPar(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
}
func TestApPar(t *testing.T) {
g := F.Pipe1(
Of(N.Add(10)),
ApPar[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
}
func TestAsk(t *testing.T) {
rio := Ask()
ctx := context.WithValue(context.Background(), "key", "value")
result := rio(ctx)()
assert.Equal(t, ctx, result)
}
func TestFromIO(t *testing.T) {
ioAction := G.Of(42)
rio := FromIO(ioAction)
result := rio(context.Background())()
assert.Equal(t, 42, result)
}
func TestFromReader(t *testing.T) {
rdr := func(ctx context.Context) int {
return 42
}
rio := FromReader(rdr)
result := rio(context.Background())()
assert.Equal(t, 42, result)
}
func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
rio := FromLazy(lazy)
result := rio(context.Background())()
assert.Equal(t, 42, result)
}
func TestMonadChainIOK(t *testing.T) {
rio := Of(5)
result := MonadChainIOK(rio, func(n int) G.IO[int] {
return G.Of(n * 4)
})
assert.Equal(t, 20, result(context.Background())())
}
func TestChainIOK(t *testing.T) {
result := F.Pipe1(
Of(5),
ChainIOK(func(n int) G.IO[int] {
return G.Of(n * 4)
}),
)
assert.Equal(t, 20, result(context.Background())())
}
func TestMonadChainFirstIOK(t *testing.T) {
sideEffect := 0
rio := Of(42)
result := MonadChainFirstIOK(rio, func(n int) G.IO[string] {
sideEffect = n
return G.Of("side effect")
})
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestChainFirstIOK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of(42),
ChainFirstIOK(func(n int) G.IO[string] {
sideEffect = n
return G.Of("side effect")
}),
)
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadTapIOK(t *testing.T) {
sideEffect := 0
rio := Of(42)
result := MonadTapIOK(rio, func(n int) G.IO[func()] {
sideEffect = n
return G.Of(func() {})
})
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestTapIOK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of(42),
TapIOK(func(n int) G.IO[func()] {
sideEffect = n
return G.Of(func() {})
}),
)
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestDefer(t *testing.T) {
counter := 0
rio := Defer(func() ReaderIO[int] {
counter++
return Of(counter)
})
result1 := rio(context.Background())()
result2 := rio(context.Background())()
assert.Equal(t, 1, result1)
assert.Equal(t, 2, result2)
}
func TestMemoize(t *testing.T) {
counter := 0
rio := Of(0)
memoized := Memoize(MonadMap(rio, func(int) int {
counter++
return counter
}))
result1 := memoized(context.Background())()
result2 := memoized(context.Background())()
assert.Equal(t, 1, result1)
assert.Equal(t, 1, result2) // Same value, memoized
}
func TestFlatten(t *testing.T) {
nested := Of(Of(42))
flattened := Flatten(nested)
result := flattened(context.Background())()
assert.Equal(t, 42, result)
}
func TestMonadFlap(t *testing.T) {
fabIO := Of(N.Mul(3))
result := MonadFlap(fabIO, 7)
assert.Equal(t, 21, result(context.Background())())
}
func TestFlap(t *testing.T) {
result := F.Pipe1(
Of(N.Mul(3)),
Flap[int](7),
)
assert.Equal(t, 21, result(context.Background())())
}
func TestMonadChainReaderK(t *testing.T) {
rio := Of(5)
result := MonadChainReaderK(rio, func(n int) reader.Reader[context.Context, int] {
return func(ctx context.Context) int { return n * 2 }
})
assert.Equal(t, 10, result(context.Background())())
}
func TestChainReaderK(t *testing.T) {
result := F.Pipe1(
Of(5),
ChainReaderK(func(n int) reader.Reader[context.Context, int] {
return func(ctx context.Context) int { return n * 2 }
}),
)
assert.Equal(t, 10, result(context.Background())())
}
func TestMonadChainFirstReaderK(t *testing.T) {
sideEffect := 0
rio := Of(42)
result := MonadChainFirstReaderK(rio, func(n int) reader.Reader[context.Context, string] {
return func(ctx context.Context) string {
sideEffect = n
return "side effect"
}
})
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestChainFirstReaderK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of(42),
ChainFirstReaderK(func(n int) reader.Reader[context.Context, string] {
return func(ctx context.Context) string {
sideEffect = n
return "side effect"
}
}),
)
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestMonadTapReaderK(t *testing.T) {
sideEffect := 0
rio := Of(42)
result := MonadTapReaderK(rio, func(n int) reader.Reader[context.Context, func()] {
return func(ctx context.Context) func() {
sideEffect = n
return func() {}
}
})
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestTapReaderK(t *testing.T) {
sideEffect := 0
result := F.Pipe1(
Of(42),
TapReaderK(func(n int) reader.Reader[context.Context, func()] {
return func(ctx context.Context) func() {
sideEffect = n
return func() {}
}
}),
)
value := result(context.Background())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestRead(t *testing.T) {
rio := Of(42)
ctx := context.Background()
ioAction := Read[int](ctx)(rio)
result := ioAction()
assert.Equal(t, 42, result)
}
func TestComplexPipeline(t *testing.T) {
// Test a complex pipeline combining multiple operations
result := F.Pipe3(
Ask(),
Map(func(ctx context.Context) int { return 5 }),
Chain(func(n int) ReaderIO[int] {
return Of(n * 2)
}),
Map(N.Add(10)),
)
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
}
func TestFromIOWithChain(t *testing.T) {
ioAction := G.Of(10)
result := F.Pipe1(
FromIO(ioAction),
Chain(func(n int) ReaderIO[int] {
return Of(n + 5)
}),
)
assert.Equal(t, 15, result(context.Background())())
}
func TestTapWithLogging(t *testing.T) {
// Simulate logging scenario
logged := []int{}
result := F.Pipe3(
Of(42),
Tap(func(n int) ReaderIO[func()] {
logged = append(logged, n)
return Of(func() {})
}),
Map(N.Mul(2)),
Tap(func(n int) ReaderIO[func()] {
logged = append(logged, n)
return Of(func() {})
}),
)
value := result(context.Background())()
assert.Equal(t, 84, value)
assert.Equal(t, []int{42, 84}, logged)
}

View File

@@ -0,0 +1,69 @@
// 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 readerio
import (
"context"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
type (
// Lazy represents a deferred computation that produces a value of type A when executed.
// The computation is not executed until explicitly invoked.
Lazy[A any] = lazy.Lazy[A]
// IO represents a side-effectful computation that produces a value of type A.
// The computation is deferred and only executed when invoked.
//
// IO[A] is equivalent to func() A
IO[A any] = io.IO[A]
// Reader represents a computation that depends on a context of type R.
// This is used for dependency injection and accessing shared context.
//
// Reader[R, A] is equivalent to func(R) A
Reader[R, A any] = reader.Reader[R, A]
// ReaderIO represents a context-dependent computation that performs side effects.
// This is specialized to use [context.Context] as the context type.
//
// ReaderIO[A] is equivalent to func(context.Context) func() A
ReaderIO[A any] = readerio.ReaderIO[context.Context, A]
// Kleisli represents a Kleisli arrow for the ReaderIO monad.
// It is a function that takes a value of type A and returns a ReaderIO computation
// that produces a value of type B.
//
// Kleisli arrows are used for composing monadic computations and are fundamental
// to functional programming patterns involving effects and context.
//
// Kleisli[A, B] is equivalent to func(A) func(context.Context) func() B
Kleisli[A, B any] = reader.Reader[A, ReaderIO[B]]
// Operator represents a transformation from one ReaderIO computation to another.
// It takes a ReaderIO[A] and returns a ReaderIO[B], allowing for the composition
// of context-dependent, side-effectful computations.
//
// Operators are useful for building pipelines of ReaderIO computations where
// each step can depend on the previous computation's result.
//
// Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B
Operator[A, B any] = Kleisli[ReaderIO[A], B]
)

View File

@@ -1,251 +0,0 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:27.21,29.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:35.47,42.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:48.47,54.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:60.47,66.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:71.46,76.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/bind.go:82.47,89.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/bracket.go:33.21,44.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:35.65,36.47 1 1
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:36.47,37.44 1 1
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:37.44,39.4 1 1
github.com/IBM/fp-go/v2/context/readerioeither/cancel.go:40.3,40.40 1 1
github.com/IBM/fp-go/v2/context/readerioeither/eq.go:42.84,44.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:18.91,20.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:24.93,26.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:30.101,32.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:36.103,38.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:43.36,48.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:53.36,58.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:63.36,68.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:71.98,76.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:79.101,84.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:87.101,92.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:95.129,96.68 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:96.68,102.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:106.132,107.68 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:107.68,113.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:117.132,118.68 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:118.68,124.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:129.113,131.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:135.115,137.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:143.40,150.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:156.40,163.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:169.40,176.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:179.126,185.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:188.129,194.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:197.129,203.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:206.185,207.76 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:207.76,215.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:219.188,220.76 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:220.76,228.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:232.188,233.76 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:233.76,241.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:246.125,248.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:252.127,254.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:261.44,270.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:277.44,286.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:293.44,302.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:305.154,312.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:315.157,322.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:325.157,332.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:335.241,336.84 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:336.84,346.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:350.244,351.84 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:351.84,361.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:365.244,366.84 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:366.84,376.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:381.137,383.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:387.139,389.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:397.48,408.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:416.48,427.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:435.48,446.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:449.182,457.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:460.185,468.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:471.185,479.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:482.297,483.92 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:483.92,495.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:499.300,500.92 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:500.92,512.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:516.300,517.92 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:517.92,529.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:534.149,536.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:540.151,542.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:551.52,564.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:573.52,586.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:595.52,608.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:611.210,620.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:623.213,632.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:635.213,644.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:647.353,648.100 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:648.100,662.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:666.356,667.100 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:667.100,681.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:685.356,686.100 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:686.100,700.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:705.161,707.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:711.163,713.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:723.56,738.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:748.56,763.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:773.56,788.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:791.238,801.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:804.241,814.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:817.241,827.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:830.409,831.108 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:831.108,847.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:851.412,852.108 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:852.108,868.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:872.412,873.108 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:873.108,889.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:894.173,896.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:900.175,902.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:913.60,930.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:941.60,958.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:969.60,986.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:989.266,1000.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1003.269,1014.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1017.269,1028.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1031.465,1032.116 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1032.116,1050.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1054.468,1055.116 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1055.116,1073.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1077.468,1078.116 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1078.116,1096.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1101.185,1103.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1107.187,1109.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1121.64,1140.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1152.64,1171.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1183.64,1202.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1205.294,1217.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1220.297,1232.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1235.297,1247.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1250.521,1251.124 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1251.124,1271.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1275.524,1276.124 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1276.124,1296.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1300.524,1301.124 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1301.124,1321.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1326.197,1328.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1332.199,1334.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1347.68,1368.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1381.68,1402.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1415.68,1436.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1439.322,1452.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1455.325,1468.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1471.325,1484.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1487.577,1488.132 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1488.132,1510.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1514.580,1515.132 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1515.132,1537.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1541.580,1542.132 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1542.132,1564.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1569.210,1571.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1575.212,1577.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1591.74,1614.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1628.74,1651.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1665.74,1688.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1691.356,1705.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1708.359,1722.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1725.359,1739.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1742.645,1743.144 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1743.144,1767.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1771.648,1772.144 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1772.144,1796.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1800.648,1801.144 1 0
github.com/IBM/fp-go/v2/context/readerioeither/gen.go:1801.144,1825.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:36.61,43.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:52.64,59.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:68.64,75.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:85.61,93.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/monoid.go:103.63,108.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:42.55,44.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:52.45,54.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:62.42,64.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:74.78,76.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:85.75,87.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:97.72,99.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:108.69,110.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:120.96,122.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:131.93,133.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:143.101,145.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:154.71,156.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:165.39,167.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:169.93,173.56 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:173.56,174.32 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:174.32,174.47 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:189.98,194.47 3 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:194.47,196.44 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:196.44,198.4 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:200.3,200.27 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:200.27,202.45 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:202.45,204.5 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:207.4,213.47 5 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:227.95,229.17 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:229.17,231.3 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:232.2,232.28 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:243.98,245.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:254.91,256.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:265.94,267.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:276.94,278.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:288.95,290.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:299.73,301.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:307.44,309.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:319.95,321.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:330.95,332.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:342.100,344.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:353.100,355.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:364.116,366.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:375.75,377.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:386.47,388.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:398.51,400.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:406.39,407.47 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:407.47,408.27 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:408.27,411.4 2 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:423.87,425.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:434.87,436.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:446.92,448.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:457.92,459.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:468.115,470.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:479.85,480.54 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:480.54,481.48 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:481.48,482.28 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:482.28,487.12 3 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:488.30,489.22 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:490.23,491.47 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:505.59,511.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:520.66,522.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:531.83,533.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:543.97,545.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:554.64,556.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:566.62,568.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:577.78,579.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:589.80,591.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:600.76,602.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:612.136,614.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:623.91,625.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/reader.go:634.71,636.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/resource.go:58.151,63.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/semigroup.go:39.41,43.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/sync.go:46.78,54.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:31.89,39.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:48.103,56.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:65.71,67.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:75.112,83.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:92.124,100.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:108.94,110.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:120.95,128.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:137.92,145.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:148.106,156.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:165.74,167.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:170.118,178.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:181.115,189.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:192.127,200.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:203.97,205.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:215.95,223.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:232.92,240.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:243.106,251.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:260.74,262.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:265.115,273.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:276.127,284.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:287.118,295.2 1 0
github.com/IBM/fp-go/v2/context/readerioeither/traverse.go:304.97,306.2 1 0

View File

@@ -1,15 +0,0 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:117.52,119.103 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:119.103,120.80 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:120.80,121.41 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:121.41,123.19 2 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:123.19,126.6 2 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:127.5,127.20 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.2,132.93 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.93,133.80 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:133.80,134.41 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:134.41,136.19 2 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:136.19,138.6 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:139.5,139.20 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:144.2,150.50 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:150.50,153.4 2 1

View File

@@ -1,11 +0,0 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:111.76,116.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:134.49,136.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:161.90,162.65 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:162.65,166.76 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:166.76,176.5 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:198.73,203.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:222.74,227.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:234.76,236.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:245.74,254.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:281.76,286.2 1 1

View File

@@ -1,720 +0,0 @@
// 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 readerioeither
import (
"context"
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/readerioeither"
)
const (
// useParallel is the feature flag to control if we use the parallel or the sequential implementation of ap
useParallel = true
)
// FromEither converts an [Either] into a [ReaderIOEither].
// The resulting computation ignores the context and immediately returns the Either value.
//
// Parameters:
// - e: The Either value to lift into ReaderIOEither
//
// Returns a ReaderIOEither that produces the given Either value.
//
//go:inline
func FromEither[A any](e Either[A]) ReaderIOEither[A] {
return readerioeither.FromEither[context.Context](e)
}
// Left creates a [ReaderIOEither] that represents a failed computation with the given error.
//
// Parameters:
// - l: The error value
//
// Returns a ReaderIOEither that always fails with the given error.
func Left[A any](l error) ReaderIOEither[A] {
return readerioeither.Left[context.Context, A](l)
}
// Right creates a [ReaderIOEither] that represents a successful computation with the given value.
//
// Parameters:
// - r: The success value
//
// Returns a ReaderIOEither that always succeeds with the given value.
//
//go:inline
func Right[A any](r A) ReaderIOEither[A] {
return readerioeither.Right[context.Context, error](r)
}
// MonadMap transforms the success value of a [ReaderIOEither] using the provided function.
// If the computation fails, the error is propagated unchanged.
//
// Parameters:
// - fa: The ReaderIOEither to transform
// - f: The transformation function
//
// Returns a new ReaderIOEither with the transformed value.
//
//go:inline
func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
return readerioeither.MonadMap(fa, f)
}
// Map transforms the success value of a [ReaderIOEither] using the provided function.
// This is the curried version of [MonadMap], useful for composition.
//
// Parameters:
// - f: The transformation function
//
// Returns a function that transforms a ReaderIOEither.
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return readerioeither.Map[context.Context, error](f)
}
// MonadMapTo replaces the success value of a [ReaderIOEither] with a constant value.
// If the computation fails, the error is propagated unchanged.
//
// Parameters:
// - fa: The ReaderIOEither to transform
// - b: The constant value to use
//
// Returns a new ReaderIOEither with the constant value.
//
//go:inline
func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
return readerioeither.MonadMapTo(fa, b)
}
// MapTo replaces the success value of a [ReaderIOEither] with a constant value.
// This is the curried version of [MonadMapTo].
//
// Parameters:
// - b: The constant value to use
//
// Returns a function that transforms a ReaderIOEither.
//
//go:inline
func MapTo[A, B any](b B) Operator[A, B] {
return readerioeither.MapTo[context.Context, error, A](b)
}
// MonadChain sequences two [ReaderIOEither] computations, where the second depends on the result of the first.
// If the first computation fails, the second is not executed.
//
// Parameters:
// - ma: The first ReaderIOEither
// - f: Function that produces the second ReaderIOEither based on the first's result
//
// Returns a new ReaderIOEither representing the sequenced computation.
//
//go:inline
func MonadChain[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[B] {
return readerioeither.MonadChain(ma, f)
}
// Chain sequences two [ReaderIOEither] computations, where the second depends on the result of the first.
// This is the curried version of [MonadChain], useful for composition.
//
// Parameters:
// - f: Function that produces the second ReaderIOEither based on the first's result
//
// Returns a function that sequences ReaderIOEither computations.
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return readerioeither.Chain(f)
}
// MonadChainFirst sequences two [ReaderIOEither] computations but returns the result of the first.
// The second computation is executed for its side effects only.
//
// Parameters:
// - ma: The first ReaderIOEither
// - f: Function that produces the second ReaderIOEither
//
// Returns a ReaderIOEither with the result of the first computation.
//
//go:inline
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[A] {
return readerioeither.MonadChainFirst(ma, f)
}
// ChainFirst sequences two [ReaderIOEither] computations but returns the result of the first.
// This is the curried version of [MonadChainFirst].
//
// Parameters:
// - f: Function that produces the second ReaderIOEither
//
// Returns a function that sequences ReaderIOEither computations.
//
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return readerioeither.ChainFirst(f)
}
// Of creates a [ReaderIOEither] that always succeeds with the given value.
// This is the same as [Right] and represents the monadic return operation.
//
// Parameters:
// - a: The value to wrap
//
// Returns a ReaderIOEither that always succeeds with the given value.
//
//go:inline
func Of[A any](a A) ReaderIOEither[A] {
return readerioeither.Of[context.Context, error](a)
}
func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOEither[A]) IOEither[A] {
return function.Pipe3(
ma,
ioeither.Swap[error, A],
ioeither.ChainFirstIOK[A](func(err error) func() any {
return io.FromImpure(func() { cancel(err) })
}),
ioeither.Swap[A, error],
)
}
// MonadApPar implements parallel applicative application for [ReaderIOEither].
// It executes both computations in parallel and creates a sub-context that will be canceled
// if either operation fails. This provides automatic cancellation propagation.
//
// Parameters:
// - fab: ReaderIOEither containing a function
// - fa: ReaderIOEither containing a value
//
// Returns a ReaderIOEither with the function applied to the value.
func MonadApPar[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
// context sensitive input
cfab := WithContext(fab)
cfa := WithContext(fa)
return func(ctx context.Context) IOEither[B] {
// quick check for cancellation
if err := context.Cause(ctx); err != nil {
return ioeither.Left[B](err)
}
return func() Either[B] {
// quick check for cancellation
if err := context.Cause(ctx); err != nil {
return either.Left[B](err)
}
// create sub-contexts for fa and fab, so they can cancel one other
ctxSub, cancelSub := context.WithCancelCause(ctx)
defer cancelSub(nil) // cancel has to be called in all paths
fabIOE := withCancelCauseFunc(cancelSub, cfab(ctxSub))
faIOE := withCancelCauseFunc(cancelSub, cfa(ctxSub))
return ioeither.MonadApPar(fabIOE, faIOE)()
}
}
}
// MonadAp implements applicative application for [ReaderIOEither].
// By default, it uses parallel execution ([MonadApPar]) but can be configured to use
// sequential execution ([MonadApSeq]) via the useParallel constant.
//
// Parameters:
// - fab: ReaderIOEither containing a function
// - fa: ReaderIOEither containing a value
//
// Returns a ReaderIOEither with the function applied to the value.
func MonadAp[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
// dispatch to the configured version
if useParallel {
return MonadApPar(fab, fa)
}
return MonadApSeq(fab, fa)
}
// MonadApSeq implements sequential applicative application for [ReaderIOEither].
// It executes the function computation first, then the value computation.
//
// Parameters:
// - fab: ReaderIOEither containing a function
// - fa: ReaderIOEither containing a value
//
// Returns a ReaderIOEither with the function applied to the value.
//
//go:inline
func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
return readerioeither.MonadApSeq(fab, fa)
}
// Ap applies a function wrapped in a [ReaderIOEither] to a value wrapped in a ReaderIOEither.
// This is the curried version of [MonadAp], using the default execution mode.
//
// Parameters:
// - fa: ReaderIOEither containing a value
//
// Returns a function that applies a ReaderIOEither function to the value.
//
//go:inline
func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadAp[B, A], fa)
}
// ApSeq applies a function wrapped in a [ReaderIOEither] to a value sequentially.
// This is the curried version of [MonadApSeq].
//
// Parameters:
// - fa: ReaderIOEither containing a value
//
// Returns a function that applies a ReaderIOEither function to the value sequentially.
//
//go:inline
func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadApSeq[B, A], fa)
}
// ApPar applies a function wrapped in a [ReaderIOEither] to a value in parallel.
// This is the curried version of [MonadApPar].
//
// Parameters:
// - fa: ReaderIOEither containing a value
//
// Returns a function that applies a ReaderIOEither function to the value in parallel.
//
//go:inline
func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadApPar[B, A], fa)
}
// FromPredicate creates a [ReaderIOEither] from a predicate function.
// If the predicate returns true, the value is wrapped in Right; otherwise, Left with the error from onFalse.
//
// Parameters:
// - pred: Predicate function to test the value
// - onFalse: Function to generate an error when predicate fails
//
// Returns a function that converts a value to ReaderIOEither based on the predicate.
//
//go:inline
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
return readerioeither.FromPredicate[context.Context](pred, onFalse)
}
// OrElse provides an alternative [ReaderIOEither] computation if the first one fails.
// The alternative is only executed if the first computation results in a Left (error).
//
// Parameters:
// - onLeft: Function that produces an alternative ReaderIOEither from the error
//
// Returns a function that provides fallback behavior for failed computations.
//
//go:inline
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
return readerioeither.OrElse[context.Context](onLeft)
}
// Ask returns a [ReaderIOEither] that provides access to the context.
// This is useful for accessing the [context.Context] within a computation.
//
// Returns a ReaderIOEither that produces the context.
//
//go:inline
func Ask() ReaderIOEither[context.Context] {
return readerioeither.Ask[context.Context, error]()
}
// MonadChainEitherK chains a function that returns an [Either] into a [ReaderIOEither] computation.
// This is useful for integrating pure Either-returning functions into ReaderIOEither workflows.
//
// Parameters:
// - ma: The ReaderIOEither to chain from
// - f: Function that produces an Either
//
// Returns a new ReaderIOEither with the chained computation.
//
//go:inline
func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[B] {
return readerioeither.MonadChainEitherK[context.Context](ma, f)
}
// ChainEitherK chains a function that returns an [Either] into a [ReaderIOEither] computation.
// This is the curried version of [MonadChainEitherK].
//
// Parameters:
// - f: Function that produces an Either
//
// Returns a function that chains the Either-returning function.
//
//go:inline
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
return readerioeither.ChainEitherK[context.Context](f)
}
// MonadChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
// The Either-returning function is executed for its validation/side effects only.
//
// Parameters:
// - ma: The ReaderIOEither to chain from
// - f: Function that produces an Either
//
// Returns a ReaderIOEither with the original value if both computations succeed.
//
//go:inline
func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[A] {
return readerioeither.MonadChainFirstEitherK[context.Context](ma, f)
}
// ChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
// This is the curried version of [MonadChainFirstEitherK].
//
// Parameters:
// - f: Function that produces an Either
//
// Returns a function that chains the Either-returning function.
//
//go:inline
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return readerioeither.ChainFirstEitherK[context.Context](f)
}
// ChainOptionK chains a function that returns an [Option] into a [ReaderIOEither] computation.
// If the Option is None, the provided error function is called.
//
// Parameters:
// - onNone: Function to generate an error when Option is None
//
// Returns a function that chains Option-returning functions into ReaderIOEither.
//
//go:inline
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
return readerioeither.ChainOptionK[context.Context, A, B](onNone)
}
// FromIOEither converts an [IOEither] into a [ReaderIOEither].
// The resulting computation ignores the context.
//
// Parameters:
// - t: The IOEither to convert
//
// Returns a ReaderIOEither that executes the IOEither.
//
//go:inline
func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
return readerioeither.FromIOEither[context.Context](t)
}
// FromIO converts an [IO] into a [ReaderIOEither].
// The IO computation always succeeds, so it's wrapped in Right.
//
// Parameters:
// - t: The IO to convert
//
// Returns a ReaderIOEither that executes the IO and wraps the result in Right.
//
//go:inline
func FromIO[A any](t IO[A]) ReaderIOEither[A] {
return readerioeither.FromIO[context.Context, error](t)
}
// FromLazy converts a [Lazy] computation into a [ReaderIOEither].
// The Lazy computation always succeeds, so it's wrapped in Right.
// This is an alias for [FromIO] since Lazy and IO have the same structure.
//
// Parameters:
// - t: The Lazy computation to convert
//
// Returns a ReaderIOEither that executes the Lazy computation and wraps the result in Right.
//
//go:inline
func FromLazy[A any](t Lazy[A]) ReaderIOEither[A] {
return readerioeither.FromIO[context.Context, error](t)
}
// Never returns a [ReaderIOEither] that blocks indefinitely until the context is canceled.
// This is useful for creating computations that wait for external cancellation signals.
//
// Returns a ReaderIOEither that waits for context cancellation and returns the cancellation error.
func Never[A any]() ReaderIOEither[A] {
return func(ctx context.Context) IOEither[A] {
return func() Either[A] {
<-ctx.Done()
return either.Left[A](context.Cause(ctx))
}
}
}
// MonadChainIOK chains a function that returns an [IO] into a [ReaderIOEither] computation.
// The IO computation always succeeds, so it's wrapped in Right.
//
// Parameters:
// - ma: The ReaderIOEither to chain from
// - f: Function that produces an IO
//
// Returns a new ReaderIOEither with the chained IO computation.
//
//go:inline
func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[B] {
return readerioeither.MonadChainIOK(ma, f)
}
// ChainIOK chains a function that returns an [IO] into a [ReaderIOEither] computation.
// This is the curried version of [MonadChainIOK].
//
// Parameters:
// - f: Function that produces an IO
//
// Returns a function that chains the IO-returning function.
//
//go:inline
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
return readerioeither.ChainIOK[context.Context, error](f)
}
// MonadChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// The IO computation is executed for its side effects only.
//
// Parameters:
// - ma: The ReaderIOEither to chain from
// - f: Function that produces an IO
//
// Returns a ReaderIOEither with the original value after executing the IO.
//
//go:inline
func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[A] {
return readerioeither.MonadChainFirstIOK(ma, f)
}
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// This is the curried version of [MonadChainFirstIOK].
//
// Parameters:
// - f: Function that produces an IO
//
// Returns a function that chains the IO-returning function.
//
//go:inline
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return readerioeither.ChainFirstIOK[context.Context, error](f)
}
// ChainIOEitherK chains a function that returns an [IOEither] into a [ReaderIOEither] computation.
// This is useful for integrating IOEither-returning functions into ReaderIOEither workflows.
//
// Parameters:
// - f: Function that produces an IOEither
//
// Returns a function that chains the IOEither-returning function.
//
//go:inline
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) Operator[A, B] {
return readerioeither.ChainIOEitherK[context.Context](f)
}
// Delay creates an operation that delays execution by the specified duration.
// The computation waits for either the delay to expire or the context to be canceled.
//
// Parameters:
// - delay: The duration to wait before executing the computation
//
// Returns a function that delays a ReaderIOEither computation.
func Delay[A any](delay time.Duration) Operator[A, A] {
return func(ma ReaderIOEither[A]) ReaderIOEither[A] {
return func(ctx context.Context) IOEither[A] {
return func() Either[A] {
// manage the timeout
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
defer cancelTimeout()
// whatever comes first
select {
case <-timeoutCtx.Done():
return ma(ctx)()
case <-ctx.Done():
return either.Left[A](context.Cause(ctx))
}
}
}
}
}
// Timer returns the current time after waiting for the specified delay.
// This is useful for creating time-based computations.
//
// Parameters:
// - delay: The duration to wait before returning the time
//
// Returns a ReaderIOEither that produces the current time after the delay.
func Timer(delay time.Duration) ReaderIOEither[time.Time] {
return function.Pipe2(
io.Now,
FromIO[time.Time],
Delay[time.Time](delay),
)
}
// Defer creates a [ReaderIOEither] by lazily generating a new computation each time it's executed.
// This is useful for creating computations that should be re-evaluated on each execution.
//
// Parameters:
// - gen: Lazy generator function that produces a ReaderIOEither
//
// Returns a ReaderIOEither that generates a fresh computation on each execution.
//
//go:inline
func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
return readerioeither.Defer(gen)
}
// TryCatch wraps a function that returns a tuple (value, error) into a [ReaderIOEither].
// This is the standard way to convert Go error-returning functions into ReaderIOEither.
//
// Parameters:
// - f: Function that takes a context and returns a function producing (value, error)
//
// Returns a ReaderIOEither that wraps the error-returning function.
//
//go:inline
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A] {
return readerioeither.TryCatch(f, errors.IdentityError)
}
// MonadAlt provides an alternative [ReaderIOEither] if the first one fails.
// The alternative is lazily evaluated only if needed.
//
// Parameters:
// - first: The primary ReaderIOEither to try
// - second: Lazy alternative ReaderIOEither to use if first fails
//
// Returns a ReaderIOEither that tries the first, then the second if first fails.
//
//go:inline
func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
return readerioeither.MonadAlt(first, second)
}
// Alt provides an alternative [ReaderIOEither] if the first one fails.
// This is the curried version of [MonadAlt].
//
// Parameters:
// - second: Lazy alternative ReaderIOEither to use if first fails
//
// Returns a function that provides fallback behavior.
//
//go:inline
func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
return readerioeither.Alt(second)
}
// Memoize computes the value of the provided [ReaderIOEither] monad lazily but exactly once.
// The context used to compute the value is the context of the first call, so do not use this
// method if the value has a functional dependency on the content of the context.
//
// Parameters:
// - rdr: The ReaderIOEither to memoize
//
// Returns a ReaderIOEither that caches its result after the first execution.
//
//go:inline
func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
return readerioeither.Memoize(rdr)
}
// Flatten converts a nested [ReaderIOEither] into a flat [ReaderIOEither].
// This is equivalent to [MonadChain] with the identity function.
//
// Parameters:
// - rdr: The nested ReaderIOEither to flatten
//
// Returns a flattened ReaderIOEither.
//
//go:inline
func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
return readerioeither.Flatten(rdr)
}
// MonadFlap applies a value to a function wrapped in a [ReaderIOEither].
// This is the reverse of [MonadAp], useful in certain composition scenarios.
//
// Parameters:
// - fab: ReaderIOEither containing a function
// - a: The value to apply to the function
//
// Returns a ReaderIOEither with the function applied to the value.
//
//go:inline
func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
return readerioeither.MonadFlap(fab, a)
}
// Flap applies a value to a function wrapped in a [ReaderIOEither].
// This is the curried version of [MonadFlap].
//
// Parameters:
// - a: The value to apply to the function
//
// Returns a function that applies the value to a ReaderIOEither function.
//
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return readerioeither.Flap[context.Context, error, B](a)
}
// Fold handles both success and error cases of a [ReaderIOEither] by providing handlers for each.
// Both handlers return ReaderIOEither, allowing for further composition.
//
// Parameters:
// - onLeft: Handler for error case
// - onRight: Handler for success case
//
// Returns a function that folds a ReaderIOEither into a new ReaderIOEither.
//
//go:inline
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
return readerioeither.Fold(onLeft, onRight)
}
// GetOrElse extracts the value from a [ReaderIOEither], providing a default via a function if it fails.
// The result is a [ReaderIO] that always succeeds.
//
// Parameters:
// - onLeft: Function to provide a default value from the error
//
// Returns a function that converts a ReaderIOEither to a ReaderIO.
//
//go:inline
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) ReaderIO[A] {
return readerioeither.GetOrElse(onLeft)
}
// OrLeft transforms the error of a [ReaderIOEither] using the provided function.
// The success value is left unchanged.
//
// Parameters:
// - onLeft: Function to transform the error
//
// Returns a function that transforms the error of a ReaderIOEither.
//
//go:inline
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
return readerioeither.OrLeft[A](onLeft)
}

View File

@@ -1,532 +0,0 @@
// 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 readerioeither
import (
"context"
"errors"
"fmt"
"testing"
"time"
E "github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestFromEither(t *testing.T) {
ctx := t.Context()
// Test with Right
rightVal := E.Right[error](42)
result := FromEither(rightVal)(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with Left
err := errors.New("test error")
leftVal := E.Left[int](err)
result = FromEither(leftVal)(ctx)()
assert.Equal(t, E.Left[int](err), result)
}
func TestLeftRight(t *testing.T) {
ctx := t.Context()
// Test Left
err := errors.New("test error")
result := Left[int](err)(ctx)()
assert.True(t, E.IsLeft(result))
// Test Right
result = Right(42)(ctx)()
assert.True(t, E.IsRight(result))
val, _ := E.Unwrap(result)
assert.Equal(t, 42, val)
}
func TestOf(t *testing.T) {
ctx := t.Context()
result := Of(42)(ctx)()
assert.Equal(t, E.Right[error](42), result)
}
func TestMonadMap(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadMap(Right(42), func(x int) int { return x * 2 })(ctx)()
assert.Equal(t, E.Right[error](84), result)
// Test with Left
err := errors.New("test error")
result = MonadMap(Left[int](err), func(x int) int { return x * 2 })(ctx)()
assert.Equal(t, E.Left[int](err), result)
}
func TestMonadMapTo(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadMapTo(Right(42), "hello")(ctx)()
assert.Equal(t, E.Right[error]("hello"), result)
// Test with Left
err := errors.New("test error")
result = MonadMapTo(Left[int](err), "hello")(ctx)()
assert.Equal(t, E.Left[string](err), result)
}
func TestMonadChain(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadChain(Right(42), func(x int) ReaderIOEither[int] {
return Right(x * 2)
})(ctx)()
assert.Equal(t, E.Right[error](84), result)
// Test with Left
err := errors.New("test error")
result = MonadChain(Left[int](err), func(x int) ReaderIOEither[int] {
return Right(x * 2)
})(ctx)()
assert.Equal(t, E.Left[int](err), result)
// Test where function returns Left
result = MonadChain(Right(42), func(x int) ReaderIOEither[int] {
return Left[int](errors.New("chain error"))
})(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestMonadChainFirst(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadChainFirst(Right(42), func(x int) ReaderIOEither[string] {
return Right("ignored")
})(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with Left in first
err := errors.New("test error")
result = MonadChainFirst(Left[int](err), func(x int) ReaderIOEither[string] {
return Right("ignored")
})(ctx)()
assert.Equal(t, E.Left[int](err), result)
// Test with Left in second
result = MonadChainFirst(Right(42), func(x int) ReaderIOEither[string] {
return Left[string](errors.New("chain error"))
})(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestMonadApSeq(t *testing.T) {
ctx := t.Context()
// Test with both Right
fct := Right(func(x int) int { return x * 2 })
val := Right(42)
result := MonadApSeq(fct, val)(ctx)()
assert.Equal(t, E.Right[error](84), result)
// Test with Left function
err := errors.New("function error")
fct = Left[func(int) int](err)
result = MonadApSeq(fct, val)(ctx)()
assert.Equal(t, E.Left[int](err), result)
// Test with Left value
fct = Right(func(x int) int { return x * 2 })
err = errors.New("value error")
val = Left[int](err)
result = MonadApSeq(fct, val)(ctx)()
assert.Equal(t, E.Left[int](err), result)
}
func TestMonadApPar(t *testing.T) {
ctx := t.Context()
// Test with both Right
fct := Right(func(x int) int { return x * 2 })
val := Right(42)
result := MonadApPar(fct, val)(ctx)()
assert.Equal(t, E.Right[error](84), result)
}
func TestFromPredicate(t *testing.T) {
ctx := t.Context()
pred := func(x int) bool { return x > 0 }
onFalse := func(x int) error { return fmt.Errorf("value %d is not positive", x) }
// Test with predicate true
result := FromPredicate(pred, onFalse)(42)(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with predicate false
result = FromPredicate(pred, onFalse)(-1)(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestAsk(t *testing.T) {
ctx := context.WithValue(t.Context(), "key", "value")
result := Ask()(ctx)()
assert.True(t, E.IsRight(result))
retrievedCtx, _ := E.Unwrap(result)
assert.Equal(t, "value", retrievedCtx.Value("key"))
}
func TestMonadChainEitherK(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadChainEitherK(Right(42), func(x int) E.Either[error, int] {
return E.Right[error](x * 2)
})(ctx)()
assert.Equal(t, E.Right[error](84), result)
// Test with Left in Either
result = MonadChainEitherK(Right(42), func(x int) E.Either[error, int] {
return E.Left[int](errors.New("either error"))
})(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestMonadChainFirstEitherK(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadChainFirstEitherK(Right(42), func(x int) E.Either[error, string] {
return E.Right[error]("ignored")
})(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with Left in Either
result = MonadChainFirstEitherK(Right(42), func(x int) E.Either[error, string] {
return E.Left[string](errors.New("either error"))
})(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestChainOptionKFunc(t *testing.T) {
ctx := t.Context()
onNone := func() error { return errors.New("none error") }
// Test with Some
chainFunc := ChainOptionK[int, int](onNone)
result := chainFunc(func(x int) O.Option[int] {
return O.Some(x * 2)
})(Right(42))(ctx)()
assert.Equal(t, E.Right[error](84), result)
// Test with None
result = chainFunc(func(x int) O.Option[int] {
return O.None[int]()
})(Right(42))(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestFromIOEither(t *testing.T) {
ctx := t.Context()
// Test with Right
ioe := func() E.Either[error, int] {
return E.Right[error](42)
}
result := FromIOEither(ioe)(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with Left
err := errors.New("test error")
ioe = func() E.Either[error, int] {
return E.Left[int](err)
}
result = FromIOEither(ioe)(ctx)()
assert.Equal(t, E.Left[int](err), result)
}
func TestFromIO(t *testing.T) {
ctx := t.Context()
io := func() int { return 42 }
result := FromIO(io)(ctx)()
assert.Equal(t, E.Right[error](42), result)
}
func TestFromLazy(t *testing.T) {
ctx := t.Context()
lazy := func() int { return 42 }
result := FromLazy(lazy)(ctx)()
assert.Equal(t, E.Right[error](42), result)
}
func TestNeverWithCancel(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
// Start Never in a goroutine
done := make(chan E.Either[error, int])
go func() {
done <- Never[int]()(ctx)()
}()
// Cancel the context
cancel()
// Should receive cancellation error
result := <-done
assert.True(t, E.IsLeft(result))
}
func TestMonadChainIOK(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadChainIOK(Right(42), func(x int) func() int {
return func() int { return x * 2 }
})(ctx)()
assert.Equal(t, E.Right[error](84), result)
}
func TestMonadChainFirstIOK(t *testing.T) {
ctx := t.Context()
// Test with Right
result := MonadChainFirstIOK(Right(42), func(x int) func() string {
return func() string { return "ignored" }
})(ctx)()
assert.Equal(t, E.Right[error](42), result)
}
func TestDelayFunc(t *testing.T) {
ctx := t.Context()
delay := 100 * time.Millisecond
start := time.Now()
delayFunc := Delay[int](delay)
result := delayFunc(Right(42))(ctx)()
elapsed := time.Since(start)
assert.True(t, E.IsRight(result))
assert.GreaterOrEqual(t, elapsed, delay)
}
func TestDefer(t *testing.T) {
ctx := t.Context()
count := 0
gen := func() ReaderIOEither[int] {
count++
return Right(count)
}
deferred := Defer(gen)
// First call
result1 := deferred(ctx)()
assert.Equal(t, E.Right[error](1), result1)
// Second call should generate new value
result2 := deferred(ctx)()
assert.Equal(t, E.Right[error](2), result2)
}
func TestTryCatch(t *testing.T) {
ctx := t.Context()
// Test success
result := TryCatch(func(ctx context.Context) func() (int, error) {
return func() (int, error) {
return 42, nil
}
})(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test error
err := errors.New("test error")
result = TryCatch(func(ctx context.Context) func() (int, error) {
return func() (int, error) {
return 0, err
}
})(ctx)()
assert.Equal(t, E.Left[int](err), result)
}
func TestMonadAlt(t *testing.T) {
ctx := t.Context()
// Test with Right (alternative not called)
result := MonadAlt(Right(42), func() ReaderIOEither[int] {
return Right(99)
})(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with Left (alternative called)
err := errors.New("test error")
result = MonadAlt(Left[int](err), func() ReaderIOEither[int] {
return Right(99)
})(ctx)()
assert.Equal(t, E.Right[error](99), result)
}
func TestMemoize(t *testing.T) {
ctx := t.Context()
count := 0
rdr := Memoize(FromLazy(func() int {
count++
return count
}))
// First call
result1 := rdr(ctx)()
assert.Equal(t, E.Right[error](1), result1)
// Second call should return memoized value
result2 := rdr(ctx)()
assert.Equal(t, E.Right[error](1), result2)
}
func TestFlatten(t *testing.T) {
ctx := t.Context()
nested := Right(Right(42))
result := Flatten(nested)(ctx)()
assert.Equal(t, E.Right[error](42), result)
}
func TestMonadFlap(t *testing.T) {
ctx := t.Context()
fab := Right(func(x int) int { return x * 2 })
result := MonadFlap(fab, 42)(ctx)()
assert.Equal(t, E.Right[error](84), result)
}
func TestWithContext(t *testing.T) {
// Test with non-canceled context
ctx := t.Context()
result := WithContext(Right(42))(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with canceled context
ctx, cancel := context.WithCancel(t.Context())
cancel()
result = WithContext(Right(42))(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestMonadAp(t *testing.T) {
ctx := t.Context()
// Test with both Right
fct := Right(func(x int) int { return x * 2 })
val := Right(42)
result := MonadAp(fct, val)(ctx)()
assert.Equal(t, E.Right[error](84), result)
}
// Test traverse functions
func TestSequenceArray(t *testing.T) {
ctx := t.Context()
// Test with all Right
arr := []ReaderIOEither[int]{Right(1), Right(2), Right(3)}
result := SequenceArray(arr)(ctx)()
assert.True(t, E.IsRight(result))
vals, _ := E.Unwrap(result)
assert.Equal(t, []int{1, 2, 3}, vals)
// Test with one Left
err := errors.New("test error")
arr = []ReaderIOEither[int]{Right(1), Left[int](err), Right(3)}
result = SequenceArray(arr)(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestTraverseArray(t *testing.T) {
ctx := t.Context()
// Test transformation
arr := []int{1, 2, 3}
result := TraverseArray(func(x int) ReaderIOEither[int] {
return Right(x * 2)
})(arr)(ctx)()
assert.True(t, E.IsRight(result))
vals, _ := E.Unwrap(result)
assert.Equal(t, []int{2, 4, 6}, vals)
}
func TestSequenceRecord(t *testing.T) {
ctx := t.Context()
// Test with all Right
rec := map[string]ReaderIOEither[int]{
"a": Right(1),
"b": Right(2),
}
result := SequenceRecord(rec)(ctx)()
assert.True(t, E.IsRight(result))
vals, _ := E.Unwrap(result)
assert.Equal(t, 1, vals["a"])
assert.Equal(t, 2, vals["b"])
}
func TestTraverseRecord(t *testing.T) {
ctx := t.Context()
// Test transformation
rec := map[string]int{"a": 1, "b": 2}
result := TraverseRecord[string](func(x int) ReaderIOEither[int] {
return Right(x * 2)
})(rec)(ctx)()
assert.True(t, E.IsRight(result))
vals, _ := E.Unwrap(result)
assert.Equal(t, 2, vals["a"])
assert.Equal(t, 4, vals["b"])
}
// Test monoid functions
func TestAltSemigroup(t *testing.T) {
ctx := t.Context()
sg := AltSemigroup[int]()
// Test with Right (first succeeds)
result := sg.Concat(Right(42), Right(99))(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with Left then Right (fallback)
err := errors.New("test error")
result = sg.Concat(Left[int](err), Right(99))(ctx)()
assert.Equal(t, E.Right[error](99), result)
}
// Test Do notation
func TestDo(t *testing.T) {
ctx := t.Context()
type State struct {
Value int
}
result := Do(State{Value: 42})(ctx)()
assert.True(t, E.IsRight(result))
state, _ := E.Unwrap(result)
assert.Equal(t, 42, state.Value)
}

View File

@@ -1,63 +0,0 @@
// 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 readerioeither
import (
"context"
"github.com/IBM/fp-go/v2/function"
RIE "github.com/IBM/fp-go/v2/readerioeither"
)
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource.
// This implements the RAII (Resource Acquisition Is Initialization) pattern, ensuring that resources are
// properly released even if the operation fails or the context is canceled.
//
// The resource is created, used, and released in a safe manner:
// - onCreate: Creates the resource
// - The provided function uses the resource
// - onRelease: Releases the resource (always called, even on error)
//
// Parameters:
// - onCreate: ReaderIOEither that creates the resource
// - onRelease: Function to release the resource
//
// Returns a function that takes a resource-using function and returns a ReaderIOEither.
//
// Example:
//
// file := WithResource(
// openFile("data.txt"),
// func(f *os.File) ReaderIOEither[any] {
// return TryCatch(func(ctx context.Context) func() (any, error) {
// return func() (any, error) { return nil, f.Close() }
// })
// },
// )
// result := file(func(f *os.File) ReaderIOEither[string] {
// return TryCatch(func(ctx context.Context) func() (string, error) {
// return func() (string, error) {
// data, err := io.ReadAll(f)
// return string(data), err
// }
// })
// })
func WithResource[A, R, ANY any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[ANY]) Kleisli[Kleisli[R, A], A] {
return function.Flow2(
function.Bind2nd(function.Flow2[func(R) ReaderIOEither[A], Operator[A, A], R, ReaderIOEither[A], ReaderIOEither[A]], WithContext[A]),
RIE.WithResource[A, context.Context, error, R](WithContext(onCreate), onRelease),
)
}

View File

@@ -1,4 +1,4 @@
# ReaderIOEither Benchmarks
# ReaderIOResult Benchmarks
This document describes the benchmark suite for the `context/readerioeither` package and how to interpret the results to identify performance bottlenecks.
@@ -35,8 +35,8 @@ go test -bench=. -benchmem -benchtime=100000x
- Construction is very fast, suitable for hot paths
### 2. Conversion Operations
- `BenchmarkFromEither_Right/Left` - Converting Either to ReaderIOEither (~70ns, 2 allocs)
- `BenchmarkFromIO` - Converting IO to ReaderIOEither (~78ns, 3 allocs)
- `BenchmarkFromEither_Right/Left` - Converting Either to ReaderIOResult (~70ns, 2 allocs)
- `BenchmarkFromIO` - Converting IO to ReaderIOResult (~78ns, 3 allocs)
- `BenchmarkFromIOEither_Right/Left` - Converting IOEither (~23ns, 1 alloc)
**Key Insights:**

View File

@@ -0,0 +1,504 @@
# Sequence Functions and Point-Free Style Programming
This document explains how the `Sequence*` functions in the `context/readerioresult` package enable point-free style programming and improve code composition.
## Table of Contents
1. [What is Point-Free Style?](#what-is-point-free-style)
2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
5. [Practical Benefits](#practical-benefits)
6. [Examples](#examples)
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
## What is Point-Free Style?
Point-free style (also called tacit programming) is a programming paradigm where function definitions don't explicitly mention their arguments. Instead, functions are composed using combinators and higher-order functions.
**Traditional style (with points):**
```go
func double(x int) int {
return x * 2
}
```
**Point-free style (without points):**
```go
var double = F.Flow2(
N.Mul(2),
identity,
)
```
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
## The Problem: Nested Function Application
In functional programming with monadic types like `ReaderIOResult`, we often have nested structures where we need to apply parameters in a specific order. Consider:
```go
type ReaderIOResult[A any] = func(context.Context) func() Either[error, A]
type Reader[R, A any] = func(R) A
// A computation that produces a Reader
type Computation = ReaderIOResult[Reader[Config, int]]
// Expands to: func(context.Context) func() Either[error, func(Config) int]
```
To use this, we must apply parameters in this order:
1. First, provide `context.Context`
2. Then, execute the IO effect (call the function)
3. Then, unwrap the `Either` to get the `Reader`
4. Finally, provide the `Config`
This creates several problems:
### Problem 1: Awkward Parameter Order
```go
computation := getComputation()
ctx := context.Background()
cfg := Config{Value: 42}
// Must apply in this specific order
result := computation(ctx)() // Get Either[error, Reader[Config, int]]
if reader, err := either.Unwrap(result); err == nil {
value := reader(cfg) // Finally apply Config
// use value
}
```
The `Config` parameter, which is often known early and stable, must be provided last. This prevents partial application and reuse.
### Problem 2: Cannot Partially Apply Dependencies
```go
// Want to do this: create a reusable computation with Config baked in
// But can't because Config comes last!
withConfig := computation(cfg) // ❌ Doesn't work - cfg comes last, not first
```
### Problem 3: Breaks Point-Free Composition
```go
// Want to compose like this:
var pipeline = F.Flow3(
getComputation,
applyConfig(cfg), // ❌ Can't do this - Config comes last
processResult,
)
```
## The Solution: Sequence Functions
The `Sequence*` functions solve this by "flipping" or "sequencing" the nested structure, changing the order in which parameters are applied.
### SequenceReader
```go
func SequenceReader[R, A any](
ma ReaderIOResult[Reader[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]]
```
**Type transformation:**
```
From: func(context.Context) func() Either[error, func(R) A]
To: func(R) func(context.Context) func() Either[error, A]
```
Now `R` (the Reader's environment) comes **first**, before `context.Context`!
### SequenceReaderIO
```go
func SequenceReaderIO[R, A any](
ma ReaderIOResult[ReaderIO[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]]
```
**Type transformation:**
```
From: func(context.Context) func() Either[error, func(R) func() A]
To: func(R) func(context.Context) func() Either[error, A]
```
### SequenceReaderResult
```go
func SequenceReaderResult[R, A any](
ma ReaderIOResult[ReaderResult[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]]
```
**Type transformation:**
```
From: func(context.Context) func() Either[error, func(R) Either[error, A]]
To: func(R) func(context.Context) func() Either[error, A]
```
## How Sequence Enables Point-Free Style
### 1. Partial Application
By moving the environment parameter first, we can partially apply it:
```go
type Config struct { Multiplier int }
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
sequenced := SequenceReader[Config, int](computation)
// Partially apply Config
cfg := Config{Multiplier: 5}
withConfig := sequenced(cfg) // ✅ Now we have ReaderIOResult[int]
// Reuse with different contexts
result1 := withConfig(ctx1)()
result2 := withConfig(ctx2)()
```
### 2. Dependency Injection
Inject dependencies early in the pipeline:
```go
type Database struct { ConnectionString string }
makeQuery := func(ctx context.Context) func() Either[error, func(Database) string] {
// ... implementation
}
// Sequence to enable DI
queryWithDB := SequenceReader[Database, string](makeQuery)
// Inject database
db := Database{ConnectionString: "localhost:5432"}
query := queryWithDB(db) // ✅ Database injected
// Use query with any context
result := query(context.Background())()
```
### 3. Point-Free Composition
Build pipelines without mentioning intermediate values:
```go
var pipeline = F.Flow3(
getComputation, // ReaderIOResult[Reader[Config, int]]
SequenceReader[Config, int], // func(Config) ReaderIOResult[int]
applyConfig(cfg), // ReaderIOResult[int]
)
// Or with partial application:
var withConfig = F.Pipe1(
getComputation(),
SequenceReader[Config, int],
)
result := withConfig(cfg)(ctx)()
```
### 4. Reusable Computations
Create specialized versions of generic computations:
```go
// Generic computation
makeServiceInfo := func(ctx context.Context) func() Either[error, func(ServiceConfig) string] {
// ... implementation
}
sequenced := SequenceReader[ServiceConfig, string](makeServiceInfo)
// Create specialized versions
authService := sequenced(ServiceConfig{Name: "Auth", Version: "1.0"})
userService := sequenced(ServiceConfig{Name: "User", Version: "2.0"})
// Reuse across contexts
authInfo := authService(ctx)()
userInfo := userService(ctx)()
```
## Practical Benefits
### 1. **Improved Testability**
Inject test dependencies easily:
```go
// Production
prodDB := Database{ConnectionString: "prod:5432"}
prodQuery := queryWithDB(prodDB)
// Testing
testDB := Database{ConnectionString: "test:5432"}
testQuery := queryWithDB(testDB)
// Same computation, different dependencies
```
### 2. **Better Separation of Concerns**
Separate configuration from execution:
```go
// Configuration phase (pure, no effects)
cfg := loadConfig()
computation := sequenced(cfg)
// Execution phase (with effects)
result := computation(ctx)()
```
### 3. **Enhanced Composability**
Build complex pipelines from simple pieces:
```go
var processUser = F.Flow4(
loadUserConfig, // ReaderIOResult[Reader[Database, User]]
SequenceReader, // func(Database) ReaderIOResult[User]
applyDatabase(db), // ReaderIOResult[User]
Chain(validateUser), // ReaderIOResult[ValidatedUser]
)
```
### 4. **Reduced Boilerplate**
No need to manually thread parameters:
```go
// Without Sequence - manual threading
func processWithConfig(cfg Config) ReaderIOResult[Result] {
return func(ctx context.Context) func() Either[error, Result] {
return func() Either[error, Result] {
comp := getComputation()(ctx)()
if reader, err := either.Unwrap(comp); err == nil {
value := reader(cfg)
// ... more processing
}
// ... error handling
}
}
}
// With Sequence - point-free
var processWithConfig = F.Flow2(
getComputation,
SequenceReader[Config, Result],
)
```
## Examples
### Example 1: Database Query with Configuration
```go
type QueryConfig struct {
Timeout time.Duration
MaxRows int
}
type Database struct {
ConnectionString string
}
// Without Sequence
func executeQueryOld(cfg QueryConfig, db Database) ReaderIOResult[[]Row] {
return func(ctx context.Context) func() Either[error, []Row] {
return func() Either[error, []Row] {
// Must manually handle all parameters
// ...
}
}
}
// With Sequence
func makeQuery(ctx context.Context) func() Either[error, func(Database) []Row] {
return func() Either[error, func(Database) []Row] {
return Right[error](func(db Database) []Row {
// Implementation
return []Row{}
})
}
}
var executeQuery = F.Flow2(
makeQuery,
SequenceReader[Database, []Row],
)
// Usage
db := Database{ConnectionString: "localhost:5432"}
query := executeQuery(db)
result := query(ctx)()
```
### Example 2: Multi-Service Architecture
```go
type ServiceRegistry struct {
AuthService AuthService
UserService UserService
EmailService EmailService
}
// Create computations that depend on services
makeAuthCheck := func(ctx context.Context) func() Either[error, func(ServiceRegistry) bool] {
// ... implementation
}
makeSendEmail := func(ctx context.Context) func() Either[error, func(ServiceRegistry) error] {
// ... implementation
}
// Sequence them
authCheck := SequenceReader[ServiceRegistry, bool](makeAuthCheck)
sendEmail := SequenceReader[ServiceRegistry, error](makeSendEmail)
// Inject services once
registry := ServiceRegistry{ /* ... */ }
checkAuth := authCheck(registry)
sendMail := sendEmail(registry)
// Use with different contexts
if isAuth, _ := either.Unwrap(checkAuth(ctx1)()); isAuth {
sendMail(ctx2)()
}
```
### Example 3: Configuration-Driven Pipeline
```go
type PipelineConfig struct {
Stage1Config Stage1Config
Stage2Config Stage2Config
Stage3Config Stage3Config
}
// Define stages
stage1 := SequenceReader[Stage1Config, IntermediateResult1](makeStage1)
stage2 := SequenceReader[Stage2Config, IntermediateResult2](makeStage2)
stage3 := SequenceReader[Stage3Config, FinalResult](makeStage3)
// Build pipeline with configuration
func buildPipeline(cfg PipelineConfig) ReaderIOResult[FinalResult] {
return F.Pipe3(
stage1(cfg.Stage1Config),
Chain(func(r1 IntermediateResult1) ReaderIOResult[IntermediateResult2] {
return stage2(cfg.Stage2Config)
}),
Chain(func(r2 IntermediateResult2) ReaderIOResult[FinalResult] {
return stage3(cfg.Stage3Config)
}),
)
}
// Execute pipeline
cfg := loadPipelineConfig()
pipeline := buildPipeline(cfg)
result := pipeline(ctx)()
```
## Comparison: With and Without Sequence
### Without Sequence (Imperative Style)
```go
func processUser(userID string) ReaderIOResult[ProcessedUser] {
return func(ctx context.Context) func() Either[error, ProcessedUser] {
return func() Either[error, ProcessedUser] {
// Get database
dbComp := getDatabase()(ctx)()
if dbReader, err := either.Unwrap(dbComp); err != nil {
return Left[ProcessedUser](err)
}
db := dbReader(dbConfig)
// Get user
userComp := getUser(userID)(ctx)()
if userReader, err := either.Unwrap(userComp); err != nil {
return Left[ProcessedUser](err)
}
user := userReader(db)
// Process user
processComp := processUserData(user)(ctx)()
if processReader, err := either.Unwrap(processComp); err != nil {
return Left[ProcessedUser](err)
}
result := processReader(processingConfig)
return Right[error](result)
}
}
}
```
### With Sequence (Point-Free Style)
```go
var processUser = func(userID string) ReaderIOResult[ProcessedUser] {
return F.Pipe3(
getDatabase,
SequenceReader[DatabaseConfig, Database],
applyConfig(dbConfig),
Chain(func(db Database) ReaderIOResult[User] {
return F.Pipe2(
getUser(userID),
SequenceReader[Database, User],
applyDB(db),
)
}),
Chain(func(user User) ReaderIOResult[ProcessedUser] {
return F.Pipe2(
processUserData(user),
SequenceReader[ProcessingConfig, ProcessedUser],
applyConfig(processingConfig),
)
}),
)
}
```
## Key Takeaways
1. **Sequence functions flip parameter order** to enable partial application
2. **Dependencies come first**, making them easy to inject and test
3. **Point-free style** becomes natural and readable
4. **Composition** is enhanced through proper parameter ordering
5. **Reusability** increases as computations can be specialized early
6. **Testability** improves through easy dependency injection
7. **Separation of concerns** is clearer (configuration vs. execution)
## When to Use Sequence
Use `Sequence*` functions when:
- ✅ You want to partially apply environment/configuration parameters
- ✅ You're building reusable computations with injected dependencies
- ✅ You need to test with different dependency implementations
- ✅ You're composing complex pipelines in point-free style
- ✅ You want to separate configuration from execution
- ✅ You're working with nested Reader-like structures
Don't use `Sequence*` when:
- ❌ The original parameter order is already optimal
- ❌ You're not doing any composition or partial application
- ❌ The added abstraction doesn't provide value
- ❌ The code is simpler without it
## Conclusion
The `Sequence*` functions are powerful tools for enabling point-free style programming in Go. By flipping the parameter order of nested monadic structures, they make it easy to:
- Partially apply dependencies
- Build composable pipelines
- Improve testability
- Write more declarative code
While they add a layer of abstraction, the benefits in terms of code reusability, testability, and composability make them invaluable for functional programming in Go.

View File

@@ -0,0 +1,724 @@
// 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 readerioresult
import (
"context"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// result := readerioeither.Do(State{})
//
//go:inline
func Do[S any](
empty S,
) ReaderIOResult[S] {
return RIOR.Of[context.Context](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the context.Context from the environment.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerioeither.ReaderIOResult[User] {
// return func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// },
// ),
// readerioeither.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) readerioeither.ReaderIOResult[Config] {
// // This can access s.User from the previous step
// return func(ctx context.Context) ioeither.IOEither[error, Config] {
// return ioeither.TryCatch(func() (Config, error) {
// return fetchConfigForUser(ctx, s.User.ID)
// })
// }
// },
// ),
// )
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return RIOR.Bind(setter, f)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[S1, S2] {
return RIOR.Let[context.Context](setter, f)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[S1, S2] {
return RIOR.LetTo[context.Context](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return RIOR.BindTo[context.Context](setter)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other.
//
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
// and can conceptually run in parallel.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// // These operations are independent and can be combined with ApS
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// getConfig := func(ctx context.Context) ioeither.IOEither[error, Config] {
// return ioeither.TryCatch(func() (Config, error) {
// return fetchConfig(ctx)
// })
// }
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.ApS(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// readerioeither.ApS(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// )
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIOResult[T],
) Operator[S1, S2] {
return apply.ApS(
Ap,
Map,
setter,
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// result := F.Pipe2(
// readerioeither.Of(State{}),
// readerioeither.ApSL(userLens, getUser),
// )
//
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
fa ReaderIOResult[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a ReaderIOResult computation that produces an updated value.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.BindL(userLens, func(user User) readerioeither.ReaderIOResult[User] {
// return func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// }),
// )
//
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return RIOR.BindL(lens, f)
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a ReaderIOResult).
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do(State{User: User{Name: "Alice"}}),
// readerioeither.LetL(userLens, func(user User) User {
// user.Name = "Bob"
// return user
// }),
// )
//
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[S, S] {
return RIOR.LetL[context.Context](lens, f)
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// newUser := User{Name: "Bob", ID: 123}
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.LetToL(userLens, newUser),
// )
//
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return RIOR.LetToL[context.Context](lens, b)
}
// BindIOEitherK is a variant of Bind that works with IOEither computations.
// It lifts an IOEither Kleisli arrow into the ReaderIOResult context (with context.Context as environment).
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - f: An IOEither Kleisli arrow (S1 -> IOEither[error, T])
//
//go:inline
func BindIOEitherK[S1, S2, T any](
setter func(T) func(S1) S2,
f ioresult.Kleisli[S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromIOEither[T]))
}
// BindIOResultK is a variant of Bind that works with IOResult computations.
// This is an alias for BindIOEitherK for consistency with the Result naming convention.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - f: An IOResult Kleisli arrow (S1 -> IOResult[T])
//
//go:inline
func BindIOResultK[S1, S2, T any](
setter func(T) func(S1) S2,
f ioresult.Kleisli[S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromIOResult[T]))
}
// BindIOK is a variant of Bind that works with IO computations.
// It lifts an IO Kleisli arrow into the ReaderIOResult context.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - f: An IO Kleisli arrow (S1 -> IO[T])
//
//go:inline
func BindIOK[S1, S2, T any](
setter func(T) func(S1) S2,
f io.Kleisli[S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromIO[T]))
}
// BindReaderK is a variant of Bind that works with Reader computations.
// It lifts a Reader Kleisli arrow (with context.Context) into the ReaderIOResult context.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - f: A Reader Kleisli arrow (S1 -> Reader[context.Context, T])
//
//go:inline
func BindReaderK[S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[context.Context, S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromReader[T]))
}
// BindReaderIOK is a variant of Bind that works with ReaderIO computations.
// It lifts a ReaderIO Kleisli arrow (with context.Context) into the ReaderIOResult context.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - f: A ReaderIO Kleisli arrow (S1 -> ReaderIO[context.Context, T])
//
//go:inline
func BindReaderIOK[S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[context.Context, S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromReaderIO[T]))
}
// BindEitherK is a variant of Bind that works with Either (Result) computations.
// It lifts an Either Kleisli arrow into the ReaderIOResult context.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - f: An Either Kleisli arrow (S1 -> Either[error, T])
//
//go:inline
func BindEitherK[S1, S2, T any](
setter func(T) func(S1) S2,
f result.Kleisli[S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromEither[T]))
}
// BindResultK is a variant of Bind that works with Result computations.
// This is an alias for BindEitherK for consistency with the Result naming convention.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - f: A Result Kleisli arrow (S1 -> Result[T])
//
//go:inline
func BindResultK[S1, S2, T any](
setter func(T) func(S1) S2,
f result.Kleisli[S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromResult[T]))
}
// BindIOEitherKL is a lens-based variant of BindIOEitherK.
// It combines a lens with an IOEither Kleisli arrow, focusing on a specific field
// within the state structure.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - f: An IOEither Kleisli arrow (T -> IOEither[error, T])
//
//go:inline
func BindIOEitherKL[S, T any](
lens L.Lens[S, T],
f ioresult.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[T]))
}
// BindIOResultKL is a lens-based variant of BindIOResultK.
// This is an alias for BindIOEitherKL for consistency with the Result naming convention.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - f: An IOResult Kleisli arrow (T -> IOResult[T])
//
//go:inline
func BindIOResultKL[S, T any](
lens L.Lens[S, T],
f ioresult.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[T]))
}
// BindIOKL is a lens-based variant of BindIOK.
// It combines a lens with an IO Kleisli arrow, focusing on a specific field
// within the state structure.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - f: An IO Kleisli arrow (T -> IO[T])
//
//go:inline
func BindIOKL[S, T any](
lens L.Lens[S, T],
f io.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIO[T]))
}
// BindReaderKL is a lens-based variant of BindReaderK.
// It combines a lens with a Reader Kleisli arrow (with context.Context), focusing on a specific field
// within the state structure.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - f: A Reader Kleisli arrow (T -> Reader[context.Context, T])
//
//go:inline
func BindReaderKL[S, T any](
lens L.Lens[S, T],
f reader.Kleisli[context.Context, T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromReader[T]))
}
// BindReaderIOKL is a lens-based variant of BindReaderIOK.
// It combines a lens with a ReaderIO Kleisli arrow (with context.Context), focusing on a specific field
// within the state structure.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - f: A ReaderIO Kleisli arrow (T -> ReaderIO[context.Context, T])
//
//go:inline
func BindReaderIOKL[S, T any](
lens L.Lens[S, T],
f readerio.Kleisli[context.Context, T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
}
// ApIOEitherS is an applicative variant that works with IOEither values.
// Unlike BindIOEitherK, this uses applicative composition (ApS) instead of monadic
// composition (Bind), allowing independent computations to be combined.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - fa: An IOEither value
//
//go:inline
func ApIOEitherS[S1, S2, T any](
setter func(T) func(S1) S2,
fa IOResult[T],
) Operator[S1, S2] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S1], ioresult.Operator[S1, S2]], ioeither.ApS(setter, fa))
}
// ApIOResultS is an applicative variant that works with IOResult values.
// This is an alias for ApIOEitherS for consistency with the Result naming convention.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - fa: An IOResult value
//
//go:inline
func ApIOResultS[S1, S2, T any](
setter func(T) func(S1) S2,
fa IOResult[T],
) Operator[S1, S2] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S1], ioresult.Operator[S1, S2]], ioeither.ApS(setter, fa))
}
// ApIOS is an applicative variant that works with IO values.
// It lifts an IO value into the ReaderIOResult context using applicative composition.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - fa: An IO value
//
//go:inline
func ApIOS[S1, S2, T any](
setter func(T) func(S1) S2,
fa IO[T],
) Operator[S1, S2] {
return ApS(setter, FromIO(fa))
}
// ApReaderS is an applicative variant that works with Reader values.
// It lifts a Reader value (with context.Context) into the ReaderIOResult context using applicative composition.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - fa: A Reader value
//
//go:inline
func ApReaderS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[context.Context, T],
) Operator[S1, S2] {
return ApS(setter, FromReader(fa))
}
// ApReaderIOS is an applicative variant that works with ReaderIO values.
// It lifts a ReaderIO value (with context.Context) into the ReaderIOResult context using applicative composition.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - fa: A ReaderIO value
//
//go:inline
func ApReaderIOS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[T],
) Operator[S1, S2] {
return ApS(setter, FromReaderIO(fa))
}
// ApEitherS is an applicative variant that works with Either (Result) values.
// It lifts an Either value into the ReaderIOResult context using applicative composition.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - fa: An Either value
//
//go:inline
func ApEitherS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Result[T],
) Operator[S1, S2] {
return ApS(setter, FromEither(fa))
}
// ApResultS is an applicative variant that works with Result values.
// This is an alias for ApEitherS for consistency with the Result naming convention.
//
// Parameters:
// - setter: Updates state from S1 to S2 using result T
// - fa: A Result value
//
//go:inline
func ApResultS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Result[T],
) Operator[S1, S2] {
return ApS(setter, FromResult(fa))
}
// ApIOEitherSL is a lens-based variant of ApIOEitherS.
// It combines a lens with an IOEither value using applicative composition.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - fa: An IOEither value
//
//go:inline
func ApIOEitherSL[S, T any](
lens L.Lens[S, T],
fa IOResult[T],
) Operator[S, S] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
}
// ApIOResultSL is a lens-based variant of ApIOResultS.
// This is an alias for ApIOEitherSL for consistency with the Result naming convention.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - fa: An IOResult value
//
//go:inline
func ApIOResultSL[S, T any](
lens L.Lens[S, T],
fa IOResult[T],
) Operator[S, S] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
}
// ApIOSL is a lens-based variant of ApIOS.
// It combines a lens with an IO value using applicative composition.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - fa: An IO value
//
//go:inline
func ApIOSL[S, T any](
lens L.Lens[S, T],
fa IO[T],
) Operator[S, S] {
return ApSL(lens, FromIO(fa))
}
// ApReaderSL is a lens-based variant of ApReaderS.
// It combines a lens with a Reader value (with context.Context) using applicative composition.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - fa: A Reader value
//
//go:inline
func ApReaderSL[S, T any](
lens L.Lens[S, T],
fa Reader[context.Context, T],
) Operator[S, S] {
return ApSL(lens, FromReader(fa))
}
// ApReaderIOSL is a lens-based variant of ApReaderIOS.
// It combines a lens with a ReaderIO value (with context.Context) using applicative composition.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - fa: A ReaderIO value
//
//go:inline
func ApReaderIOSL[S, T any](
lens L.Lens[S, T],
fa ReaderIO[T],
) Operator[S, S] {
return ApSL(lens, FromReaderIO(fa))
}
// ApEitherSL is a lens-based variant of ApEitherS.
// It combines a lens with an Either value using applicative composition.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - fa: An Either value
//
//go:inline
func ApEitherSL[S, T any](
lens L.Lens[S, T],
fa Result[T],
) Operator[S, S] {
return ApSL(lens, FromEither(fa))
}
// ApResultSL is a lens-based variant of ApResultS.
// This is an alias for ApEitherSL for consistency with the Result naming convention.
//
// Parameters:
// - lens: A lens focusing on field T within state S
// - fa: A Result value
//
//go:inline
func ApResultSL[S, T any](
lens L.Lens[S, T],
fa Result[T],
) Operator[S, S] {
return ApSL(lens, FromResult(fa))
}

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
@@ -26,11 +26,11 @@ import (
"github.com/stretchr/testify/assert"
)
func getLastName(s utils.Initial) ReaderIOEither[string] {
func getLastName(s utils.Initial) ReaderIOResult[string] {
return Of("Doe")
}
func getGivenName(s utils.WithLastName) ReaderIOEither[string] {
func getGivenName(s utils.WithLastName) ReaderIOResult[string] {
return Of("John")
}
@@ -229,7 +229,7 @@ func TestApS_ChainedWithBind(t *testing.T) {
}
}
getDependentValue := func(s State) ReaderIOEither[string] {
getDependentValue := func(s State) ReaderIOResult[string] {
// This depends on the Independent field
return Of(s.Independent + "-dependent")
}

View File

@@ -13,13 +13,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/internal/bracket"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
@@ -27,18 +24,9 @@ import (
func Bracket[
A, B, ANY any](
acquire ReaderIOEither[A],
acquire ReaderIOResult[A],
use Kleisli[A, B],
release func(A, Either[B]) ReaderIOEither[ANY],
) ReaderIOEither[B] {
return bracket.Bracket[ReaderIOEither[A], ReaderIOEither[B], ReaderIOEither[ANY], Either[B], A, B](
readerio.Of[context.Context, Either[B]],
MonadChain[A, B],
readerio.MonadChain[context.Context, Either[B], Either[B]],
MonadChain[ANY, B],
acquire,
use,
release,
)
release func(A, Either[B]) ReaderIOResult[ANY],
) ReaderIOResult[B] {
return RIOR.Bracket(acquire, use, release)
}

View File

@@ -13,26 +13,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
CIOE "github.com/IBM/fp-go/v2/context/ioeither"
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/ioeither"
)
// WithContext wraps an existing [ReaderIOEither] and performs a context check for cancellation before delegating.
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
// This ensures that if the context is already canceled, the computation short-circuits immediately
// without executing the wrapped computation.
//
// This is useful for adding cancellation awareness to computations that might not check the context themselves.
//
// Parameters:
// - ma: The ReaderIOEither to wrap with context checking
// - ma: The ReaderIOResult to wrap with context checking
//
// Returns a ReaderIOEither that checks for cancellation before executing.
func WithContext[A any](ma ReaderIOEither[A]) ReaderIOEither[A] {
// Returns a ReaderIOResult that checks for cancellation before executing.
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOEither[A] {
if err := context.Cause(ctx); err != nil {
return ioeither.Left[A](err)

View File

@@ -0,0 +1,251 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:27.21,29.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:35.47,42.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:48.47,54.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:60.47,66.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:71.46,76.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/bind.go:82.47,89.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/bracket.go:33.21,44.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:35.65,36.47 1 1
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:36.47,37.44 1 1
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:37.44,39.4 1 1
github.com/IBM/fp-go/v2/context/readerioresult/cancel.go:40.3,40.40 1 1
github.com/IBM/fp-go/v2/context/readerioresult/eq.go:42.84,44.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:18.91,20.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:24.93,26.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:30.101,32.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:36.103,38.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:43.36,48.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:53.36,58.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:63.36,68.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:71.98,76.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:79.101,84.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:87.101,92.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:95.129,96.68 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:96.68,102.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:106.132,107.68 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:107.68,113.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:117.132,118.68 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:118.68,124.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:129.113,131.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:135.115,137.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:143.40,150.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:156.40,163.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:169.40,176.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:179.126,185.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:188.129,194.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:197.129,203.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:206.185,207.76 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:207.76,215.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:219.188,220.76 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:220.76,228.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:232.188,233.76 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:233.76,241.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:246.125,248.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:252.127,254.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:261.44,270.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:277.44,286.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:293.44,302.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:305.154,312.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:315.157,322.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:325.157,332.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:335.241,336.84 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:336.84,346.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:350.244,351.84 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:351.84,361.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:365.244,366.84 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:366.84,376.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:381.137,383.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:387.139,389.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:397.48,408.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:416.48,427.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:435.48,446.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:449.182,457.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:460.185,468.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:471.185,479.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:482.297,483.92 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:483.92,495.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:499.300,500.92 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:500.92,512.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:516.300,517.92 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:517.92,529.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:534.149,536.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:540.151,542.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:551.52,564.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:573.52,586.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:595.52,608.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:611.210,620.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:623.213,632.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:635.213,644.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:647.353,648.100 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:648.100,662.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:666.356,667.100 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:667.100,681.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:685.356,686.100 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:686.100,700.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:705.161,707.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:711.163,713.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:723.56,738.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:748.56,763.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:773.56,788.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:791.238,801.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:804.241,814.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:817.241,827.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:830.409,831.108 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:831.108,847.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:851.412,852.108 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:852.108,868.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:872.412,873.108 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:873.108,889.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:894.173,896.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:900.175,902.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:913.60,930.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:941.60,958.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:969.60,986.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:989.266,1000.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1003.269,1014.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1017.269,1028.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1031.465,1032.116 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1032.116,1050.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1054.468,1055.116 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1055.116,1073.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1077.468,1078.116 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1078.116,1096.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1101.185,1103.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1107.187,1109.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1121.64,1140.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1152.64,1171.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1183.64,1202.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1205.294,1217.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1220.297,1232.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1235.297,1247.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1250.521,1251.124 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1251.124,1271.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1275.524,1276.124 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1276.124,1296.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1300.524,1301.124 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1301.124,1321.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1326.197,1328.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1332.199,1334.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1347.68,1368.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1381.68,1402.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1415.68,1436.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1439.322,1452.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1455.325,1468.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1471.325,1484.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1487.577,1488.132 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1488.132,1510.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1514.580,1515.132 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1515.132,1537.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1541.580,1542.132 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1542.132,1564.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1569.210,1571.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1575.212,1577.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1591.74,1614.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1628.74,1651.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1665.74,1688.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1691.356,1705.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1708.359,1722.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1725.359,1739.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1742.645,1743.144 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1743.144,1767.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1771.648,1772.144 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1772.144,1796.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1800.648,1801.144 1 0
github.com/IBM/fp-go/v2/context/readerioresult/gen.go:1801.144,1825.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:36.61,43.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:52.64,59.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:68.64,75.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:85.61,93.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/monoid.go:103.63,108.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:42.55,44.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:52.45,54.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:62.42,64.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:74.78,76.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:85.75,87.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:97.72,99.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:108.69,110.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:120.96,122.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:131.93,133.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:143.101,145.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:154.71,156.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:165.39,167.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:169.93,173.56 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:173.56,174.32 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:174.32,174.47 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:189.98,194.47 3 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:194.47,196.44 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:196.44,198.4 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:200.3,200.27 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:200.27,202.45 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:202.45,204.5 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:207.4,213.47 5 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:227.95,229.17 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:229.17,231.3 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:232.2,232.28 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:243.98,245.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:254.91,256.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:265.94,267.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:276.94,278.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:288.95,290.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:299.73,301.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:307.44,309.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:319.95,321.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:330.95,332.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:342.100,344.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:353.100,355.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:364.116,366.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:375.75,377.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:386.47,388.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:398.51,400.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:406.39,407.47 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:407.47,408.27 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:408.27,411.4 2 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:423.87,425.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:434.87,436.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:446.92,448.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:457.92,459.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:468.115,470.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:479.85,480.54 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:480.54,481.48 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:481.48,482.28 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:482.28,487.12 3 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:488.30,489.22 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:490.23,491.47 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:505.59,511.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:520.66,522.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:531.83,533.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:543.97,545.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:554.64,556.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:566.62,568.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:577.78,579.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:589.80,591.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:600.76,602.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:612.136,614.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:623.91,625.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/reader.go:634.71,636.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/resource.go:58.151,63.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/semigroup.go:39.41,43.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/sync.go:46.78,54.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:31.89,39.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:48.103,56.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:65.71,67.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:75.112,83.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:92.124,100.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:108.94,110.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:120.95,128.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:137.92,145.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:148.106,156.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:165.74,167.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:170.118,178.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:181.115,189.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:192.127,200.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:203.97,205.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:215.95,223.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:232.92,240.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:243.106,251.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:260.74,262.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:265.115,273.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:276.127,284.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:287.118,295.2 1 0
github.com/IBM/fp-go/v2/context/readerioresult/traverse.go:304.97,306.2 1 0

View File

@@ -13,13 +13,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package readerioeither provides a specialized version of [readerioeither.ReaderIOEither] that uses
// package readerioresult provides a specialized version of [readerioeither.ReaderIOResult] that uses
// [context.Context] as the context type and [error] as the left (error) type. This package is designed
// for typical Go applications where context-aware, effectful computations with error handling are needed.
//
// # Core Concept
//
// ReaderIOEither[A] represents a computation that:
// ReaderIOResult[A] represents a computation that:
// - Depends on a [context.Context] (Reader aspect)
// - Performs side effects (IO aspect)
// - Can fail with an [error] (Either aspect)
@@ -27,7 +27,7 @@
//
// The type is defined as:
//
// ReaderIOEither[A] = func(context.Context) func() Either[error, A]
// ReaderIOResult[A] = func(context.Context) func() Either[error, A]
//
// This combines three powerful functional programming concepts:
// - Reader: Dependency injection via context
@@ -50,7 +50,7 @@
// - [Left]: Create failed computations
// - [FromEither], [FromIO], [FromIOEither]: Convert from other types
// - [TryCatch]: Wrap error-returning functions
// - [Eitherize0-10]: Convert standard Go functions to ReaderIOEither
// - [Eitherize0-10]: Convert standard Go functions to ReaderIOResult
//
// Transformation:
// - [Map]: Transform success values
@@ -90,15 +90,15 @@
// import (
// "context"
// "fmt"
// RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
// RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Define a computation that reads from context and may fail
// func fetchUser(id string) RIOE.ReaderIOEither[User] {
// func fetchUser(id string) RIOE.ReaderIOResult[User] {
// return F.Pipe2(
// RIOE.Ask(),
// RIOE.Chain(func(ctx context.Context) RIOE.ReaderIOEither[User] {
// RIOE.Chain(func(ctx context.Context) RIOE.ReaderIOResult[User] {
// return RIOE.TryCatch(func(ctx context.Context) func() (User, error) {
// return func() (User, error) {
// return userService.Get(ctx, id)
@@ -138,8 +138,8 @@
// openFile("data.txt"),
// closeFile,
// ),
// func(use func(func(*os.File) RIOE.ReaderIOEither[string]) RIOE.ReaderIOEither[string]) RIOE.ReaderIOEither[string] {
// return use(func(file *os.File) RIOE.ReaderIOEither[string] {
// func(use func(func(*os.File) RIOE.ReaderIOResult[string]) RIOE.ReaderIOResult[string]) RIOE.ReaderIOResult[string] {
// return use(func(file *os.File) RIOE.ReaderIOResult[string] {
// return readContent(file)
// })
// },
@@ -166,4 +166,4 @@
// result := computation(ctx)() // Returns Left with cancellation error
//
//go:generate go run ../.. contextreaderioeither --count 10 --filename gen.go
package readerioeither
package readerioresult

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
@@ -22,14 +22,14 @@ import (
RIOE "github.com/IBM/fp-go/v2/readerioeither"
)
// Eq implements the equals predicate for values contained in the [ReaderIOEither] monad.
// It creates an equality checker that can compare two ReaderIOEither values by executing them
// Eq implements the equals predicate for values contained in the [ReaderIOResult] monad.
// It creates an equality checker that can compare two ReaderIOResult values by executing them
// with a given context and comparing their results using the provided Either equality checker.
//
// Parameters:
// - eq: Equality checker for Either[A] values
//
// Returns a function that takes a context and returns an equality checker for ReaderIOEither[A].
// Returns a function that takes a context and returns an equality checker for ReaderIOResult[A].
//
// Example:
//
@@ -41,6 +41,6 @@ import (
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
//
//go:inline
func Eq[A any](eq eq.Eq[Either[A]]) func(context.Context) eq.Eq[ReaderIOEither[A]] {
func Eq[A any](eq eq.Eq[Either[A]]) func(context.Context) eq.Eq[ReaderIOResult[A]] {
return RIOE.Eq[context.Context](eq)
}

View File

@@ -18,7 +18,7 @@ package exec
import (
"context"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/exec"
F "github.com/IBM/fp-go/v2/function"
GE "github.com/IBM/fp-go/v2/internal/exec"
@@ -30,7 +30,7 @@ var (
Command = F.Curry3(command)
)
func command(name string, args []string, in []byte) RIOE.ReaderIOEither[exec.CommandOutput] {
func command(name string, args []string, in []byte) RIOE.ReaderIOResult[exec.CommandOutput] {
return func(ctx context.Context) IOE.IOEither[error, exec.CommandOutput] {
return IOE.TryCatchError(func() (exec.CommandOutput, error) {
return GE.Exec(ctx, name, args, in)

View File

@@ -20,7 +20,7 @@ import (
"io"
"os"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/file"
@@ -44,7 +44,7 @@ var (
)
// Close closes an object
func Close[C io.Closer](c C) RIOE.ReaderIOEither[any] {
func Close[C io.Closer](c C) RIOE.ReaderIOResult[any] {
return F.Pipe2(
c,
IOEF.Close[C],
@@ -53,8 +53,8 @@ func Close[C io.Closer](c C) RIOE.ReaderIOEither[any] {
}
// ReadFile reads a file in the scope of a context
func ReadFile(path string) RIOE.ReaderIOEither[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOEither[[]byte] {
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
return func(ctx context.Context) IOE.IOEither[error, []byte] {
return func() ET.Either[error, []byte] {
return file.ReadAll(ctx, r)

View File

@@ -19,7 +19,7 @@ import (
"context"
"fmt"
R "github.com/IBM/fp-go/v2/context/readerioeither"
R "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
J "github.com/IBM/fp-go/v2/json"

View File

@@ -18,7 +18,7 @@ package file
import (
"os"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
IO "github.com/IBM/fp-go/v2/io"
IOF "github.com/IBM/fp-go/v2/io/file"
@@ -38,7 +38,7 @@ var (
)
// CreateTemp created a temp file with proper parametrization
func CreateTemp(dir, pattern string) RIOE.ReaderIOEither[*os.File] {
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
return F.Pipe2(
IOEF.CreateTemp(dir, pattern),
RIOE.FromIOEither[*os.File],
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOEither[*os.File] {
}
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOEither[A]) RIOE.ReaderIOEither[A] {
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
}

View File

@@ -20,7 +20,7 @@ import (
"os"
"testing"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
@@ -35,7 +35,7 @@ func TestWithTempFile(t *testing.T) {
func TestWithTempFileOnClosedFile(t *testing.T) {
res := WithTempFile(func(f *os.File) RIOE.ReaderIOEither[[]byte] {
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
return F.Pipe2(
f,
onWriteAll[*os.File]([]byte("Carsten")),

View File

@@ -19,12 +19,12 @@ import (
"context"
"io"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
)
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOEither[[]byte] {
return func(w W) RIOE.ReaderIOEither[[]byte] {
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
return func(w W) RIOE.ReaderIOResult[[]byte] {
return F.Pipe1(
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
return func() ([]byte, error) {
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOEither[[]byte]
}
// WriteAll uses a generator function to create a stream, writes data to it and closes it
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOEither[W]) RIOE.ReaderIOEither[[]byte] {
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
onWrite := onWriteAll[W](data)
return func(onCreate RIOE.ReaderIOEither[W]) RIOE.ReaderIOEither[[]byte] {
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](
onCreate,
Close[W])(
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOEither[W]
}
// Write uses a generator function to create a stream, writes data to it and closes it
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOEither[W]) func(use func(W) RIOE.ReaderIOEither[R]) RIOE.ReaderIOEither[R] {
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
return RIOE.WithResource[R](
acquire,
Close[W])

View File

@@ -0,0 +1,295 @@
// 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 readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/reader"
RIO "github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
RR "github.com/IBM/fp-go/v2/readerresult"
)
// SequenceReader transforms a ReaderIOResult containing a Reader into a function that
// takes the Reader's environment first, then returns a ReaderIOResult.
//
// This function "flips" or "sequences" the nested structure, changing the order in which
// parameters are applied. It's particularly useful for point-free style programming where
// you want to partially apply the inner Reader's environment before dealing with the
// outer context.
//
// Type transformation:
//
// From: ReaderIOResult[Reader[R, A]]
// = func(context.Context) func() Either[error, func(R) A]
//
// To: func(context.Context) func(R) IOResult[A]
// = func(context.Context) func(R) func() Either[error, A]
//
// This allows you to:
// 1. Provide the context.Context first
// 2. Then provide the Reader's environment R
// 3. Finally execute the IO effect to get Either[error, A]
//
// Point-free style benefits:
// - Enables partial application of the Reader environment
// - Facilitates composition of Reader-based computations
// - Allows building reusable computation pipelines
// - Supports dependency injection patterns where R represents dependencies
//
// Example:
//
// type Config struct {
// Timeout int
// }
//
// // A computation that produces a Reader based on context
// func getMultiplier(ctx context.Context) func() Either[error, func(Config) int] {
// return func() Either[error, func(Config) int] {
// return Right[error](func(cfg Config) int {
// return cfg.Timeout * 2
// })
// }
// }
//
// // Sequence it to apply Config first
// sequenced := SequenceReader[Config, int](getMultiplier)
//
// // Now we can partially apply the Config
// cfg := Config{Timeout: 30}
// ctx := context.Background()
// result := sequenced(ctx)(cfg)() // Returns Right(60)
//
// This is especially useful in point-free style when building computation pipelines:
//
// var pipeline = F.Flow3(
// loadConfig, // ReaderIOResult[Reader[Database, Config]]
// SequenceReader, // func(context.Context) func(Database) IOResult[Config]
// applyToDatabase(db), // IOResult[Config]
// )
//
//go:inline
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
return RIOR.SequenceReader(ma)
}
// SequenceReaderIO transforms a ReaderIOResult containing a ReaderIO into a function that
// takes the ReaderIO's environment first, then returns a ReaderIOResult.
//
// This is similar to SequenceReader but works with ReaderIO, which represents a computation
// that depends on an environment R and performs IO effects.
//
// Type transformation:
//
// From: ReaderIOResult[ReaderIO[R, A]]
// = func(context.Context) func() Either[error, func(R) func() A]
//
// To: func(context.Context) func(R) IOResult[A]
// = func(context.Context) func(R) func() Either[error, A]
//
// The key difference from SequenceReader is that the inner computation (ReaderIO) already
// performs IO effects, so the sequencing combines these effects properly.
//
// Point-free style benefits:
// - Enables composition of ReaderIO-based computations
// - Allows partial application of environment before IO execution
// - Facilitates building effect pipelines with dependency injection
// - Supports layered architecture where R represents service dependencies
//
// Example:
//
// type Database struct {
// ConnectionString string
// }
//
// // A computation that produces a ReaderIO based on context
// func getQuery(ctx context.Context) func() Either[error, func(Database) func() string] {
// return func() Either[error, func(Database) func() string] {
// return Right[error](func(db Database) func() string {
// return func() string {
// // Perform actual IO here
// return "Query result from " + db.ConnectionString
// }
// })
// }
// }
//
// // Sequence it to apply Database first
// sequenced := SequenceReaderIO[Database, string](getQuery)
//
// // Partially apply the Database
// db := Database{ConnectionString: "localhost:5432"}
// ctx := context.Background()
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
//
// In point-free style, this enables clean composition:
//
// var executeQuery = F.Flow3(
// prepareQuery, // ReaderIOResult[ReaderIO[Database, QueryResult]]
// SequenceReaderIO, // func(context.Context) func(Database) IOResult[QueryResult]
// withDatabase(db), // IOResult[QueryResult]
// )
//
//go:inline
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
return RIOR.SequenceReaderIO(ma)
}
// SequenceReaderResult transforms a ReaderIOResult containing a ReaderResult into a function
// that takes the ReaderResult's environment first, then returns a ReaderIOResult.
//
// This is similar to SequenceReader but works with ReaderResult, which represents a computation
// that depends on an environment R and can fail with an error.
//
// Type transformation:
//
// From: ReaderIOResult[ReaderResult[R, A]]
// = func(context.Context) func() Either[error, func(R) Either[error, A]]
//
// To: func(context.Context) func(R) IOResult[A]
// = func(context.Context) func(R) func() Either[error, A]
//
// The sequencing properly combines the error handling from both the outer ReaderIOResult
// and the inner ReaderResult, ensuring that errors from either level are propagated correctly.
//
// Point-free style benefits:
// - Enables composition of error-handling computations with dependency injection
// - Allows partial application of dependencies before error handling
// - Facilitates building validation pipelines with environment dependencies
// - Supports service-oriented architectures with proper error propagation
//
// Example:
//
// type Config struct {
// MaxRetries int
// }
//
// // A computation that produces a ReaderResult based on context
// func validateRetries(ctx context.Context) func() Either[error, func(Config) Either[error, int]] {
// return func() Either[error, func(Config) Either[error, int]] {
// return Right[error](func(cfg Config) Either[error, int] {
// if cfg.MaxRetries < 0 {
// return Left[int](errors.New("negative retries"))
// }
// return Right[error](cfg.MaxRetries)
// })
// }
// }
//
// // Sequence it to apply Config first
// sequenced := SequenceReaderResult[Config, int](validateRetries)
//
// // Partially apply the Config
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
// result := sequenced(ctx)(cfg)() // Returns Right(3)
//
// // With invalid config
// badCfg := Config{MaxRetries: -1}
// badResult := sequenced(ctx)(badCfg)() // Returns Left(error("negative retries"))
//
// In point-free style, this enables validation pipelines:
//
// var validateAndProcess = F.Flow4(
// loadConfig, // ReaderIOResult[ReaderResult[Config, Settings]]
// SequenceReaderResult, // func(context.Context) func(Config) IOResult[Settings]
// applyConfig(cfg), // IOResult[Settings]
// Chain(processSettings), // IOResult[Result]
// )
//
//go:inline
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
return RIOR.SequenceReaderEither(ma)
}
// TraverseReader transforms a ReaderIOResult computation by applying a Reader-based function,
// effectively introducing a new environment dependency.
//
// This function takes a Reader-based transformation (Kleisli arrow) and returns a function that
// can transform a ReaderIOResult. The result allows you to provide the Reader's environment (R)
// first, which then produces a ReaderIOResult that depends on the context.
//
// Type transformation:
//
// From: ReaderIOResult[A]
// = func(context.Context) func() Either[error, A]
//
// With: reader.Kleisli[R, A, B]
// = func(A) func(R) B
//
// To: func(ReaderIOResult[A]) func(R) ReaderIOResult[B]
// = func(ReaderIOResult[A]) func(R) func(context.Context) func() Either[error, B]
//
// This enables:
// 1. Transforming values within a ReaderIOResult using environment-dependent logic
// 2. Introducing new environment dependencies into existing computations
// 3. Building composable pipelines where transformations depend on configuration or dependencies
// 4. Point-free style composition with Reader-based transformations
//
// Type Parameters:
// - R: The environment type that the Reader depends on
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R
//
// Returns:
// - A function that takes a ReaderIOResult[A] and returns a Kleisli[R, B],
// which is func(R) ReaderIOResult[B]
//
// The function preserves error handling and IO effects while adding the Reader environment dependency.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // A Reader-based transformation that depends on Config
// multiply := func(x int) func(Config) int {
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }
// }
//
// // Original computation that produces an int
// computation := Right[int](10)
//
// // Apply TraverseReader to introduce Config dependency
// traversed := TraverseReader[Config, int, int](multiply)
// result := traversed(computation)
//
// // Now we can provide the Config to get the final result
// cfg := Config{Multiplier: 5}
// ctx := context.Background()
// finalResult := result(cfg)(ctx)() // Returns Right(50)
//
// In point-free style, this enables clean composition:
//
// var pipeline = F.Flow3(
// loadValue, // ReaderIOResult[int]
// TraverseReader(multiplyByConfig), // func(Config) ReaderIOResult[int]
// applyConfig(cfg), // ReaderIOResult[int]
// )
//
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderIOResult[A]) Kleisli[R, B] {
return RIOR.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,333 @@
// 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 readerioresult_test
import (
"context"
"fmt"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
)
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
// to flip the parameter order, enabling point-free style programming.
func Example_sequenceReader_basicUsage() {
type Config struct {
Multiplier int
}
// A computation that produces a Reader based on context
getComputation := func(ctx context.Context) func() either.Either[error, func(Config) int] {
return func() either.Either[error, func(Config) int] {
// This could check context for cancellation, deadlines, etc.
return either.Right[error](func(cfg Config) int {
return cfg.Multiplier * 10
})
}
}
// Sequence it to flip the parameter order
// Now Config comes first, then context
sequenced := RIOE.SequenceReader(getComputation)
// Partially apply the Config - this is the key benefit for point-free style
cfg := Config{Multiplier: 5}
withConfig := sequenced(cfg)
// Now we have a ReaderIOResult[int] that can be used with any context
ctx := context.Background()
result := withConfig(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: 50
}
// Example_sequenceReader_dependencyInjection demonstrates how SequenceReader
// enables clean dependency injection patterns in point-free style.
func Example_sequenceReader_dependencyInjection() {
// Define our dependencies
type Database struct {
ConnectionString string
}
type UserService struct {
db Database
}
// A function that creates a computation requiring a Database
makeQuery := func(ctx context.Context) func() either.Either[error, func(Database) string] {
return func() either.Either[error, func(Database) string] {
return either.Right[error](func(db Database) string {
return fmt.Sprintf("Querying %s", db.ConnectionString)
})
}
}
// Sequence to enable dependency injection
queryWithDB := RIOE.SequenceReader(makeQuery)
// Inject the database dependency
db := Database{ConnectionString: "localhost:5432"}
query := queryWithDB(db)
// Execute with context
ctx := context.Background()
result := query(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: Querying localhost:5432
}
// Example_sequenceReader_pointFreeComposition demonstrates how SequenceReader
// enables point-free style composition of computations.
func Example_sequenceReader_pointFreeComposition() {
type Config struct {
BaseValue int
}
// Step 1: Create a computation that produces a Reader
step1 := func(ctx context.Context) func() either.Either[error, func(Config) int] {
return func() either.Either[error, func(Config) int] {
return either.Right[error](func(cfg Config) int {
return cfg.BaseValue * 2
})
}
}
// Step 2: Sequence it to enable partial application
sequenced := RIOE.SequenceReader(step1)
// Step 3: Build a pipeline using point-free style
// Partially apply the config
cfg := Config{BaseValue: 10}
// Create a reusable computation with the config baked in
computation := F.Pipe1(
sequenced(cfg),
RIOE.Map(func(x int) int { return x + 5 }),
)
// Execute the pipeline
ctx := context.Background()
result := computation(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: 25
}
// Example_sequenceReader_multipleEnvironments demonstrates using SequenceReader
// to work with multiple environment types in a clean, composable way.
func Example_sequenceReader_multipleEnvironments() {
type DatabaseConfig struct {
Host string
Port int
}
type APIConfig struct {
Endpoint string
APIKey string
}
// Function that needs DatabaseConfig
getDatabaseURL := func(ctx context.Context) func() either.Either[error, func(DatabaseConfig) string] {
return func() either.Either[error, func(DatabaseConfig) string] {
return either.Right[error](func(cfg DatabaseConfig) string {
return fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
})
}
}
// Function that needs APIConfig
getAPIURL := func(ctx context.Context) func() either.Either[error, func(APIConfig) string] {
return func() either.Either[error, func(APIConfig) string] {
return either.Right[error](func(cfg APIConfig) string {
return cfg.Endpoint
})
}
}
// Sequence both to enable partial application
withDBConfig := RIOE.SequenceReader(getDatabaseURL)
withAPIConfig := RIOE.SequenceReader(getAPIURL)
// Partially apply different configs
dbCfg := DatabaseConfig{Host: "localhost", Port: 5432}
apiCfg := APIConfig{Endpoint: "https://api.example.com", APIKey: "secret"}
dbQuery := withDBConfig(dbCfg)
apiQuery := withAPIConfig(apiCfg)
// Execute both with the same context
ctx := context.Background()
dbResult := dbQuery(ctx)()
apiResult := apiQuery(ctx)()
if dbURL, err := either.Unwrap(dbResult); err == nil {
fmt.Println("Database:", dbURL)
}
if apiURL, err := either.Unwrap(apiResult); err == nil {
fmt.Println("API:", apiURL)
}
// Output:
// Database: localhost:5432
// API: https://api.example.com
}
// Example_sequenceReaderResult_errorHandling demonstrates how SequenceReaderResult
// enables point-free style with proper error handling at multiple levels.
func Example_sequenceReaderResult_errorHandling() {
type ValidationConfig struct {
MinValue int
MaxValue int
}
// A computation that can fail at both outer and inner levels
makeValidator := func(ctx context.Context) func() either.Either[error, func(context.Context) either.Either[error, int]] {
return func() either.Either[error, func(context.Context) either.Either[error, int]] {
// Outer level: check context
if ctx.Err() != nil {
return either.Left[func(context.Context) either.Either[error, int]](ctx.Err())
}
// Return inner computation
return either.Right[error](func(innerCtx context.Context) either.Either[error, int] {
// Inner level: perform validation
value := 42
if value < 0 {
return either.Left[int](fmt.Errorf("value too small: %d", value))
}
if value > 100 {
return either.Left[int](fmt.Errorf("value too large: %d", value))
}
return either.Right[error](value)
})
}
}
// Sequence to enable point-free composition
sequenced := RIOE.SequenceReaderResult(makeValidator)
// Build a pipeline with error handling
ctx := context.Background()
pipeline := F.Pipe2(
sequenced(ctx),
RIOE.Map(func(x int) int { return x * 2 }),
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
return RIOE.Of(fmt.Sprintf("Result: %d", x))
}),
)
result := pipeline(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: Result: 84
}
// Example_sequenceReader_partialApplication demonstrates the power of partial
// application enabled by SequenceReader for building reusable computations.
func Example_sequenceReader_partialApplication() {
type ServiceConfig struct {
ServiceName string
Version string
}
// Create a computation factory
makeServiceInfo := func(ctx context.Context) func() either.Either[error, func(ServiceConfig) string] {
return func() either.Either[error, func(ServiceConfig) string] {
return either.Right[error](func(cfg ServiceConfig) string {
return fmt.Sprintf("%s v%s", cfg.ServiceName, cfg.Version)
})
}
}
// Sequence it
sequenced := RIOE.SequenceReader(makeServiceInfo)
// Create multiple service configurations
authConfig := ServiceConfig{ServiceName: "AuthService", Version: "1.0.0"}
userConfig := ServiceConfig{ServiceName: "UserService", Version: "2.1.0"}
// Partially apply each config to create specialized computations
getAuthInfo := sequenced(authConfig)
getUserInfo := sequenced(userConfig)
// These can now be reused across different contexts
ctx := context.Background()
authResult := getAuthInfo(ctx)()
userResult := getUserInfo(ctx)()
if auth, err := either.Unwrap(authResult); err == nil {
fmt.Println(auth)
}
if user, err := either.Unwrap(userResult); err == nil {
fmt.Println(user)
}
// Output:
// AuthService v1.0.0
// UserService v2.1.0
}
// Example_sequenceReader_testingBenefits demonstrates how SequenceReader
// makes testing easier by allowing you to inject test dependencies.
func Example_sequenceReader_testingBenefits() {
// Simple logger that collects messages
type SimpleLogger struct {
Messages []string
}
// A computation that depends on a logger (using the struct directly)
makeLoggingOperation := func(ctx context.Context) func() either.Either[error, func(*SimpleLogger) string] {
return func() either.Either[error, func(*SimpleLogger) string] {
return either.Right[error](func(logger *SimpleLogger) string {
logger.Messages = append(logger.Messages, "Operation started")
result := "Success"
logger.Messages = append(logger.Messages, fmt.Sprintf("Operation completed: %s", result))
return result
})
}
}
// Sequence to enable dependency injection
sequenced := RIOE.SequenceReader(makeLoggingOperation)
// Inject a test logger
testLogger := &SimpleLogger{Messages: []string{}}
operation := sequenced(testLogger)
// Execute
ctx := context.Background()
result := operation(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println("Result:", value)
fmt.Println("Logs:", len(testLogger.Messages))
}
// Output:
// Result: Success
// Logs: 2
}

View File

@@ -0,0 +1,866 @@
// 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 readerioresult
import (
"context"
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/either"
"github.com/stretchr/testify/assert"
)
func TestSequenceReader(t *testing.T) {
t.Run("flips parameter order for simple types", func(t *testing.T) {
// Original: ReaderIOResult[Reader[string, int]]
// = func(context.Context) func() Either[error, func(string) int]
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return 10 + len(s)
})
}
}
// Sequenced: func(string) func(context.Context) IOResult[int]
// The Reader environment (string) is now the first parameter
sequenced := SequenceReader(original)
ctx := context.Background()
// Test original
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("hello")
assert.Equal(t, 15, value1)
// Test sequenced - note the flipped order: string first, then context
result2 := sequenced("hello")(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 15, value2)
})
t.Run("flips parameter order for struct types", func(t *testing.T) {
type Database struct {
ConnectionString string
}
// Original: ReaderIOResult[Reader[Database, string]]
query := func(ctx context.Context) func() Either[Reader[Database, string]] {
return func() Either[Reader[Database, string]] {
if ctx.Err() != nil {
return either.Left[Reader[Database, string]](ctx.Err())
}
return either.Right[error](func(db Database) string {
return fmt.Sprintf("Query on %s", db.ConnectionString)
})
}
}
db := Database{ConnectionString: "localhost:5432"}
ctx := context.Background()
expected := "Query on localhost:5432"
// Sequence it
sequenced := SequenceReader(query)
// Test original with valid inputs
result1 := query(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(db)
assert.Equal(t, expected, value1)
// Test sequenced with valid inputs - Database first, then context
result2 := sequenced(db)(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, expected, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
// Original that fails at outer level
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Left[Reader[string, int]](expectedError)
}
}
ctx := context.Background()
// Test original with error
result1 := original(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Equal(t, expectedError, err1)
// Test sequenced - the outer error is preserved
sequenced := SequenceReader(original)
result2 := sequenced("test")(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Equal(t, expectedError, err2)
})
t.Run("preserves computation logic", func(t *testing.T) {
// Original function
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return 3 * len(s)
})
}
}
ctx := context.Background()
// Sequence
sequenced := SequenceReader(original)
// Test that sequence produces correct results
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("test")
result2 := sequenced("test")(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, value1, value2)
assert.Equal(t, 12, value2) // 3 * 4
})
t.Run("works with zero values", func(t *testing.T) {
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return len(s)
})
}
}
ctx := context.Background()
sequenced := SequenceReader(original)
// Test with zero values
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("")
assert.Equal(t, 0, value1)
result2 := sequenced("")(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 0, value2)
})
t.Run("respects context cancellation", func(t *testing.T) {
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
if ctx.Err() != nil {
return either.Left[Reader[string, int]](ctx.Err())
}
return either.Right[error](func(s string) int {
return len(s)
})
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
sequenced := SequenceReader(original)
result := sequenced("test")(ctx)()
assert.True(t, either.IsLeft(result))
_, err := either.Unwrap(result)
assert.Equal(t, context.Canceled, err)
})
t.Run("enables point-free style with partial application", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := func(ctx context.Context) func() Either[Reader[Config, int]] {
return func() Either[Reader[Config, int]] {
return either.Right[error](func(cfg Config) int {
return cfg.Multiplier * 10
})
}
}
// Sequence to enable partial application
sequenced := SequenceReader(original)
// Partially apply the Config
cfg := Config{Multiplier: 5}
withConfig := sequenced(cfg)
// Now we have a ReaderIOResult[int] that can be used in different contexts
ctx1 := context.Background()
result1 := withConfig(ctx1)()
assert.True(t, either.IsRight(result1))
value1, _ := either.Unwrap(result1)
assert.Equal(t, 50, value1)
// Can reuse with different context
ctx2 := context.Background()
result2 := withConfig(ctx2)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 50, value2)
})
}
func TestSequenceReaderIO(t *testing.T) {
t.Run("flips parameter order for simple types", func(t *testing.T) {
// Original: ReaderIOResult[ReaderIO[int]]
// = func(context.Context) func() Either[error, func(context.Context) func() int]
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
return func() Either[ReaderIO[int]] {
return either.Right[error](func(innerCtx context.Context) func() int {
return func() int {
return 20
}
})
}
}
ctx := context.Background()
sequenced := SequenceReaderIO(original)
// Test original
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(ctx)()
assert.Equal(t, 20, value1)
// Test sequenced - context first, then context again for inner ReaderIO
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 20, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
// Original that fails at outer level
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
return func() Either[ReaderIO[int]] {
return either.Left[ReaderIO[int]](expectedError)
}
}
ctx := context.Background()
// Test original with error
result1 := original(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Equal(t, expectedError, err1)
// Test sequenced - the outer error is preserved
sequenced := SequenceReaderIO(original)
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Equal(t, expectedError, err2)
})
t.Run("respects context cancellation in outer context", func(t *testing.T) {
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
return func() Either[ReaderIO[int]] {
if ctx.Err() != nil {
return either.Left[ReaderIO[int]](ctx.Err())
}
return either.Right[error](func(innerCtx context.Context) func() int {
return func() int {
return 20
}
})
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
sequenced := SequenceReaderIO(original)
result := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result))
_, err := either.Unwrap(result)
assert.Equal(t, context.Canceled, err)
})
}
func TestSequenceReaderResult(t *testing.T) {
t.Run("flips parameter order for simple types", func(t *testing.T) {
// Original: ReaderIOResult[ReaderResult[int]]
// = func(context.Context) func() Either[error, func(context.Context) Either[error, int]]
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
return either.Right[error](func(innerCtx context.Context) Either[int] {
return either.Right[error](20)
})
}
}
ctx := context.Background()
sequenced := SequenceReaderResult(original)
// Test original
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
innerResult1 := innerFunc1(ctx)
assert.True(t, either.IsRight(innerResult1))
value1, _ := either.Unwrap(innerResult1)
assert.Equal(t, 20, value1)
// Test sequenced
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 20, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
// Original that fails at outer level
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
return either.Left[ReaderResult[int]](expectedError)
}
}
ctx := context.Background()
// Test original with error
result1 := original(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Equal(t, expectedError, err1)
// Test sequenced - the outer error is preserved
sequenced := SequenceReaderResult(original)
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Equal(t, expectedError, err2)
})
t.Run("preserves inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
// Original that fails at inner level
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
return either.Right[error](func(innerCtx context.Context) Either[int] {
return either.Left[int](expectedError)
})
}
}
ctx := context.Background()
// Test original with inner error
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
innerResult1 := innerFunc1(ctx)
assert.True(t, either.IsLeft(innerResult1))
_, innerErr1 := either.Unwrap(innerResult1)
assert.Equal(t, expectedError, innerErr1)
// Test sequenced with inner error
sequenced := SequenceReaderResult(original)
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, innerErr2 := either.Unwrap(result2)
assert.Equal(t, expectedError, innerErr2)
})
t.Run("handles errors at different levels", func(t *testing.T) {
// Original that can fail at both levels
makeOriginal := func(x int) ReaderIOResult[ReaderResult[int]] {
return func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
if x < -10 {
return either.Left[ReaderResult[int]](errors.New("outer: too negative"))
}
return either.Right[error](func(innerCtx context.Context) Either[int] {
if x < 0 {
return either.Left[int](errors.New("inner: negative value"))
}
return either.Right[error](x * 2)
})
}
}
}
ctx := context.Background()
// Test outer error
sequenced1 := SequenceReaderResult(makeOriginal(-20))
result1 := sequenced1(ctx)(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Contains(t, err1.Error(), "outer")
// Test inner error
sequenced2 := SequenceReaderResult(makeOriginal(-5))
result2 := sequenced2(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Contains(t, err2.Error(), "inner")
// Test success
sequenced3 := SequenceReaderResult(makeOriginal(10))
result3 := sequenced3(ctx)(ctx)()
assert.True(t, either.IsRight(result3))
value3, _ := either.Unwrap(result3)
assert.Equal(t, 20, value3)
})
t.Run("respects context cancellation", func(t *testing.T) {
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
if ctx.Err() != nil {
return either.Left[ReaderResult[int]](ctx.Err())
}
return either.Right[error](func(innerCtx context.Context) Either[int] {
if innerCtx.Err() != nil {
return either.Left[int](innerCtx.Err())
}
return either.Right[error](20)
})
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
sequenced := SequenceReaderResult(original)
result := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result))
_, err := either.Unwrap(result)
assert.Equal(t, context.Canceled, err)
})
}
func TestSequenceEdgeCases(t *testing.T) {
t.Run("works with empty struct", func(t *testing.T) {
type Empty struct{}
original := func(ctx context.Context) func() Either[Reader[Empty, int]] {
return func() Either[Reader[Empty, int]] {
return either.Right[error](func(e Empty) int {
return 20
})
}
}
ctx := context.Background()
empty := Empty{}
sequenced := SequenceReader(original)
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(empty)
assert.Equal(t, 20, value1)
result2 := sequenced(empty)(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 20, value2)
})
t.Run("works with pointer types", func(t *testing.T) {
type Data struct {
Value int
}
original := func(ctx context.Context) func() Either[Reader[*Data, int]] {
return func() Either[Reader[*Data, int]] {
return either.Right[error](func(d *Data) int {
if d == nil {
return 42
}
return 42 + d.Value
})
}
}
ctx := context.Background()
data := &Data{Value: 100}
sequenced := SequenceReader(original)
// Test with non-nil pointer
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(data)
assert.Equal(t, 142, value1)
result2 := sequenced(data)(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 142, value2)
// Test with nil pointer
result3 := sequenced(nil)(ctx)()
value3, _ := either.Unwrap(result3)
assert.Equal(t, 42, value3)
})
t.Run("maintains referential transparency", func(t *testing.T) {
// The same inputs should always produce the same outputs
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return 10 + len(s)
})
}
}
ctx := context.Background()
sequenced := SequenceReader(original)
// Call multiple times with same inputs
for range 5 {
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("hello")
assert.Equal(t, 15, value1)
result2 := sequenced("hello")(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 15, value2)
}
})
}
func TestTraverseReader(t *testing.T) {
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := Right(10)
// Reader-based transformation
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, 50, value)
})
t.Run("preserves outer error", func(t *testing.T) {
type Config struct {
Multiplier int
}
expectedError := errors.New("computation failed")
// Original computation that fails
original := Left[int](expectedError)
// Reader-based transformation (won't be called)
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsLeft(finalResult))
_, err := either.Unwrap(finalResult)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
type Database struct {
Prefix string
}
// Original computation producing an int
original := Right(42)
// Reader-based transformation: int -> string using Database
format := func(x int) func(Database) string {
return func(db Database) string {
return fmt.Sprintf("%s:%d", db.Prefix, x)
}
}
// Apply TraverseReader
traversed := TraverseReader(format)
result := traversed(original)
// Provide Database and execute
db := Database{Prefix: "ID"}
ctx := context.Background()
finalResult := result(db)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, "ID:42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Settings struct {
Prefix string
Suffix string
}
// Original computation
original := Right("value")
// Reader-based transformation using Settings
decorate := func(s string) func(Settings) string {
return func(settings Settings) string {
return settings.Prefix + s + settings.Suffix
}
}
// Apply TraverseReader
traversed := TraverseReader(decorate)
result := traversed(original)
// Provide Settings and execute
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := context.Background()
finalResult := result(settings)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, "[value]", value)
})
t.Run("enables partial application", func(t *testing.T) {
type Config struct {
Factor int
}
// Original computation
original := Right(10)
// Reader-based transformation
scale := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Factor
}
}
// Apply TraverseReader
traversed := TraverseReader(scale)
result := traversed(original)
// Partially apply Config
cfg := Config{Factor: 3}
withConfig := result(cfg)
// Can now use with different contexts
ctx1 := context.Background()
finalResult1 := withConfig(ctx1)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)
assert.Equal(t, 30, value1)
// Reuse with different context
ctx2 := context.Background()
finalResult2 := withConfig(ctx2)()
assert.True(t, either.IsRight(finalResult2))
value2, _ := either.Unwrap(finalResult2)
assert.Equal(t, 30, value2)
})
t.Run("respects context cancellation", func(t *testing.T) {
type Config struct {
Value int
}
// Original computation that checks context
original := func(ctx context.Context) func() Either[int] {
return func() Either[int] {
if ctx.Err() != nil {
return either.Left[int](ctx.Err())
}
return either.Right[error](10)
}
}
// Reader-based transformation
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Value
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Use canceled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
cfg := Config{Value: 5}
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsLeft(finalResult))
_, err := either.Unwrap(finalResult)
assert.Equal(t, context.Canceled, err)
})
t.Run("works with zero values", func(t *testing.T) {
type Config struct {
Offset int
}
// Original computation with zero value
original := Right(0)
// Reader-based transformation
add := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x + cfg.Offset
}
}
// Apply TraverseReader
traversed := TraverseReader(add)
result := traversed(original)
// Provide Config with zero offset
cfg := Config{Offset: 0}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, 0, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := Right(5)
// First Reader-based transformation
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 4}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, 20, value) // 5 * 4 = 20
})
t.Run("works with complex Reader logic", func(t *testing.T) {
type ValidationRules struct {
MinValue int
MaxValue int
}
// Original computation
original := Right(50)
// Reader-based transformation with validation logic
validate := func(x int) func(ValidationRules) int {
return func(rules ValidationRules) int {
if x < rules.MinValue {
return rules.MinValue
}
if x > rules.MaxValue {
return rules.MaxValue
}
return x
}
}
// Apply TraverseReader
traversed := TraverseReader(validate)
result := traversed(original)
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
ctx := context.Background()
finalResult1 := result(rules1)(ctx)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)
assert.Equal(t, 50, value1)
// Test with value above max
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
finalResult2 := result(rules2)(ctx)()
assert.True(t, either.IsRight(finalResult2))
value2, _ := either.Unwrap(finalResult2)
assert.Equal(t, 30, value2) // Clamped to max
// Test with value below min
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
finalResult3 := result(rules3)(ctx)()
assert.True(t, either.IsRight(finalResult3))
value3, _ := either.Unwrap(finalResult3)
assert.Equal(t, 60, value3) // Clamped to min
})
}

View File

@@ -14,12 +14,12 @@
// limitations under the License.
// Package builder provides utilities for building HTTP requests in a functional way
// using the ReaderIOEither monad. It integrates with the http/builder package to
// using the ReaderIOResult monad. It integrates with the http/builder package to
// create composable, type-safe HTTP request builders with proper error handling
// and context support.
//
// The main function, Requester, converts a Builder from the http/builder package
// into a ReaderIOEither that produces HTTP requests. This allows for:
// into a ReaderIOResult that produces HTTP requests. This allows for:
// - Immutable request building with method chaining
// - Automatic header management including Content-Length
// - Support for requests with and without bodies
@@ -31,7 +31,7 @@
// import (
// "context"
// B "github.com/IBM/fp-go/v2/http/builder"
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
// RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
// )
//
// builder := F.Pipe3(
@@ -51,17 +51,17 @@ import (
"net/http"
"strconv"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOEH "github.com/IBM/fp-go/v2/context/readerioeither/http"
E "github.com/IBM/fp-go/v2/either"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
RIOEH "github.com/IBM/fp-go/v2/context/readerioresult/http"
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 ReaderIOEither that produces HTTP requests.
// Requester converts an http/builder.Builder into a ReaderIOResult that produces HTTP requests.
// It handles both requests with and without bodies, automatically managing headers including
// Content-Length for requests with bodies.
//
@@ -86,14 +86,14 @@ import (
// - builder: A pointer to an http/builder.Builder containing request configuration
//
// Returns:
// - A Requester (ReaderIOEither[*http.Request]) that, when executed with a context,
// - A Requester (ReaderIOResult[*http.Request]) that, when executed with a context,
// produces either an error or a configured *http.Request
//
// Example with body:
//
// import (
// B "github.com/IBM/fp-go/v2/http/builder"
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
// RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder"
// )
//
// builder := F.Pipe3(
@@ -116,7 +116,7 @@ import (
// result := requester(context.Background())()
func Requester(builder *R.Builder) RIOEH.Requester {
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOEither[*http.Request] {
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
return func() (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(data))
@@ -129,7 +129,7 @@ func Requester(builder *R.Builder) RIOEH.Requester {
})
})
withoutBody := F.Curry2(func(url string, method string) RIOE.ReaderIOEither[*http.Request] {
withoutBody := F.Curry2(func(url string, method string) RIOE.ReaderIOResult[*http.Request] {
return RIOE.TryCatch(func(ctx context.Context) func() (*http.Request, error) {
return func() (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
@@ -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.ReaderIOEither[*http.Request]](builder.GetTargetURL()),
E.Flap[error, RIOE.ReaderIOEither[*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

@@ -21,7 +21,7 @@ import (
"net/url"
"testing"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
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"

View File

@@ -0,0 +1,15 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:117.52,119.103 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:119.103,120.80 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:120.80,121.41 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:121.41,123.19 2 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:123.19,126.6 2 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:127.5,127.20 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:132.2,132.93 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:132.93,133.80 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:133.80,134.41 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:134.41,136.19 2 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:136.19,138.6 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:139.5,139.20 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:144.2,150.50 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/builder/builder.go:150.50,153.4 2 1

View File

@@ -0,0 +1,11 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:111.76,116.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:134.49,136.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:161.90,162.65 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:162.65,166.76 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:166.76,176.5 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:198.73,203.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:222.74,227.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:234.76,236.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:245.74,254.2 1 1
github.com/IBM/fp-go/v2/context/readerioresult/http/request.go:281.76,286.2 1 1

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package http provides functional HTTP client utilities built on top of ReaderIOEither monad.
// Package http provides functional HTTP client utilities built on top of ReaderIOResult monad.
// It offers a composable way to make HTTP requests with context support, error handling,
// and response parsing capabilities. The package follows functional programming principles
// to ensure type-safe, testable, and maintainable HTTP operations.
@@ -36,7 +36,7 @@ import (
"net/http"
B "github.com/IBM/fp-go/v2/bytes"
RIOE "github.com/IBM/fp-go/v2/context/readerioeither"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
H "github.com/IBM/fp-go/v2/http"
IOE "github.com/IBM/fp-go/v2/ioeither"
@@ -50,13 +50,13 @@ type (
// It represents a computation that, given a context, produces either an error
// or an HTTP request. This allows for composable request building with proper
// error handling and context propagation.
Requester = RIOE.ReaderIOEither[*http.Request]
Requester = RIOE.ReaderIOResult[*http.Request]
// Client is an interface for executing HTTP requests in a functional way.
// It wraps the standard http.Client and provides a Do method that works
// with the ReaderIOEither monad for composable, type-safe HTTP operations.
// with the ReaderIOResult monad for composable, type-safe HTTP operations.
Client interface {
// Do executes an HTTP request and returns the response wrapped in a ReaderIOEither.
// Do executes an HTTP request and returns the response wrapped in a ReaderIOResult.
// It takes a Requester (which builds the request) and returns a computation that,
// when executed with a context, performs the HTTP request and returns either
// an error or the HTTP response.
@@ -65,15 +65,15 @@ type (
// - req: A Requester that builds the HTTP request
//
// Returns:
// - A ReaderIOEither that produces either an error or an *http.Response
Do(Requester) RIOE.ReaderIOEither[*http.Response]
// - A ReaderIOResult that produces either an error or an *http.Response
Do(Requester) RIOE.ReaderIOResult[*http.Response]
}
// client is the internal implementation of the Client interface.
// It wraps a standard http.Client and provides functional HTTP operations.
client struct {
delegate *http.Client
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
doIOE IOE.Kleisli[error, *http.Request, *http.Response]
}
)
@@ -108,7 +108,7 @@ var (
MakeGetRequest = makeRequest("GET", nil)
)
func (client client) Do(req Requester) RIOE.ReaderIOEither[*http.Response] {
func (client client) Do(req Requester) RIOE.ReaderIOResult[*http.Response] {
return F.Pipe1(
req,
RIOE.ChainIOEitherK(client.doIOE),
@@ -117,7 +117,7 @@ func (client client) Do(req Requester) RIOE.ReaderIOEither[*http.Response] {
// MakeClient creates a functional HTTP client wrapper around a standard http.Client.
// The returned Client provides methods for executing HTTP requests in a functional,
// composable way using the ReaderIOEither monad.
// composable way using the ReaderIOResult monad.
//
// Parameters:
// - httpClient: A standard *http.Client to wrap (e.g., http.DefaultClient)
@@ -149,7 +149,7 @@ func MakeClient(httpClient *http.Client) Client {
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[FullResponse]
// - A function that takes a Requester and returns a ReaderIOResult[FullResponse]
// where FullResponse is a tuple of (*http.Response, []byte)
//
// Example:
@@ -158,8 +158,8 @@ func MakeClient(httpClient *http.Client) Client {
// request := MakeGetRequest("https://api.example.com/data")
// fullResp := ReadFullResponse(client)(request)
// result := fullResp(context.Background())()
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullResponse] {
return func(req Requester) RIOE.ReaderIOEither[H.FullResponse] {
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
return F.Flow3(
client.Do(req),
IOE.ChainEitherK(H.ValidateResponse),
@@ -186,7 +186,7 @@ func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullR
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[[]byte]
// - A function that takes a Requester and returns a ReaderIOResult[[]byte]
// containing the response body as bytes
//
// Example:
@@ -195,7 +195,7 @@ func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullR
// request := MakeGetRequest("https://api.example.com/data")
// readBytes := ReadAll(client)
// result := readBytes(request)(context.Background())()
func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
return F.Flow2(
ReadFullResponse(client),
RIOE.Map(H.Body),
@@ -210,7 +210,7 @@ func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[string]
// - A function that takes a Requester and returns a ReaderIOResult[string]
// containing the response body as a string
//
// Example:
@@ -219,7 +219,7 @@ func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
// request := MakeGetRequest("https://api.example.com/text")
// readText := ReadText(client)
// result := readText(request)(context.Background())()
func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
return F.Flow2(
ReadAll(client),
RIOE.Map(B.ToString),
@@ -231,7 +231,7 @@ func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
// but will be removed in a future version. The capitalized version follows Go naming
// conventions for acronyms.
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
return ReadJSON[A](client)
}
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
// 3. Reads the response body as bytes
//
// This function is used internally by ReadJSON to ensure proper JSON response handling.
func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
return F.Flow3(
ReadFullResponse(client),
RIOE.ChainFirstEitherK(F.Flow2(
@@ -264,7 +264,7 @@ func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[A]
// - A function that takes a Requester and returns a ReaderIOResult[A]
// containing the parsed JSON data
//
// Example:
@@ -278,7 +278,7 @@ func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
// request := MakeGetRequest("https://api.example.com/user/1")
// readUser := ReadJSON[User](client)
// result := readUser(request)(context.Background())()
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
return F.Flow2(
readJSON(client),
RIOE.ChainEitherK(J.Unmarshal[A]),

View File

@@ -22,7 +22,7 @@ import (
H "net/http"
R "github.com/IBM/fp-go/v2/context/readerioeither"
R "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function"
@@ -66,7 +66,7 @@ func (b simpleRequestBuilder) WithHeader(key, value string) simpleRequestBuilder
return b
}
func (b simpleRequestBuilder) Build() R.ReaderIOEither[*H.Request] {
func (b simpleRequestBuilder) Build() R.ReaderIOResult[*H.Request] {
return func(ctx context.Context) IOE.IOEither[error, *H.Request] {
return IOE.TryCatchError(func() (*H.Request, error) {
req, err := H.NewRequestWithContext(ctx, b.method, b.url, nil)

View File

@@ -0,0 +1,726 @@
// 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 readerioresult provides logging utilities for ReaderIOResult computations.
// It includes functions for entry/exit logging with timing, correlation IDs, and context management.
package readerioresult
import (
"context"
"log/slog"
"sync/atomic"
"time"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/logging"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
type (
// loggingContextKeyType is the type used as a key for storing logging information in context.Context
loggingContextKeyType int
// LoggingID is a unique identifier assigned to each logged operation for correlation
LoggingID uint64
// loggingContext holds the logging state for a computation, including timing,
// correlation ID, logger instance, and whether logging is enabled.
loggingContext struct {
contextID LoggingID // Unique identifier for this logged operation
startTime time.Time // When the operation started (for duration calculation)
logger *slog.Logger // The logger instance to use for this operation
isEnabled bool // Whether logging is enabled for this operation
}
)
var (
// loggingContextKey is the singleton key used to store/retrieve logging data from context
loggingContextKey loggingContextKeyType
// loggingCounter is an atomic counter that generates unique LoggingIDs
loggingCounter atomic.Uint64
loggingContextValue = function.Bind2nd(context.Context.Value, any(loggingContextKey))
withLoggingContextValue = function.Bind2of3(context.WithValue)(any(loggingContextKey))
// getLoggingContext retrieves the logging information (start time and ID) from the context.
// It returns a Pair containing the start time and the logging ID.
// This function assumes the context contains logging information; it will panic if not present.
getLoggingContext = function.Flow3(
loggingContextValue,
option.ToType[loggingContext],
option.GetOrElse(getDefaultLoggingContext),
)
)
// getDefaultLoggingContext returns a default logging context with the global logger.
// This is used when no logging context is found in the context.Context.
func getDefaultLoggingContext() loggingContext {
return loggingContext{
logger: logging.GetLogger(),
}
}
// withLoggingContext creates an endomorphism that adds a logging context to a context.Context.
// This is used internally to store logging state in the context for retrieval by nested operations.
//
// Parameters:
// - lctx: The logging context to store
//
// Returns:
// - An endomorphism that adds the logging context to a context.Context
func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
return function.Bind2nd(withLoggingContextValue, any(lctx))
}
// LogEntryExitF creates a customizable operator that wraps a ReaderIOResult computation with entry/exit callbacks.
//
// This is a more flexible version of LogEntryExit that allows you to provide custom callbacks for
// entry and exit events. The onEntry callback receives the current context and can return a modified
// context (e.g., with additional logging information). The onExit callback receives the computation
// result and can perform custom logging, metrics collection, or cleanup.
//
// The function uses the bracket pattern to ensure that:
// - The onEntry callback is executed before the computation starts
// - The computation runs with the context returned by onEntry
// - The onExit callback is executed after the computation completes (success or failure)
// - The original result is preserved and returned unchanged
// - Cleanup happens even if the computation fails
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
// - ANY: The return type of the onExit callback (typically any)
//
// Parameters:
// - onEntry: A ReaderIO that receives the current context and returns a (possibly modified) context.
// This is executed before the computation starts. Use this for logging entry, adding context values,
// starting timers, or initialization logic.
// - onExit: A Kleisli function that receives the Result[A] and returns a ReaderIO[ANY].
// This is executed after the computation completes, regardless of success or failure.
// Use this for logging exit, recording metrics, cleanup, or finalization logic.
//
// Returns:
// - An Operator that wraps the ReaderIOResult computation with the custom entry/exit callbacks
//
// Example with custom context modification:
//
// type RequestID string
//
// logOp := LogEntryExitF[User, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// reqID := RequestID(uuid.New().String())
// log.Printf("[%s] Starting operation", reqID)
// return context.WithValue(ctx, "requestID", reqID)
// }
// },
// func(res Result[User]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// reqID := ctx.Value("requestID").(RequestID)
// return function.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// log.Printf("[%s] Operation failed: %v", reqID, err)
// return nil
// },
// func(_ User) any {
// log.Printf("[%s] Operation succeeded", reqID)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// wrapped := logOp(fetchUser(123))
//
// Example with metrics collection:
//
// import "github.com/prometheus/client_golang/prometheus"
//
// metricsOp := LogEntryExitF[Response, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// requestCount.WithLabelValues("api_call", "started").Inc()
// return context.WithValue(ctx, "startTime", time.Now())
// }
// },
// func(res Result[Response]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// startTime := ctx.Value("startTime").(time.Time)
// duration := time.Since(startTime).Seconds()
//
// return function.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// requestCount.WithLabelValues("api_call", "error").Inc()
// requestDuration.WithLabelValues("api_call", "error").Observe(duration)
// return nil
// },
// func(_ Response) any {
// requestCount.WithLabelValues("api_call", "success").Inc()
// requestDuration.WithLabelValues("api_call", "success").Observe(duration)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// Use Cases:
// - Custom context modification: Adding request IDs, trace IDs, or other context values
// - Structured logging: Integration with zap, logrus, or other structured loggers
// - Metrics collection: Recording operation durations, success/failure rates
// - Distributed tracing: OpenTelemetry, Jaeger integration
// - Custom monitoring: Application-specific monitoring and alerting
//
// Note: LogEntryExit is implemented using LogEntryExitF with standard logging and context management.
// Use LogEntryExitF when you need more control over the entry/exit behavior or context modification.
func LogEntryExitF[A, ANY any](
onEntry ReaderIO[context.Context],
onExit readerio.Kleisli[Result[A], ANY],
) Operator[A, A] {
bracket := function.Bind13of3(readerio.Bracket[context.Context, Result[A], ANY])(onEntry, func(newCtx context.Context, res Result[A]) ReaderIO[ANY] {
return readerio.FromIO(onExit(res)(newCtx)) // Get the exit callback for this result
})
return func(src ReaderIOResult[A]) ReaderIOResult[A] {
return bracket(function.Flow2(
src,
FromIOResult,
))
}
}
// onEntry creates a ReaderIO that handles the entry logging for an operation.
// It generates a unique logging ID, captures the start time, and logs the entry message.
// The logging context is stored in the context.Context for later retrieval.
//
// Parameters:
// - logLevel: The slog.Level to use for logging (e.g., slog.LevelInfo, slog.LevelDebug)
// - cb: Callback function to retrieve the logger from the context
// - nameAttr: The slog.Attr containing the operation name
//
// Returns:
// - A ReaderIO that prepares the context with logging information and logs the entry
func onEntry(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
nameAttr slog.Attr,
) ReaderIO[context.Context] {
return func(ctx context.Context) IO[context.Context] {
// logger
logger := cb(ctx)
return func() context.Context {
// check if the logger is enabled
if logger.Enabled(ctx, logLevel) {
// Generate unique logging ID and capture start time
contextID := LoggingID(loggingCounter.Add(1))
startTime := time.Now()
newLogger := logger.With("ID", contextID)
// log using ID
newLogger.LogAttrs(ctx, logLevel, "[entering]", nameAttr)
withCtx := withLoggingContext(loggingContext{
contextID: contextID,
startTime: startTime,
logger: newLogger,
isEnabled: true,
})
withLogger := logging.WithLogger(newLogger)
return withCtx(withLogger(ctx))
}
// logging disabled
withCtx := withLoggingContext(loggingContext{
logger: logger,
isEnabled: false,
})
return withCtx(ctx)
}
}
}
// onExitAny creates a Kleisli function that handles exit logging for an operation.
// It logs either success or error based on the Result, including the operation duration.
// Only logs if logging was enabled during entry (checked via loggingContext.isEnabled).
//
// Parameters:
// - logLevel: The slog.Level to use for logging
// - nameAttr: The slog.Attr containing the operation name
//
// Returns:
// - A Kleisli function that logs the exit/error and returns nil
func onExitAny(
logLevel slog.Level,
nameAttr slog.Attr,
) readerio.Kleisli[Result[any], any] {
return func(res Result[any]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
value := getLoggingContext(ctx)
if value.isEnabled {
return func() any {
// Retrieve logging information from context
durationAttr := slog.Duration("duration", time.Since(value.startTime))
// Log error with ID and duration
onError := func(err error) any {
value.logger.LogAttrs(ctx, logLevel, "[throwing]",
nameAttr,
durationAttr,
slog.Any("error", err))
return nil
}
// Log success with ID and duration
onSuccess := func(_ any) any {
value.logger.LogAttrs(ctx, logLevel, "[exiting ]", nameAttr, durationAttr)
return nil
}
return function.Pipe1(
res,
result.Fold(onError, onSuccess),
)
}
}
// nothing to do
return io.Of[any](nil)
}
}
}
// LogEntryExitWithCallback creates an operator that logs entry and exit of a ReaderIOResult computation
// using a custom logger callback and log level. This provides more control than LogEntryExit.
//
// This function allows you to:
// - Use a custom log level (Debug, Info, Warn, Error)
// - Retrieve the logger from the context using a custom callback
// - Control whether logging is enabled based on the logger's configuration
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
//
// Parameters:
// - logLevel: The slog.Level to use for all log messages (entry, exit, error)
// - cb: Callback function to retrieve the *slog.Logger from the context
// - name: A descriptive name for the operation
//
// Returns:
// - An Operator that wraps the ReaderIOResult with customizable logging
//
// Example with custom log level:
//
// // Log at debug level
// debugOp := LogEntryExitWithCallback[User](
// slog.LevelDebug,
// logging.GetLoggerFromContext,
// "fetchUser",
// )
// result := debugOp(fetchUser(123))
//
// Example with custom logger callback:
//
// type loggerKey int
// const myLoggerKey loggerKey = 0
//
// getMyLogger := func(ctx context.Context) *slog.Logger {
// if logger := ctx.Value(myLoggerKey); logger != nil {
// return logger.(*slog.Logger)
// }
// return slog.Default()
// }
//
// customOp := LogEntryExitWithCallback[Data](
// slog.LevelInfo,
// getMyLogger,
// "processData",
// )
func LogEntryExitWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
name string) Operator[A, A] {
nameAttr := slog.String("name", name)
return LogEntryExitF(
onEntry(logLevel, cb, nameAttr),
function.Flow2(
result.MapTo[A, any](nil),
onExitAny(logLevel, nameAttr),
),
)
}
// LogEntryExit creates an operator that logs the entry and exit of a ReaderIOResult computation with timing and correlation IDs.
//
// This function wraps a ReaderIOResult computation with automatic logging that tracks:
// - Entry: Logs when the computation starts with "[entering <id>] <name>"
// - Exit: Logs when the computation completes successfully with "[exiting <id>] <name> [duration]"
// - Error: Logs when the computation fails with "[throwing <id>] <name> [duration]: <error>"
//
// Each logged operation is assigned a unique LoggingID (a monotonically increasing counter) that
// appears in all log messages for that operation. This ID enables correlation of entry and exit
// logs, even when multiple operations are running concurrently or are interleaved.
//
// The logging information (start time and ID) is stored in the context and can be retrieved using
// getLoggingContext or getLoggingID. This allows nested operations to access the parent operation's
// logging information.
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
//
// Parameters:
// - name: A descriptive name for the computation, used in log messages to identify the operation
//
// Returns:
// - An Operator that wraps the ReaderIOResult computation with entry/exit logging
//
// The function uses the bracket pattern to ensure that:
// - Entry is logged before the computation starts
// - A unique LoggingID is assigned and stored in the context
// - Exit/error is logged after the computation completes, regardless of success or failure
// - Timing is accurate, measuring from entry to exit
// - The original result is preserved and returned unchanged
//
// Log Format:
// - Entry: "[entering <id>] <name>"
// - Success: "[exiting <id>] <name> [<duration>s]"
// - Error: "[throwing <id>] <name> [<duration>s]: <error>"
//
// Example with successful computation:
//
// fetchUser := func(id int) ReaderIOResult[User] {
// return Of(User{ID: id, Name: "Alice"})
// }
//
// // Wrap with logging
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
//
// // Execute
// result := loggedFetch(context.Background())()
// // Logs:
// // [entering 1] fetchUser
// // [exiting 1] fetchUser [0.1s]
//
// Example with error:
//
// failingOp := func() ReaderIOResult[string] {
// return Left[string](errors.New("connection timeout"))
// }
//
// logged := LogEntryExit[string]("failingOp")(failingOp())
// result := logged(context.Background())()
// // Logs:
// // [entering 2] failingOp
// // [throwing 2] failingOp [0.0s]: connection timeout
//
// Example with nested operations:
//
// fetchOrders := func(userID int) ReaderIOResult[[]Order] {
// return Of([]Order{{ID: 1}})
// }
//
// pipeline := F.Pipe3(
// fetchUser(123),
// LogEntryExit[User]("fetchUser"),
// Chain(func(user User) ReaderIOResult[[]Order] {
// return fetchOrders(user.ID)
// }),
// LogEntryExit[[]Order]("fetchOrders"),
// )
//
// result := pipeline(context.Background())()
// // Logs:
// // [entering 3] fetchUser
// // [exiting 3] fetchUser [0.1s]
// // [entering 4] fetchOrders
// // [exiting 4] fetchOrders [0.2s]
//
// Example with concurrent operations:
//
// // Multiple operations can run concurrently, each with unique IDs
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
//
// go op1(context.Background())()
// go op2(context.Background())()
// // Logs (order may vary):
// // [entering 5] operation1
// // [entering 6] operation2
// // [exiting 5] operation1 [0.1s]
// // [exiting 6] operation2 [0.2s]
// // The IDs allow correlation even when logs are interleaved
//
// Use Cases:
// - Debugging: Track execution flow through complex ReaderIOResult chains with correlation IDs
// - Performance monitoring: Identify slow operations with timing information
// - Production logging: Monitor critical operations with unique identifiers
// - Concurrent operations: Correlate logs from multiple concurrent operations
// - Nested operations: Track parent-child relationships in operation hierarchies
// - Troubleshooting: Quickly identify where errors occur and correlate with entry logs
//
//go:inline
func LogEntryExit[A any](name string) Operator[A, A] {
return LogEntryExitWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, name)
}
// SLogWithCallback creates a Kleisli arrow that logs a Result value (success or error) with a custom logger and log level.
//
// This function logs both successful values and errors, making it useful for debugging and monitoring
// Result values as they flow through a computation. Unlike TapSLog which only logs successful values,
// SLogWithCallback logs the Result regardless of whether it contains a value or an error.
//
// The logged output includes:
// - For success: The message with the value as a structured "value" attribute
// - For error: The message with the error as a structured "error" attribute
//
// The Result is passed through unchanged after logging.
//
// Type Parameters:
// - A: The success type of the Result
//
// Parameters:
// - logLevel: The slog.Level to use for logging (e.g., slog.LevelInfo, slog.LevelDebug)
// - cb: Callback function to retrieve the *slog.Logger from the context
// - message: A descriptive message to include in the log entry
//
// Returns:
// - A Kleisli arrow that logs the Result (value or error) and returns it unchanged
//
// Example with custom log level:
//
// debugLog := SLogWithCallback[User](
// slog.LevelDebug,
// logging.GetLoggerFromContext,
// "User result",
// )
//
// pipeline := F.Pipe2(
// fetchUser(123),
// Chain(debugLog),
// Map(func(u User) string { return u.Name }),
// )
//
// Example with custom logger:
//
// type loggerKey int
// const myLoggerKey loggerKey = 0
//
// getMyLogger := func(ctx context.Context) *slog.Logger {
// if logger := ctx.Value(myLoggerKey); logger != nil {
// return logger.(*slog.Logger)
// }
// return slog.Default()
// }
//
// customLog := SLogWithCallback[Data](
// slog.LevelWarn,
// getMyLogger,
// "Data processing result",
// )
//
// Use Cases:
// - Debugging: Log both successful and failed Results in a pipeline
// - Error tracking: Monitor error occurrences with custom log levels
// - Custom logging: Use application-specific loggers and log levels
// - Conditional logging: Enable/disable logging based on logger configuration
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) readerio.Kleisli[Result[A], Result[A]] {
return func(ma Result[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
// logger
logger := cb(ctx)
return func() Result[A] {
return result.MonadFold(
ma,
func(e error) Result[A] {
logger.LogAttrs(ctx, logLevel, message, slog.Any("error", e))
return ma
},
func(a A) Result[A] {
logger.LogAttrs(ctx, logLevel, message, slog.Any("value", a))
return ma
},
)
}
}
}
}
// SLog creates a Kleisli arrow that logs a Result value (success or error) with a message.
//
// This function logs both successful values and errors at Info level using the logger from the context.
// It's a convenience wrapper around SLogWithCallback with standard settings.
//
// The logged output includes:
// - For success: The message with the value as a structured "value" attribute
// - For error: The message with the error as a structured "error" attribute
//
// The Result is passed through unchanged after logging, making this function transparent in the
// computation pipeline.
//
// Type Parameters:
// - A: The success type of the Result
//
// Parameters:
// - message: A descriptive message to include in the log entry
//
// Returns:
// - A Kleisli arrow that logs the Result (value or error) and returns it unchanged
//
// Example with successful Result:
//
// pipeline := F.Pipe2(
// fetchUser(123),
// Chain(SLog[User]("Fetched user")),
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
// // If error, logs: "Fetched user" error="user not found"
//
// Example in error handling pipeline:
//
// pipeline := F.Pipe3(
// fetchData(id),
// Chain(SLog[Data]("Data fetched")),
// Chain(validateData),
// Chain(SLog[Data]("Data validated")),
// Chain(processData),
// )
//
// // Logs each step, including errors:
// // "Data fetched" value={...} or error="..."
// // "Data validated" value={...} or error="..."
//
// Use Cases:
// - Debugging: Track both successful and failed Results in a pipeline
// - Error monitoring: Log errors as they occur in the computation
// - Flow tracking: See the progression of Results through a pipeline
// - Troubleshooting: Identify where errors are introduced or propagated
//
// Note: This function logs the Result itself (which may contain an error), not just successful values.
// For logging only successful values, use TapSLog instead.
//
//go:inline
func SLog[A any](message string) readerio.Kleisli[Result[A], Result[A]] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}
// TapSLog creates an operator that logs only successful values with a message and passes them through unchanged.
//
// This function is useful for debugging and monitoring values as they flow through a ReaderIOResult
// computation chain. Unlike SLog which logs both successes and errors, TapSLog only logs when the
// computation is successful. If the computation contains an error, no logging occurs and the error
// is propagated unchanged.
//
// The logged output includes:
// - The provided message
// - The value being passed through (as a structured "value" attribute)
//
// Type Parameters:
// - A: The type of the value to log and pass through
//
// Parameters:
// - message: A descriptive message to include in the log entry
//
// Returns:
// - An Operator that logs successful values and returns them unchanged
//
// Example with simple value logging:
//
// fetchUser := func(id int) ReaderIOResult[User] {
// return Of(User{ID: id, Name: "Alice"})
// }
//
// pipeline := F.Pipe2(
// fetchUser(123),
// TapSLog[User]("Fetched user"),
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Returns: result.Of("Alice")
//
// Example in a processing pipeline:
//
// processOrder := F.Pipe4(
// fetchOrder(orderId),
// TapSLog[Order]("Order fetched"),
// Chain(validateOrder),
// TapSLog[Order]("Order validated"),
// Chain(processPayment),
// TapSLog[Payment]("Payment processed"),
// )
//
// result := processOrder(context.Background())()
// // Logs each successful step with the intermediate values
// // If any step fails, subsequent TapSLog calls don't log
//
// Example with error handling:
//
// pipeline := F.Pipe3(
// fetchData(id),
// TapSLog[Data]("Data fetched"),
// Chain(func(d Data) ReaderIOResult[Result] {
// if d.IsValid() {
// return Of(processData(d))
// }
// return Left[Result](errors.New("invalid data"))
// }),
// TapSLog[Result]("Data processed"),
// )
//
// // If fetchData succeeds: logs "Data fetched" with the data
// // If processing succeeds: logs "Data processed" with the result
// // If processing fails: "Data processed" is NOT logged (error propagates)
//
// Use Cases:
// - Debugging: Inspect intermediate successful values in a computation pipeline
// - Monitoring: Track successful data flow through complex operations
// - Troubleshooting: Identify where successful computations stop (last logged value before error)
// - Auditing: Log important successful values for compliance or security
// - Development: Understand data transformations during development
//
// Note: This function only logs successful values. Errors are silently propagated without logging.
// For logging both successes and errors, use SLog instead.
//
//go:inline
func TapSLog[A any](message string) Operator[A, A] {
return readerio.ChainFirst(SLog[A](message))
}

View File

@@ -0,0 +1,662 @@
package readerioresult
import (
"bytes"
"context"
"errors"
"log/slog"
"strconv"
"strings"
"testing"
"time"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/logging"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestLoggingContext tests basic nested logging with correlation IDs
func TestLoggingContext(t *testing.T) {
data := F.Pipe2(
Of("Sample"),
LogEntryExit[string]("TestLoggingContext1"),
LogEntryExit[string]("TestLoggingContext2"),
)
assert.Equal(t, result.Of("Sample"), data(context.Background())())
}
// TestLogEntryExitSuccess tests successful operation logging
func TestLogEntryExitSuccess(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe1(
Of("success value"),
LogEntryExit[string]("TestOperation"),
)
res := operation(context.Background())()
assert.Equal(t, result.Of("success value"), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[exiting ]")
assert.Contains(t, logOutput, "TestOperation")
assert.Contains(t, logOutput, "ID=")
assert.Contains(t, logOutput, "duration=")
}
// TestLogEntryExitError tests error operation logging
func TestLogEntryExitError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
testErr := errors.New("test error")
operation := F.Pipe1(
Left[string](testErr),
LogEntryExit[string]("FailingOperation"),
)
res := operation(context.Background())()
assert.True(t, result.IsLeft(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[throwing]")
assert.Contains(t, logOutput, "FailingOperation")
assert.Contains(t, logOutput, "test error")
assert.Contains(t, logOutput, "ID=")
assert.Contains(t, logOutput, "duration=")
}
// TestLogEntryExitNested tests nested operations with different IDs
func TestLogEntryExitNested(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
innerOp := F.Pipe1(
Of("inner"),
LogEntryExit[string]("InnerOp"),
)
outerOp := F.Pipe2(
Of("outer"),
LogEntryExit[string]("OuterOp"),
Chain(func(s string) ReaderIOResult[string] {
return innerOp
}),
)
res := outerOp(context.Background())()
assert.True(t, result.IsRight(res))
logOutput := buf.String()
// Should have two different IDs
assert.Contains(t, logOutput, "OuterOp")
assert.Contains(t, logOutput, "InnerOp")
// Count entering and exiting logs
enterCount := strings.Count(logOutput, "[entering]")
exitCount := strings.Count(logOutput, "[exiting ]")
assert.Equal(t, 2, enterCount, "Should have 2 entering logs")
assert.Equal(t, 2, exitCount, "Should have 2 exiting logs")
}
// TestLogEntryExitWithCallback tests custom log level and callback
func TestLogEntryExitWithCallback(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
operation := F.Pipe1(
Of(42),
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
)
res := operation(context.Background())()
assert.Equal(t, result.Of(42), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[exiting ]")
assert.Contains(t, logOutput, "DebugOperation")
assert.Contains(t, logOutput, "level=DEBUG")
}
// TestLogEntryExitDisabled tests that logging can be disabled
func TestLogEntryExitDisabled(t *testing.T) {
var buf bytes.Buffer
// Create logger with level that disables info logs
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelError, // Only log errors
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe1(
Of("value"),
LogEntryExit[string]("DisabledOperation"),
)
res := operation(context.Background())()
assert.True(t, result.IsRight(res))
// Should have no logs since level is ERROR
logOutput := buf.String()
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
}
// TestLogEntryExitF tests custom entry/exit callbacks
func TestLogEntryExitF(t *testing.T) {
var entryCount, exitCount int
onEntry := func(ctx context.Context) IO[context.Context] {
return func() context.Context {
entryCount++
return ctx
}
}
onExit := func(res Result[string]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
return func() any {
exitCount++
return nil
}
}
}
operation := F.Pipe1(
Of("test"),
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
assert.True(t, result.IsRight(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
}
// TestLogEntryExitFWithError tests custom callbacks with error
func TestLogEntryExitFWithError(t *testing.T) {
var entryCount, exitCount int
var capturedError error
onEntry := func(ctx context.Context) IO[context.Context] {
return func() context.Context {
entryCount++
return ctx
}
}
onExit := func(res Result[string]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
return func() any {
exitCount++
if result.IsLeft(res) {
_, capturedError = result.Unwrap(res)
}
return nil
}
}
}
testErr := errors.New("custom error")
operation := F.Pipe1(
Left[string](testErr),
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
assert.True(t, result.IsLeft(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
assert.Equal(t, testErr, capturedError, "Should capture the error")
}
// TestLoggingIDUniqueness tests that logging IDs are unique
func TestLoggingIDUniqueness(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
// Run multiple operations
for i := range 5 {
op := F.Pipe1(
Of(i),
LogEntryExit[int]("Operation"),
)
op(context.Background())()
}
logOutput := buf.String()
// Extract all IDs and verify they're unique
lines := strings.Split(logOutput, "\n")
ids := make(map[string]bool)
for _, line := range lines {
if strings.Contains(line, "ID=") {
// Extract ID value
parts := strings.Split(line, "ID=")
if len(parts) > 1 {
idPart := strings.Fields(parts[1])[0]
ids[idPart] = true
}
}
}
// Should have 5 unique IDs (one per operation)
assert.GreaterOrEqual(t, len(ids), 5, "Should have at least 5 unique IDs")
}
// TestLogEntryExitWithContextLogger tests using logger from context
func TestLogEntryExitWithContextLogger(t *testing.T) {
var buf bytes.Buffer
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
operation := F.Pipe1(
Of("context value"),
LogEntryExit[string]("ContextOperation"),
)
res := operation(ctx)()
assert.True(t, result.IsRight(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[exiting ]")
assert.Contains(t, logOutput, "ContextOperation")
}
// TestLogEntryExitTiming tests that duration is captured
func TestLogEntryExitTiming(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
// Operation with delay
slowOp := func(ctx context.Context) IOResult[string] {
return func() Result[string] {
time.Sleep(10 * time.Millisecond)
return result.Of("done")
}
}
operation := F.Pipe1(
slowOp,
LogEntryExit[string]("SlowOperation"),
)
res := operation(context.Background())()
assert.True(t, result.IsRight(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "duration=")
// Verify duration is present in exit log
lines := strings.Split(logOutput, "\n")
var foundDuration bool
for _, line := range lines {
if strings.Contains(line, "[exiting ]") && strings.Contains(line, "duration=") {
foundDuration = true
break
}
}
assert.True(t, foundDuration, "Exit log should contain duration")
}
// TestLogEntryExitChainedOperations tests complex chained operations
func TestLogEntryExitChainedOperations(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
step1 := F.Pipe1(
Of(1),
LogEntryExit[int]("Step1"),
)
step2 := F.Flow3(
N.Mul(2),
Of,
LogEntryExit[int]("Step2"),
)
step3 := F.Flow3(
strconv.Itoa,
Of,
LogEntryExit[string]("Step3"),
)
pipeline := F.Pipe1(
step1,
Chain(F.Flow2(
step2,
Chain(step3),
)),
)
res := pipeline(context.Background())()
assert.Equal(t, result.Of("2"), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Step1")
assert.Contains(t, logOutput, "Step2")
assert.Contains(t, logOutput, "Step3")
// Verify all steps completed
assert.Equal(t, 3, strings.Count(logOutput, "[entering]"))
assert.Equal(t, 3, strings.Count(logOutput, "[exiting ]"))
}
// TestTapSLog tests basic TapSLog functionality
func TestTapSLog(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe2(
Of(42),
TapSLog[int]("Processing value"),
Map(N.Mul(2)),
)
res := operation(context.Background())()
assert.Equal(t, result.Of(84), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Processing value")
assert.Contains(t, logOutput, "value=42")
}
// TestTapSLogInPipeline tests TapSLog in a multi-step pipeline
func TestTapSLogInPipeline(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
step1 := F.Pipe2(
Of("hello"),
TapSLog[string]("Step 1: Initial value"),
Map(func(s string) string { return s + " world" }),
)
step2 := F.Pipe2(
step1,
TapSLog[string]("Step 2: After concatenation"),
Map(S.Size),
)
pipeline := F.Pipe1(
step2,
TapSLog[int]("Step 3: Final length"),
)
res := pipeline(context.Background())()
assert.Equal(t, result.Of(11), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Step 1: Initial value")
assert.Contains(t, logOutput, "value=hello")
assert.Contains(t, logOutput, "Step 2: After concatenation")
assert.Contains(t, logOutput, `value="hello world"`)
assert.Contains(t, logOutput, "Step 3: Final length")
assert.Contains(t, logOutput, "value=11")
}
// TestTapSLogWithError tests that TapSLog logs errors (via SLog)
func TestTapSLogWithError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
testErr := errors.New("computation failed")
pipeline := F.Pipe2(
Left[int](testErr),
TapSLog[int]("Error logged"),
Map(N.Mul(2)),
)
res := pipeline(context.Background())()
assert.True(t, result.IsLeft(res))
logOutput := buf.String()
// TapSLog uses SLog internally, which logs both successes and errors
assert.Contains(t, logOutput, "Error logged")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "computation failed")
}
// TestTapSLogWithStruct tests TapSLog with structured data
func TestTapSLogWithStruct(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
type User struct {
ID int
Name string
}
user := User{ID: 123, Name: "Alice"}
operation := F.Pipe2(
Of(user),
TapSLog[User]("User data"),
Map(func(u User) string { return u.Name }),
)
res := operation(context.Background())()
assert.Equal(t, result.Of("Alice"), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "User data")
assert.Contains(t, logOutput, "ID:123")
assert.Contains(t, logOutput, "Name:Alice")
}
// TestTapSLogDisabled tests that TapSLog respects logger level
func TestTapSLogDisabled(t *testing.T) {
var buf bytes.Buffer
// Create logger with level that disables info logs
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelError, // Only log errors
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe2(
Of(42),
TapSLog[int]("This should not be logged"),
Map(N.Mul(2)),
)
res := operation(context.Background())()
assert.Equal(t, result.Of(84), res)
// Should have no logs since level is ERROR
logOutput := buf.String()
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
}
// TestTapSLogWithContextLogger tests TapSLog using logger from context
func TestTapSLogWithContextLogger(t *testing.T) {
var buf bytes.Buffer
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
operation := F.Pipe2(
Of("test value"),
TapSLog[string]("Context logger test"),
Map(S.Size),
)
res := operation(ctx)()
assert.Equal(t, result.Of(10), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Context logger test")
assert.Contains(t, logOutput, `value="test value"`)
}
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
func TestSLogLogsSuccessValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// Create a Result and log it
res1 := result.Of(42)
logged := SLog[int]("Result value")(res1)(ctx)()
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "value=42")
}
// TestSLogLogsErrorValue tests that SLog logs error Result values
func TestSLogLogsErrorValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
testErr := errors.New("test error")
// Create an error Result and log it
res1 := result.Left[int](testErr)
logged := SLog[int]("Result value")(res1)(ctx)()
assert.True(t, result.IsLeft(logged))
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "test error")
}
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
func TestSLogWithCallbackCustomLevel(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
// Create a Result and log it with custom callback
res1 := result.Of(42)
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)()
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Debug result")
assert.Contains(t, logOutput, "value=42")
assert.Contains(t, logOutput, "level=DEBUG")
}
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
func TestSLogWithCallbackLogsError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelWarn,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
testErr := errors.New("warning error")
// Create an error Result and log it with custom callback
res1 := result.Left[int](testErr)
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)()
assert.True(t, result.IsLeft(logged))
logOutput := buf.String()
assert.Contains(t, logOutput, "Warning result")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "warning error")
assert.Contains(t, logOutput, "level=WARN")
}

View File

@@ -13,96 +13,75 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/monoid"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
type (
Monoid[A any] = monoid.Monoid[ReaderIOEither[A]]
Monoid[A any] = monoid.Monoid[ReaderIOResult[A]]
)
// ApplicativeMonoid returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
// ApplicativeMonoid returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
// This uses the default applicative behavior (parallel or sequential based on useParallel flag).
//
// The monoid combines two ReaderIOEither values by applying the underlying monoid's combine operation
// The monoid combines two ReaderIOResult values by applying the underlying monoid's combine operation
// to their success values using applicative application.
//
// Parameters:
// - m: The underlying monoid for type A
//
// Returns a Monoid for ReaderIOEither[A].
// Returns a Monoid for ReaderIOResult[A].
func ApplicativeMonoid[A any](m monoid.Monoid[A]) Monoid[A] {
return monoid.ApplicativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadAp[A, A],
m,
)
return RIOR.ApplicativeMonoid[context.Context](m)
}
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
// ApplicativeMonoidSeq returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
// This explicitly uses sequential execution for combining values.
//
// Parameters:
// - m: The underlying monoid for type A
//
// Returns a Monoid for ReaderIOEither[A] with sequential execution.
// Returns a Monoid for ReaderIOResult[A] with sequential execution.
func ApplicativeMonoidSeq[A any](m monoid.Monoid[A]) Monoid[A] {
return monoid.ApplicativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadApSeq[A, A],
m,
)
return RIOR.ApplicativeMonoidSeq[context.Context](m)
}
// ApplicativeMonoidPar returns a [Monoid] that concatenates [ReaderIOEither] instances via their applicative.
// ApplicativeMonoidPar returns a [Monoid] that concatenates [ReaderIOResult] instances via their applicative.
// This explicitly uses parallel execution for combining values.
//
// Parameters:
// - m: The underlying monoid for type A
//
// Returns a Monoid for ReaderIOEither[A] with parallel execution.
// Returns a Monoid for ReaderIOResult[A] with parallel execution.
func ApplicativeMonoidPar[A any](m monoid.Monoid[A]) Monoid[A] {
return monoid.ApplicativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadApPar[A, A],
m,
)
return RIOR.ApplicativeMonoidPar[context.Context](m)
}
// AlternativeMonoid is the alternative [Monoid] for [ReaderIOEither].
// This combines ReaderIOEither values using the alternative semantics,
// AlternativeMonoid is the alternative [Monoid] for [ReaderIOResult].
// This combines ReaderIOResult values using the alternative semantics,
// where the second value is only evaluated if the first fails.
//
// Parameters:
// - m: The underlying monoid for type A
//
// Returns a Monoid for ReaderIOEither[A] with alternative semantics.
// Returns a Monoid for ReaderIOResult[A] with alternative semantics.
func AlternativeMonoid[A any](m monoid.Monoid[A]) Monoid[A] {
return monoid.AlternativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadAp[A, A],
MonadAlt[A],
m,
)
return RIOR.AlternativeMonoid[context.Context](m)
}
// AltMonoid is the alternative [Monoid] for a [ReaderIOEither].
// AltMonoid is the alternative [Monoid] for a [ReaderIOResult].
// This creates a monoid where the empty value is provided lazily,
// and combination uses the Alt operation (try first, fallback to second on failure).
//
// Parameters:
// - zero: Lazy computation that provides the empty/identity value
//
// Returns a Monoid for ReaderIOEither[A] with Alt-based combination.
func AltMonoid[A any](zero Lazy[ReaderIOEither[A]]) Monoid[A] {
return monoid.AltMonoid(
zero,
MonadAlt[A],
)
// Returns a Monoid for ReaderIOResult[A] with Alt-based combination.
func AltMonoid[A any](zero Lazy[ReaderIOResult[A]]) Monoid[A] {
return RIOR.AltMonoid(zero)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,878 @@
// 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 readerioresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
E "github.com/IBM/fp-go/v2/either"
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"
)
func TestFromEither(t *testing.T) {
t.Run("Right value", func(t *testing.T) {
either := E.Right[error]("success")
result := FromEither(either)
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
})
t.Run("Left value", func(t *testing.T) {
err := errors.New("test error")
either := E.Left[string](err)
result := FromEither(either)
assert.Equal(t, E.Left[string](err), result(context.Background())())
})
}
func TestFromResult(t *testing.T) {
t.Run("Success", func(t *testing.T) {
result := FromResult(E.Right[error](42))
assert.Equal(t, E.Right[error](42), result(context.Background())())
})
t.Run("Error", func(t *testing.T) {
err := errors.New("test error")
result := FromResult(E.Left[int](err))
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestLeft(t *testing.T) {
err := errors.New("test error")
result := Left[string](err)
assert.Equal(t, E.Left[string](err), result(context.Background())())
}
func TestRight(t *testing.T) {
result := Right("success")
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
}
func TestOf(t *testing.T) {
result := Of(42)
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestMonadMap(t *testing.T) {
t.Run("Map over Right", func(t *testing.T) {
result := MonadMap(Of(5), N.Mul(2))
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("Map over Left", func(t *testing.T) {
err := errors.New("test error")
result := MonadMap(Left[int](err), N.Mul(2))
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestMap(t *testing.T) {
t.Run("Map with success", func(t *testing.T) {
mapper := Map(N.Mul(2))
result := mapper(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("Map with error", func(t *testing.T) {
err := errors.New("test error")
mapper := Map(N.Mul(2))
result := mapper(Left[int](err))
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("MapTo with success", func(t *testing.T) {
result := MonadMapTo(Of("original"), 42)
assert.Equal(t, E.Right[error](42), result(context.Background())())
})
t.Run("MapTo with error", func(t *testing.T) {
err := errors.New("test error")
result := MonadMapTo(Left[string](err), 42)
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestMapTo(t *testing.T) {
mapper := MapTo[string](42)
result := mapper(Of("original"))
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestMonadChain(t *testing.T) {
t.Run("Chain with success", func(t *testing.T) {
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("Chain with error in first", func(t *testing.T) {
err := errors.New("test error")
result := MonadChain(Left[int](err), func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
t.Run("Chain with error in second", func(t *testing.T) {
err := errors.New("test error")
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
return Left[int](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestChain(t *testing.T) {
chainer := Chain(func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestMonadChainFirst(t *testing.T) {
t.Run("ChainFirst keeps first value", func(t *testing.T) {
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
return Of("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
})
t.Run("ChainFirst propagates error from second", func(t *testing.T) {
err := errors.New("test error")
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
return Left[string](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestChainFirst(t *testing.T) {
chainer := ChainFirst(func(x int) ReaderIOResult[string] {
return Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
}
func TestMonadApSeq(t *testing.T) {
t.Run("ApSeq with success", func(t *testing.T) {
fab := Of(N.Mul(2))
fa := Of(5)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("ApSeq with error in function", func(t *testing.T) {
err := errors.New("test error")
fab := Left[func(int) int](err)
fa := Of(5)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
t.Run("ApSeq with error in value", func(t *testing.T) {
err := errors.New("test error")
fab := Of(N.Mul(2))
fa := Left[int](err)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestApSeq(t *testing.T) {
fa := Of(5)
fab := Of(N.Mul(2))
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestApPar(t *testing.T) {
t.Run("ApPar with success", func(t *testing.T) {
fa := Of(5)
fab := Of(N.Mul(2))
result := MonadApPar(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("ApPar with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
fa := Of(5)
fab := Of(N.Mul(2))
result := MonadApPar(fab, fa)
res := result(ctx)()
assert.True(t, E.IsLeft(res))
})
}
func TestFromPredicate(t *testing.T) {
t.Run("Predicate true", func(t *testing.T) {
pred := FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
)
result := pred(5)
assert.Equal(t, E.Right[error](5), result(context.Background())())
})
t.Run("Predicate false", func(t *testing.T) {
pred := FromPredicate(
func(x int) bool { return x > 0 },
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
)
result := pred(-5)
res := result(context.Background())()
assert.True(t, E.IsLeft(res))
})
}
func TestOrElse(t *testing.T) {
t.Run("OrElse with success", func(t *testing.T) {
fallback := OrElse(func(err error) ReaderIOResult[int] {
return Of(42)
})
result := fallback(Of(10))
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("OrElse with error", func(t *testing.T) {
err := errors.New("test error")
fallback := OrElse(func(err error) ReaderIOResult[int] {
return Of(42)
})
result := fallback(Left[int](err))
assert.Equal(t, E.Right[error](42), result(context.Background())())
})
}
func TestAsk(t *testing.T) {
result := Ask()
ctx := context.Background()
res := result(ctx)()
assert.True(t, E.IsRight(res))
ctxResult := E.ToOption(res)
assert.True(t, O.IsSome(ctxResult))
}
func TestMonadChainEitherK(t *testing.T) {
t.Run("ChainEitherK with success", func(t *testing.T) {
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
return E.Right[error](x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("ChainEitherK with error", func(t *testing.T) {
err := errors.New("test error")
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
return E.Left[int](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestChainEitherK(t *testing.T) {
chainer := ChainEitherK(func(x int) Either[int] {
return E.Right[error](x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestMonadChainFirstEitherK(t *testing.T) {
t.Run("ChainFirstEitherK keeps first value", func(t *testing.T) {
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
return E.Right[error]("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
})
t.Run("ChainFirstEitherK propagates error", func(t *testing.T) {
err := errors.New("test error")
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
return E.Left[string](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestChainFirstEitherK(t *testing.T) {
chainer := ChainFirstEitherK(func(x int) Either[string] {
return E.Right[error]("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
}
func TestChainOptionK(t *testing.T) {
t.Run("ChainOptionK with Some", func(t *testing.T) {
chainer := ChainOptionK[int, int](func() error {
return errors.New("none error")
})(func(x int) Option[int] {
return O.Some(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("ChainOptionK with None", func(t *testing.T) {
chainer := ChainOptionK[int, int](func() error {
return errors.New("none error")
})(func(x int) Option[int] {
return O.None[int]()
})
result := chainer(Of(5))
res := result(context.Background())()
assert.True(t, E.IsLeft(res))
})
}
func TestFromIOEither(t *testing.T) {
t.Run("FromIOEither with success", func(t *testing.T) {
ioe := IOE.Of[error](42)
result := FromIOEither(ioe)
assert.Equal(t, E.Right[error](42), result(context.Background())())
})
t.Run("FromIOEither with error", func(t *testing.T) {
err := errors.New("test error")
ioe := IOE.Left[int](err)
result := FromIOEither(ioe)
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestFromIOResult(t *testing.T) {
ioe := IOE.Of[error](42)
result := FromIOResult(ioe)
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestFromIO(t *testing.T) {
io := IOG.Of(42)
result := FromIO(io)
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestFromReader(t *testing.T) {
reader := R.Of[context.Context](42)
result := FromReader(reader)
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
result := FromLazy(lazy)
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestNever(t *testing.T) {
t.Run("Never with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
result := Never[int]()
// Cancel immediately
cancel()
res := result(ctx)()
assert.True(t, E.IsLeft(res))
})
t.Run("Never with timeout", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
result := Never[int]()
res := result(ctx)()
assert.True(t, E.IsLeft(res))
})
}
func TestMonadChainIOK(t *testing.T) {
result := MonadChainIOK(Of(5), func(x int) IOG.IO[int] {
return IOG.Of(x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestChainIOK(t *testing.T) {
chainer := ChainIOK(func(x int) IOG.IO[int] {
return IOG.Of(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestMonadChainFirstIOK(t *testing.T) {
result := MonadChainFirstIOK(Of(5), func(x int) IOG.IO[string] {
return IOG.Of("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
}
func TestChainFirstIOK(t *testing.T) {
chainer := ChainFirstIOK(func(x int) IOG.IO[string] {
return IOG.Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
}
func TestChainIOEitherK(t *testing.T) {
t.Run("ChainIOEitherK with success", func(t *testing.T) {
chainer := ChainIOEitherK(func(x int) IOResult[int] {
return IOE.Of[error](x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
})
t.Run("ChainIOEitherK with error", func(t *testing.T) {
err := errors.New("test error")
chainer := ChainIOEitherK(func(x int) IOResult[int] {
return IOE.Left[int](err)
})
result := chainer(Of(5))
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestDelay(t *testing.T) {
t.Run("Delay with success", func(t *testing.T) {
start := time.Now()
delayed := Delay[int](100 * time.Millisecond)
result := delayed(Of(42))
res := result(context.Background())()
elapsed := time.Since(start)
assert.True(t, E.IsRight(res))
assert.GreaterOrEqual(t, elapsed, 100*time.Millisecond)
})
t.Run("Delay with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
delayed := Delay[int](100 * time.Millisecond)
result := delayed(Of(42))
// Cancel after starting but before delay completes
cancel()
res := result(ctx)()
// The result might be either Left (if cancelled) or Right (if completed before cancel)
// This is a race condition, so we just verify it completes
assert.True(t, E.IsLeft(res) || E.IsRight(res))
})
}
func TestDefer(t *testing.T) {
counter := 0
deferred := Defer(func() ReaderIOResult[int] {
counter++
return Of(counter)
})
// First execution
res1 := deferred(context.Background())()
assert.True(t, E.IsRight(res1))
// Second execution should generate a new computation
res2 := deferred(context.Background())()
assert.True(t, E.IsRight(res2))
// Counter should be incremented for each execution
assert.Equal(t, 2, counter)
}
func TestTryCatch(t *testing.T) {
t.Run("TryCatch with success", func(t *testing.T) {
result := TryCatch(func(ctx context.Context) func() (int, error) {
return func() (int, error) {
return 42, nil
}
})
assert.Equal(t, E.Right[error](42), result(context.Background())())
})
t.Run("TryCatch with error", func(t *testing.T) {
err := errors.New("test error")
result := TryCatch(func(ctx context.Context) func() (int, error) {
return func() (int, error) {
return 0, err
}
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
})
}
func TestMonadAlt(t *testing.T) {
t.Run("Alt with first success", func(t *testing.T) {
first := Of(42)
second := func() ReaderIOResult[int] { return Of(100) }
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](42), result(context.Background())())
})
t.Run("Alt with first error", func(t *testing.T) {
err := errors.New("test error")
first := Left[int](err)
second := func() ReaderIOResult[int] { return Of(100) }
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](100), result(context.Background())())
})
}
func TestAlt(t *testing.T) {
err := errors.New("test error")
alternative := Alt(func() ReaderIOResult[int] { return Of(100) })
result := alternative(Left[int](err))
assert.Equal(t, E.Right[error](100), result(context.Background())())
}
func TestMemoize(t *testing.T) {
counter := 0
computation := Memoize(FromLazy(func() int {
counter++
return counter
}))
// First execution
res1 := computation(context.Background())()
assert.True(t, E.IsRight(res1))
val1 := E.ToOption(res1)
v1, _ := O.Unwrap(val1)
assert.Equal(t, 1, v1)
// Second execution should return cached value
res2 := computation(context.Background())()
assert.True(t, E.IsRight(res2))
val2 := E.ToOption(res2)
v2, _ := O.Unwrap(val2)
assert.Equal(t, 1, v2)
// Counter should only be incremented once
assert.Equal(t, 1, counter)
}
func TestFlatten(t *testing.T) {
nested := Of(Of(42))
result := Flatten(nested)
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestMonadFlap(t *testing.T) {
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(N.Mul(2)))
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestFold(t *testing.T) {
t.Run("Fold with success", func(t *testing.T) {
folder := Fold(
func(err error) ReaderIOResult[string] {
return Of("error: " + err.Error())
},
func(x int) ReaderIOResult[string] {
return Of(fmt.Sprintf("success: %d", x))
},
)
result := folder(Of(42))
assert.Equal(t, E.Right[error]("success: 42"), result(context.Background())())
})
t.Run("Fold with error", func(t *testing.T) {
err := errors.New("test error")
folder := Fold(
func(err error) ReaderIOResult[string] {
return Of("error: " + err.Error())
},
func(x int) ReaderIOResult[string] {
return Of(fmt.Sprintf("success: %d", x))
},
)
result := folder(Left[int](err))
assert.Equal(t, E.Right[error]("error: test error"), result(context.Background())())
})
}
func TestGetOrElse(t *testing.T) {
t.Run("GetOrElse with success", func(t *testing.T) {
getter := GetOrElse(func(err error) ReaderIO[int] {
return func(ctx context.Context) IOG.IO[int] {
return IOG.Of(0)
}
})
result := getter(Of(42))
assert.Equal(t, 42, result(context.Background())())
})
t.Run("GetOrElse with error", func(t *testing.T) {
err := errors.New("test error")
getter := GetOrElse(func(err error) ReaderIO[int] {
return func(ctx context.Context) IOG.IO[int] {
return IOG.Of(0)
}
})
result := getter(Left[int](err))
assert.Equal(t, 0, result(context.Background())())
})
}
func TestWithContext(t *testing.T) {
t.Run("WithContext with valid context", func(t *testing.T) {
computation := WithContext(Of(42))
result := computation(context.Background())()
assert.Equal(t, E.Right[error](42), result)
})
t.Run("WithContext with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
computation := WithContext(Of(42))
result := computation(ctx)()
assert.True(t, E.IsLeft(result))
})
}
func TestEitherize0(t *testing.T) {
f := func(ctx context.Context) (int, error) {
return 42, nil
}
eitherized := Eitherize0(f)
result := eitherized()
assert.Equal(t, E.Right[error](42), result(context.Background())())
}
func TestUneitherize0(t *testing.T) {
f := func() ReaderIOResult[int] {
return Of(42)
}
uneitherized := Uneitherize0(f)
result, err := uneitherized(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, result)
}
func TestEitherize1(t *testing.T) {
f := func(ctx context.Context, x int) (int, error) {
return x * 2, nil
}
eitherized := Eitherize1(f)
result := eitherized(5)
assert.Equal(t, E.Right[error](10), result(context.Background())())
}
func TestUneitherize1(t *testing.T) {
f := func(x int) ReaderIOResult[int] {
return Of(x * 2)
}
uneitherized := Uneitherize1(f)
result, err := uneitherized(context.Background(), 5)
assert.NoError(t, err)
assert.Equal(t, 10, result)
}
func TestSequenceT2(t *testing.T) {
result := SequenceT2(Of(1), Of(2))
res := result(context.Background())()
assert.True(t, E.IsRight(res))
tuple := E.ToOption(res)
assert.True(t, O.IsSome(tuple))
t1, _ := O.Unwrap(tuple)
assert.Equal(t, 1, t1.F1)
assert.Equal(t, 2, t1.F2)
}
func TestSequenceSeqT2(t *testing.T) {
result := SequenceSeqT2(Of(1), Of(2))
res := result(context.Background())()
assert.True(t, E.IsRight(res))
}
func TestSequenceParT2(t *testing.T) {
result := SequenceParT2(Of(1), Of(2))
res := result(context.Background())()
assert.True(t, E.IsRight(res))
}
func TestTraverseArray(t *testing.T) {
t.Run("TraverseArray with success", func(t *testing.T) {
arr := []int{1, 2, 3}
traverser := TraverseArray(func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
result := traverser(arr)
res := result(context.Background())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.True(t, O.IsSome(arrOpt))
resultArr, _ := O.Unwrap(arrOpt)
assert.Equal(t, []int{2, 4, 6}, resultArr)
})
t.Run("TraverseArray with error", func(t *testing.T) {
arr := []int{1, 2, 3}
err := errors.New("test error")
traverser := TraverseArray(func(x int) ReaderIOResult[int] {
if x == 2 {
return Left[int](err)
}
return Of(x * 2)
})
result := traverser(arr)
res := result(context.Background())()
assert.True(t, E.IsLeft(res))
})
}
func TestSequenceArray(t *testing.T) {
arr := []ReaderIOResult[int]{Of(1), Of(2), Of(3)}
result := SequenceArray(arr)
res := result(context.Background())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.True(t, O.IsSome(arrOpt))
resultArr, _ := O.Unwrap(arrOpt)
assert.Equal(t, []int{1, 2, 3}, resultArr)
}
func TestTraverseRecord(t *testing.T) {
rec := map[string]int{"a": 1, "b": 2}
result := TraverseRecord[string](func(x int) ReaderIOResult[int] {
return Of(x * 2)
})(rec)
res := result(context.Background())()
assert.True(t, E.IsRight(res))
recOpt := E.ToOption(res)
assert.True(t, O.IsSome(recOpt))
resultRec, _ := O.Unwrap(recOpt)
assert.Equal(t, 2, resultRec["a"])
assert.Equal(t, 4, resultRec["b"])
}
func TestSequenceRecord(t *testing.T) {
rec := map[string]ReaderIOResult[int]{
"a": Of(1),
"b": Of(2),
}
result := SequenceRecord(rec)
res := result(context.Background())()
assert.True(t, E.IsRight(res))
recOpt := E.ToOption(res)
assert.True(t, O.IsSome(recOpt))
resultRec, _ := O.Unwrap(recOpt)
assert.Equal(t, 1, resultRec["a"])
assert.Equal(t, 2, resultRec["b"])
}
func TestAltSemigroup(t *testing.T) {
sg := AltSemigroup[int]()
err := errors.New("test error")
result := sg.Concat(Left[int](err), Of(42))
res := result(context.Background())()
assert.Equal(t, E.Right[error](42), res)
}
func TestApplicativeMonoid(t *testing.T) {
// Test with int addition monoid
intAddMonoid := ApplicativeMonoid(M.MakeMonoid(
func(a, b int) int { return a + b },
0,
))
result := intAddMonoid.Concat(Of(5), Of(10))
res := result(context.Background())()
assert.Equal(t, E.Right[error](15), res)
}
func TestBracket(t *testing.T) {
t.Run("Bracket with success", func(t *testing.T) {
var acquired, released bool
acquire := FromLazy(func() int {
acquired = true
return 42
})
use := func(x int) ReaderIOResult[int] {
return Of(x * 2)
}
release := func(x int, result Either[int]) ReaderIOResult[any] {
return FromLazy(func() any {
released = true
return nil
})
}
result := Bracket(acquire, use, release)
res := result(context.Background())()
assert.True(t, acquired)
assert.True(t, released)
assert.Equal(t, E.Right[error](84), res)
})
t.Run("Bracket with error in use", func(t *testing.T) {
var acquired, released bool
err := errors.New("use error")
acquire := FromLazy(func() int {
acquired = true
return 42
})
use := func(x int) ReaderIOResult[int] {
return Left[int](err)
}
release := func(x int, result Either[int]) ReaderIOResult[any] {
return FromLazy(func() any {
released = true
return nil
})
}
result := Bracket(acquire, use, release)
res := result(context.Background())()
assert.True(t, acquired)
assert.True(t, released)
assert.Equal(t, E.Left[int](err), res)
})
}

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
@@ -143,7 +143,7 @@ func TestCanceledApply(t *testing.T) {
applied := F.Pipe1(
fct,
Ap[string, string](errValue),
Ap[string](errValue),
)
res := applied(context.Background())()
@@ -156,7 +156,7 @@ func TestRegularApply(t *testing.T) {
applied := F.Pipe1(
fct,
Ap[string, string](value),
Ap[string](value),
)
res := applied(context.Background())()
@@ -171,14 +171,14 @@ func TestWithResourceNoErrors(t *testing.T) {
return countAcquire
})
release := func(int) ReaderIOEither[int] {
release := func(int) ReaderIOResult[int] {
return FromLazy(func() int {
countRelease++
return countRelease
})
}
body := func(int) ReaderIOEither[int] {
body := func(int) ReaderIOResult[int] {
return FromLazy(func() int {
countBody++
return countBody
@@ -203,7 +203,7 @@ func TestWithResourceErrorInBody(t *testing.T) {
return countAcquire
})
release := func(int) ReaderIOEither[int] {
release := func(int) ReaderIOResult[int] {
return FromLazy(func() int {
countRelease++
return countRelease
@@ -211,7 +211,7 @@ func TestWithResourceErrorInBody(t *testing.T) {
}
err := fmt.Errorf("error in body")
body := func(int) ReaderIOEither[int] {
body := func(int) ReaderIOResult[int] {
return Left[int](err)
}
@@ -231,14 +231,14 @@ func TestWithResourceErrorInAcquire(t *testing.T) {
err := fmt.Errorf("error in acquire")
acquire := Left[int](err)
release := func(int) ReaderIOEither[int] {
release := func(int) ReaderIOResult[int] {
return FromLazy(func() int {
countRelease++
return countRelease
})
}
body := func(int) ReaderIOEither[int] {
body := func(int) ReaderIOResult[int] {
return FromLazy(func() int {
countBody++
return countBody
@@ -264,11 +264,11 @@ func TestWithResourceErrorInRelease(t *testing.T) {
})
err := fmt.Errorf("error in release")
release := func(int) ReaderIOEither[int] {
release := func(int) ReaderIOResult[int] {
return Left[int](err)
}
body := func(int) ReaderIOEither[int] {
body := func(int) ReaderIOResult[int] {
return FromLazy(func() int {
countBody++
return countBody
@@ -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

@@ -0,0 +1,169 @@
// 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 readerioresult
import (
"context"
"io"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource.
// This implements the RAII (Resource Acquisition Is Initialization) pattern, ensuring that resources are
// properly released even if the operation fails or the context is canceled.
//
// The resource is created, used, and released in a safe manner:
// - onCreate: Creates the resource
// - The provided function uses the resource
// - onRelease: Releases the resource (always called, even on error)
//
// Parameters:
// - onCreate: ReaderIOResult that creates the resource
// - onRelease: Function to release the resource
//
// Returns a function that takes a resource-using function and returns a ReaderIOResult.
//
// Example:
//
// file := WithResource(
// openFile("data.txt"),
// func(f *os.File) ReaderIOResult[any] {
// return TryCatch(func(ctx context.Context) func() (any, error) {
// return func() (any, error) { return nil, f.Close() }
// })
// },
// )
// result := file(func(f *os.File) ReaderIOResult[string] {
// return TryCatch(func(ctx context.Context) func() (string, error) {
// return func() (string, error) {
// data, err := io.ReadAll(f)
// return string(data), err
// }
// })
// })
func WithResource[A, R, ANY any](onCreate ReaderIOResult[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
return RIOR.WithResource[A](onCreate, onRelease)
}
// onClose is a helper function that creates a ReaderIOResult for closing an io.Closer resource.
// It safely calls the Close() method and handles any errors that may occur during closing.
//
// Type Parameters:
// - A: Must implement io.Closer interface
//
// Parameters:
// - a: The resource to close
//
// Returns:
// - ReaderIOResult[any]: A computation that closes the resource and returns nil on success
//
// The function ignores the context parameter since closing operations typically don't need context.
// Any error from Close() is captured and returned as a Result error.
func onClose[A io.Closer](a A) ReaderIOResult[any] {
return func(_ context.Context) IOResult[any] {
return func() Result[any] {
return result.TryCatchError[any](nil, a.Close())
}
}
}
// WithCloser creates a resource management function specifically for io.Closer resources.
// This is a specialized version of WithResource that automatically handles closing of resources
// that implement the io.Closer interface.
//
// The function ensures that:
// - The resource is created using the onCreate function
// - The resource is automatically closed when the operation completes (success or failure)
// - Any errors during closing are properly handled
// - The resource is closed even if the main operation fails or the context is canceled
//
// Type Parameters:
// - B: The type of value returned by the resource-using function
// - A: The type of resource that implements io.Closer
//
// Parameters:
// - onCreate: ReaderIOResult that creates the io.Closer resource
//
// Returns:
// - A function that takes a resource-using function and returns a ReaderIOResult[B]
//
// Example with file operations:
//
// openFile := func(filename string) ReaderIOResult[*os.File] {
// return TryCatch(func(ctx context.Context) func() (*os.File, error) {
// return func() (*os.File, error) {
// return os.Open(filename)
// }
// })
// }
//
// fileReader := WithCloser(openFile("data.txt"))
// result := fileReader(func(f *os.File) ReaderIOResult[string] {
// return TryCatch(func(ctx context.Context) func() (string, error) {
// return func() (string, error) {
// data, err := io.ReadAll(f)
// return string(data), err
// }
// })
// })
//
// Example with HTTP response:
//
// httpGet := func(url string) ReaderIOResult[*http.Response] {
// return TryCatch(func(ctx context.Context) func() (*http.Response, error) {
// return func() (*http.Response, error) {
// return http.Get(url)
// }
// })
// }
//
// responseReader := WithCloser(httpGet("https://api.example.com/data"))
// result := responseReader(func(resp *http.Response) ReaderIOResult[[]byte] {
// return TryCatch(func(ctx context.Context) func() ([]byte, error) {
// return func() ([]byte, error) {
// return io.ReadAll(resp.Body)
// }
// })
// })
//
// Example with database connection:
//
// openDB := func(dsn string) ReaderIOResult[*sql.DB] {
// return TryCatch(func(ctx context.Context) func() (*sql.DB, error) {
// return func() (*sql.DB, error) {
// return sql.Open("postgres", dsn)
// }
// })
// }
//
// dbQuery := WithCloser(openDB("postgres://..."))
// result := dbQuery(func(db *sql.DB) ReaderIOResult[[]User] {
// return TryCatch(func(ctx context.Context) func() ([]User, error) {
// return func() ([]User, error) {
// rows, err := db.QueryContext(ctx, "SELECT * FROM users")
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// return scanUsers(rows)
// }
// })
// })
func WithCloser[B any, A io.Closer](onCreate ReaderIOResult[A]) Kleisli[Kleisli[A, B], B] {
return WithResource[B](onCreate, onClose[A])
}

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
@@ -37,7 +37,7 @@ var (
)
)
func closeFile(f *os.File) ReaderIOEither[string] {
func closeFile(f *os.File) ReaderIOResult[string] {
return F.Pipe1(
TryCatch(func(_ context.Context) func() (string, error) {
return func() (string, error) {
@@ -52,7 +52,7 @@ func ExampleWithResource() {
stringReader := WithResource[string](openFile("data/file.txt"), closeFile)
rdr := stringReader(func(f *os.File) ReaderIOEither[string] {
rdr := stringReader(func(f *os.File) ReaderIOResult[string] {
return F.Pipe2(
TryCatch(func(_ context.Context) func() ([]byte, error) {
return func() ([]byte, error) {

View File

@@ -13,21 +13,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"github.com/IBM/fp-go/v2/semigroup"
)
type (
Semigroup[A any] = semigroup.Semigroup[ReaderIOEither[A]]
Semigroup[A any] = semigroup.Semigroup[ReaderIOResult[A]]
)
// AltSemigroup is a [Semigroup] that tries the first item and then the second one using an alternative.
// This creates a semigroup where combining two ReaderIOEither values means trying the first one,
// This creates a semigroup where combining two ReaderIOResult values means trying the first one,
// and if it fails, trying the second one. This is useful for implementing fallback behavior.
//
// Returns a Semigroup for ReaderIOEither[A] with Alt-based combination.
// Returns a Semigroup for ReaderIOResult[A] with Alt-based combination.
//
// Example:
//

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioeither
package readerioresult
import (
"context"
@@ -29,9 +29,9 @@ import (
// The lock parameter should return a CancelFunc that releases the lock when called.
//
// Parameters:
// - lock: ReaderIOEither that acquires a lock and returns a CancelFunc to release it
// - lock: ReaderIOResult that acquires a lock and returns a CancelFunc to release it
//
// Returns a function that wraps a ReaderIOEither with lock protection.
// Returns a function that wraps a ReaderIOResult with lock protection.
//
// Example:
//
@@ -43,9 +43,9 @@ import (
// }
// })
// protectedOp := WithLock(lock)(myOperation)
func WithLock[A any](lock ReaderIOEither[context.CancelFunc]) Operator[A, A] {
func WithLock[A any](lock ReaderIOResult[context.CancelFunc]) Operator[A, A] {
return function.Flow2(
function.Constant1[context.CancelFunc, ReaderIOEither[A]],
function.Constant1[context.CancelFunc, ReaderIOResult[A]],
WithResource[A](lock, function.Flow2(
io.FromImpure[context.CancelFunc],
FromIO[any],

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