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

Compare commits

...

42 Commits

Author SHA1 Message Date
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
503 changed files with 70424 additions and 4480 deletions

View File

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

View File

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

482
v2/BENCHMARK_COMPARISON.md Normal file
View File

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

View File

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

816
v2/IDIOMATIC_COMPARISON.md Normal file
View File

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

View File

@@ -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) [![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) [![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 ## 📚 Table of Contents
- [Overview](#-overview)
- [Features](#-features)
- [Requirements](#-requirements) - [Requirements](#-requirements)
- [Breaking Changes](#-breaking-changes) - [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Breaking Changes](#️-breaking-changes)
- [Key Improvements](#-key-improvements) - [Key Improvements](#-key-improvements)
- [Migration Guide](#-migration-guide) - [Migration Guide](#-migration-guide)
- [Installation](#-installation)
- [What's New](#-whats-new) - [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 ## 🔧 Requirements
- **Go 1.24 or later** (for generic type alias support) - **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. 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]] 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. 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. 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). 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 ```go
// Operations on first element // Operations on first element
pair := MakePair(1, "hello") 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:** **V2:**
@@ -70,6 +197,36 @@ pair := MakePair(1, "hello")
result := Map(func(s string) string { return s + "!" })(pair) // Pair(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 := func(x int) int { return x + 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 := func(x int) int { return x + 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 ## ✨ Key Improvements
### 1. Simplified Type Declarations ### 1. Simplified Type Declarations
@@ -91,16 +248,16 @@ func processData(input string) ET.Either[error, OPT.Option[int]] {
**V2 Approach:** **V2 Approach:**
```go ```go
import ( 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/option"
) )
// Define type aliases once // 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] type Option[A any] = option.Option[A]
// Use them throughout your codebase // Use them throughout your codebase
func processData(input string) Either[Option[int]] { func processData(input string) Result[Option[int]] {
// implementation // implementation
} }
``` ```
@@ -211,7 +368,7 @@ If you're using `Pair`, update operations to work on the second element:
```go ```go
pair := MakePair(42, "data") pair := MakePair(42, "data")
// Map operates on first element // Map operates on first element
result := Map(func(x int) int { return x * 2 })(pair) result := Map(N.Mul(2))(pair)
``` ```
**After (V2):** **After (V2):**
@@ -230,20 +387,14 @@ Create project-wide type aliases for common patterns:
package myapp package myapp
import ( 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/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 Option[A any] = option.Option[A]
type IOEither[A any] = ioeither.IOEither[error, A] type IOResult[A any] = ioresult.IOResult[A]
```
## 📦 Installation
```bash
go get github.com/IBM/fp-go/v2
``` ```
## 🆕 What's New ## 🆕 What's New
@@ -277,25 +428,37 @@ func process() IOET.IOEither[error, string] {
**V2 Simplified Example:** **V2 Simplified Example:**
```go ```go
import ( import (
"github.com/IBM/fp-go/v2/either" "strconv"
"github.com/IBM/fp-go/v2/ioeither" "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] { func process() IOResult[string] {
return ioeither.Map( return ioresult.Map(
strconv.Itoa, strconv.Itoa,
)(fetchData()) )(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)** - Complete API reference
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - **[Code Samples](./samples/)** - Practical examples and use cases
- [Code Samples](../samples/) - **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
- [Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)
### 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? ## 🤔 Should I Migrate?
@@ -310,10 +473,25 @@ func process() IOEither[string] {
- ⚠️ Migration effort outweighs benefits for your project - ⚠️ Migration effort outweighs benefits for your project
- ⚠️ You need stability in production (V2 is newer) - ⚠️ 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 ## 🐛 Issues and Feedback
Found a bug or have a suggestion? Please [open an issue](https://github.com/IBM/fp-go/issues) on GitHub. Found a bug or have a suggestion? Please [open an issue](https://github.com/IBM/fp-go/issues) on GitHub.
## 📄 License ## 📄 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 ( import (
G "github.com/IBM/fp-go/v2/array/generic" 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" F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array" "github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid" 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" "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. // This is the monadic version of Map that takes the array as the first parameter.
// //
//go:inline //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) 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. // 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. // 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) count := len(as)
bs := make([]B, count) bs := make([]B, count)
for i := count - 1; i >= 0; i-- { for i := range count {
bs[i] = f(&as[i]) bs[i] = f(&as[i])
} }
return bs 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. // MapWithIndex applies a function to each element and its index in an array, returning a new array with the results.
// //
//go:inline //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) return G.MapWithIndex[[]A, []B](f)
} }
@@ -77,39 +76,39 @@ func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
// //
// Example: // 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] // result := double([]int{1, 2, 3}) // [2, 4, 6]
// //
//go:inline //go:inline
func Map[A, B any](f func(a A) B) func([]A) []B { func Map[A, B any](f func(A) B) Operator[A, B] {
return G.Map[[]A, []B](f) 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. // 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. // 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) return F.Bind2nd(MonadMapRef[A, B], f)
} }
func filterRef[A any](fa []A, pred func(a *A) bool) []A { func filterRef[A any](fa []A, pred func(*A) bool) []A {
var result []A
count := len(fa) count := len(fa)
for i := 0; i < count; i++ { var result []A = make([]A, 0, count)
a := fa[i] for i := range count {
if pred(&a) { a := &fa[i]
result = append(result, a) if pred(a) {
result = append(result, *a)
} }
} }
return result return result
} }
func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B { func filterMapRef[A, B any](fa []A, pred func(*A) bool, f func(*A) B) []B {
var result []B
count := len(fa) count := len(fa)
for i := 0; i < count; i++ { var result []B = make([]B, 0, count)
a := fa[i] for i := range count {
if pred(&a) { a := &fa[i]
result = append(result, f(&a)) if pred(a) {
result = append(result, f(a))
} }
} }
return result 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 // Filter returns a new array with all elements from the original array that match a predicate
// //
//go:inline //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) return G.Filter[[]A](pred)
} }
// FilterWithIndex returns a new array with all elements from the original array that match a predicate // FilterWithIndex returns a new array with all elements from the original array that match a predicate
// //
//go:inline //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) return G.FilterWithIndex[[]A](pred)
} }
// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers. // 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) 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. // This is the monadic version that takes the array as the first parameter.
// //
//go:inline //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) 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. // keeping only the Some values. This is the monadic version that takes the array as the first parameter.
// //
//go:inline //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) 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 //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) 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 //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) 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 //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) return G.FilterChain[[]A](f)
} }
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers. // 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 func(fa []A) []B {
return filterMapRef(fa, pred, f) 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 { func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
current := initial current := initial
count := len(fa) for i := range len(fa) {
for i := 0; i < count; i++ {
current = f(current, &fa[i]) current = f(current, &fa[i])
} }
return current return current
@@ -262,6 +260,8 @@ func Empty[A any]() []A {
} }
// Zero returns an empty array of type A (alias for Empty). // Zero returns an empty array of type A (alias for Empty).
//
//go:inline
func Zero[A any]() []A { func Zero[A any]() []A {
return Empty[A]() return Empty[A]()
} }
@@ -277,7 +277,7 @@ func Of[A any](a A) []A {
// This is the monadic version that takes the array as the first parameter (also known as FlatMap). // This is the monadic version that takes the array as the first parameter (also known as FlatMap).
// //
//go:inline //go:inline
func MonadChain[A, B any](fa []A, f func(a A) []B) []B { func MonadChain[A, B any](fa []A, f Kleisli[A, B]) []B {
return G.MonadChain(fa, f) return G.MonadChain(fa, f)
} }
@@ -290,7 +290,7 @@ func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
// result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3] // result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3]
// //
//go:inline //go:inline
func Chain[A, B any](f func(A) []B) func([]A) []B { func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return G.Chain[[]A](f) return G.Chain[[]A](f)
} }
@@ -306,7 +306,7 @@ func MonadAp[B, A any](fab []func(A) B, fa []A) []B {
// This is the curried version. // This is the curried version.
// //
//go:inline //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) return G.Ap[[]B, []func(A) B](fa)
} }
@@ -328,7 +328,7 @@ func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A)
// Returns None if the array is empty. // Returns None if the array is empty.
// //
//go:inline //go:inline
func Tail[A any](as []A) O.Option[[]A] { func Tail[A any](as []A) Option[[]A] {
return G.Tail(as) return G.Tail(as)
} }
@@ -336,7 +336,7 @@ func Tail[A any](as []A) O.Option[[]A] {
// Returns None if the array is empty. // Returns None if the array is empty.
// //
//go:inline //go:inline
func Head[A any](as []A) O.Option[A] { func Head[A any](as []A) Option[A] {
return G.Head(as) return G.Head(as)
} }
@@ -344,7 +344,7 @@ func Head[A any](as []A) O.Option[A] {
// Returns None if the array is empty. // Returns None if the array is empty.
// //
//go:inline //go:inline
func First[A any](as []A) O.Option[A] { func First[A any](as []A) Option[A] {
return G.First(as) return G.First(as)
} }
@@ -352,12 +352,12 @@ func First[A any](as []A) O.Option[A] {
// Returns None if the array is empty. // Returns None if the array is empty.
// //
//go:inline //go:inline
func Last[A any](as []A) O.Option[A] { func Last[A any](as []A) Option[A] {
return G.Last(as) return G.Last(as)
} }
// PrependAll inserts a separator before each element of an array. // 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 { return func(as []A) []A {
count := len(as) count := len(as)
dst := count * 2 dst := count * 2
@@ -377,7 +377,7 @@ func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
// Example: // Example:
// //
// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3] // 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) prepend := PrependAll(middle)
return func(as []A) []A { return func(as []A) []A {
if IsEmpty(as) { if IsEmpty(as) {
@@ -406,7 +406,7 @@ func Flatten[A any](mma [][]A) []A {
} }
// Slice extracts a subarray from index low (inclusive) to high (exclusive). // 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) 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. // Returns None if the index is out of bounds.
// //
//go:inline //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) 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. // If the index is out of bounds, the element is appended.
// //
//go:inline //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) 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. // SliceRight extracts a subarray from the specified start index to the end.
// //
//go:inline //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) 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 // Clone creates a deep copy of the array using the provided endomorphism to clone the values
// //
//go:inline //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) 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). // Push adds an element to the end of an array (alias for Append).
// //
//go:inline //go:inline
func Push[A any](a A) EM.Endomorphism[[]A] { func Push[A any](a A) Operator[A, A] {
return G.Push[EM.Endomorphism[[]A]](a) return G.Push[Operator[A, A]](a)
} }
// MonadFlap applies a value to an array of functions, producing an array of results. // MonadFlap applies a value to an array of functions, producing an array of results.
@@ -526,13 +526,13 @@ func MonadFlap[B, A any](fab []func(A) B, a A) []B {
// This is the curried version. // This is the curried version.
// //
//go:inline //go:inline
func Flap[B, A any](a A) func([]func(A) B) []B { func Flap[B, A any](a A) Operator[func(A) B, B] {
return G.Flap[func(A) B, []func(A) B, []B](a) 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. // Prepend adds an element to the beginning of an array, returning a new array.
// //
//go:inline //go:inline
func Prepend[A any](head A) EM.Endomorphism[[]A] { func Prepend[A any](head A) Operator[A, A] {
return G.Prepend[EM.Endomorphism[[]A]](head) return G.Prepend[Operator[A, A]](head)
} }

View File

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

View File

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

View File

@@ -87,6 +87,6 @@ func Example_sort() {
// [abc klm zyx] // [abc klm zyx]
// [zyx klm abc] // [zyx klm abc]
// [None[int] Some[int](42) Some[int](1337)] // [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 ( import (
G "github.com/IBM/fp-go/v2/array/generic" 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. // FindFirst finds the first element which satisfies a predicate function.
@@ -30,7 +30,7 @@ import (
// result2 := findGreaterThan3([]int{1, 2, 3}) // None // result2 := findGreaterThan3([]int{1, 2, 3}) // None
// //
//go:inline //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) 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) // result := findEvenAtEvenIndex([]int{1, 3, 4, 5}) // Some(4)
// //
//go:inline //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) 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) // result := parseFirst([]string{"a", "42", "b"}) // Some(42)
// //
//go:inline //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) 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. // The selector receives both the index and the element.
// //
//go:inline //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) 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) // result := findGreaterThan3([]int{1, 4, 2, 5}) // Some(5)
// //
//go:inline //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) 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. // Returns Some(element) if found, None if no element matches.
// //
//go:inline //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) 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. // This combines finding and mapping in a single operation, searching from the end.
// //
//go:inline //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) 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. // The selector receives both the index and the element, searching from the end.
// //
//go:inline //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) return G.FindLastMapWithIndex[[]A](sel)
} }

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ package array
import ( import (
"testing" "testing"
O "github.com/IBM/fp-go/v2/option"
OR "github.com/IBM/fp-go/v2/ord" OR "github.com/IBM/fp-go/v2/ord"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -103,39 +102,6 @@ func TestSortByKey(t *testing.T) {
assert.Equal(t, "Charlie", result[2].Name) 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) { func TestUniqByKey(t *testing.T) {
type Person struct { type Person struct {
Name string Name string

View File

@@ -16,27 +16,12 @@
package array package array
import ( import (
G "github.com/IBM/fp-go/v2/array/generic"
"github.com/IBM/fp-go/v2/internal/array" "github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid" M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup" 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. // Monoid returns a Monoid instance for arrays.
// The Monoid combines arrays through concatenation, with an empty array as the identity element. // 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]() // m := array.Monoid[int]()
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4] // result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
// empty := m.Empty() // [] // empty := m.Empty() // []
//
//go:inline
func Monoid[T any]() M.Monoid[[]T] { 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. // Semigroup returns a Semigroup instance for arrays.
@@ -56,8 +43,10 @@ func Monoid[T any]() M.Monoid[[]T] {
// //
// s := array.Semigroup[int]() // s := array.Semigroup[int]()
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4] // result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
//
//go:inline
func Semigroup[T any]() S.Semigroup[[]T] { 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 { func addLen[A any](count int, data []A) int {

View File

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

View File

@@ -24,8 +24,7 @@ import (
) )
func TestSequenceOption(t *testing.T) { 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.Of([]int{1, 3}), ArrayOption([]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.None[[]int](), ArrayOption([]O.Option[int]{O.Of(1), O.None[int]()}))
} }

View File

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

View File

@@ -32,7 +32,7 @@ import (
// // Result: [1, 1, 2, 3, 4, 5, 6, 9] // // Result: [1, 1, 2, 3, 4, 5, 6, 9]
// //
//go:inline //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) 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}] // // Result: [{"Bob", 25}, {"Alice", 30}, {"Charlie", 35}]
// //
//go:inline //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) 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"}] // // Result: [{"Jones", "Bob"}, {"Smith", "Alice"}, {"Smith", "John"}]
// //
//go:inline //go:inline
func SortBy[T any](ord []O.Ord[T]) func(ma []T) []T { func SortBy[T any](ord []O.Ord[T]) Operator[T, T] {
return G.SortBy[[]T](ord) 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) return array.MonadTraverse(fof, fmap, fap, ta, f)
} }
//go:inline
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
f func(int, A) HKTB) func([]A) HKTRB {
return array.TraverseWithIndex[[]A](fof, fmap, fap, f)
}
//go:inline
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta []A,
f func(int, A) HKTB) HKTRB {
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
}

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

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

View File

@@ -46,6 +46,6 @@ func StrictUniq[A comparable](as []A) []A {
// // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}] // // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}]
// //
//go:inline //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) return G.Uniq[[]A](f)
} }

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

@@ -0,0 +1,268 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package assert provides functional assertion helpers for testing.
//
// This package wraps testify/assert functions in a Reader monad pattern,
// allowing for composable and functional test assertions. Each assertion
// returns a Reader that takes a *testing.T and performs the assertion.
//
// The package supports:
// - Equality and inequality assertions
// - Collection assertions (arrays, maps, strings)
// - Error handling assertions
// - Result type assertions
// - Custom predicate assertions
// - Composable test suites
//
// Example:
//
// func TestExample(t *testing.T) {
// value := 42
// assert.Equal(42)(value)(t) // Curried style
//
// // Composing multiple assertions
// arr := []int{1, 2, 3}
// assertions := assert.AllOf([]assert.Reader{
// assert.ArrayNotEmpty(arr),
// assert.ArrayLength[int](3)(arr),
// assert.ArrayContains(2)(arr),
// })
// assertions(t)
// }
package assert
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
var (
// Eq is the equal predicate checking if objects are equal
Eq = eq.FromEquals(assert.ObjectsAreEqual)
)
// wrap1 is an internal helper function that wraps testify assertion functions
// into the Reader monad pattern with curried parameters.
//
// It takes a testify assertion function and converts it into a curried function
// that first takes an expected value, then an actual value, and finally returns
// a Reader that performs the assertion when given a *testing.T.
//
// Parameters:
// - wrapped: The testify assertion function to wrap
// - expected: The expected value for comparison
// - msgAndArgs: Optional message and arguments for assertion failure
//
// Returns:
// - A Kleisli function that takes the actual value and returns a Reader
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, expected T, msgAndArgs ...any) Kleisli[T] {
return func(actual T) Reader {
return func(t *testing.T) bool {
return wrapped(t, expected, actual, msgAndArgs...)
}
}
}
// NotEqual tests if the expected and the actual values are not equal
func NotEqual[T any](expected T) Kleisli[T] {
return wrap1(assert.NotEqual, expected)
}
// Equal tests if the expected and the actual values are equal
func Equal[T any](expected T) Kleisli[T] {
return wrap1(assert.Equal, expected)
}
// ArrayNotEmpty checks if an array is not empty
func ArrayNotEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, arr)
}
}
// RecordNotEmpty checks if an map is not empty
func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, mp)
}
}
// ArrayLength tests if an array has the expected length
func ArrayLength[T any](expected int) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// RecordLength tests if a map has the expected length
func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// StringLength tests if a string has the expected length
func StringLength[K comparable, T any](expected int) Kleisli[string] {
return func(actual string) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// NoError validates that there is no error
func NoError(err error) Reader {
return func(t *testing.T) bool {
return assert.NoError(t, err)
}
}
// Error validates that there is an error
func Error(err error) Reader {
return func(t *testing.T) bool {
return assert.Error(t, err)
}
}
// Success checks if a [Result] represents success
func Success[T any](res Result[T]) Reader {
return NoError(result.ToError(res))
}
// Failure checks if a [Result] represents failure
func Failure[T any](res Result[T]) Reader {
return Error(result.ToError(res))
}
// ArrayContains tests if a value is contained in an array
func ArrayContains[T any](expected T) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
return assert.Contains(t, actual, expected)
}
}
}
// ContainsKey tests if a key is contained in a map
func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Contains(t, actual, expected)
}
}
}
// NotContainsKey tests if a key is not contained in a map
func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.NotContains(t, actual, expected)
}
}
}
// That asserts that a particular predicate matches
func That[T any](pred Predicate[T]) Kleisli[T] {
return func(a T) Reader {
return func(t *testing.T) bool {
if pred(a) {
return true
}
return assert.Fail(t, fmt.Sprintf("Preficate %v does not match value %v", pred, a))
}
}
}
// AllOf combines multiple assertion Readers into a single Reader that passes
// only if all assertions pass.
//
// This function uses boolean AND logic (MonoidAll) to combine the results of
// all assertions. If any assertion fails, the combined assertion fails.
//
// This is useful for grouping related assertions together and ensuring all
// conditions are met.
//
// Parameters:
// - readers: Array of assertion Readers to combine
//
// Returns:
// - A single Reader that performs all assertions and returns true only if all pass
//
// Example:
//
// func TestUser(t *testing.T) {
// user := User{Name: "Alice", Age: 30, Active: true}
// assertions := assert.AllOf([]assert.Reader{
// assert.Equal("Alice")(user.Name),
// assert.Equal(30)(user.Age),
// assert.Equal(true)(user.Active),
// })
// assertions(t)
// }
//
//go:inline
func AllOf(readers []Reader) Reader {
return reader.MonadReduceArrayM(readers, boolean.MonoidAll)
}
// RunAll executes a map of named test cases, running each as a subtest.
//
// This function creates a Reader that runs multiple named test cases using
// Go's t.Run for proper test isolation and reporting. Each test case is
// executed as a separate subtest with its own name.
//
// The function returns true only if all subtests pass. This allows for
// better test organization and clearer test output.
//
// Parameters:
// - testcases: Map of test names to assertion Readers
//
// Returns:
// - A Reader that executes all named test cases and returns true if all pass
//
// Example:
//
// func TestMathOperations(t *testing.T) {
// testcases := map[string]assert.Reader{
// "addition": assert.Equal(4)(2 + 2),
// "multiplication": assert.Equal(6)(2 * 3),
// "subtraction": assert.Equal(1)(3 - 2),
// }
// assert.RunAll(testcases)(t)
// }
//
//go:inline
func RunAll(testcases map[string]Reader) Reader {
return func(t *testing.T) bool {
current := true
for k, r := range testcases {
current = current && t.Run(k, func(t1 *testing.T) {
r(t1)
})
}
return current
}
}

View File

@@ -16,94 +16,470 @@
package assert package assert
import ( import (
"fmt" "errors"
"testing" "testing"
E "github.com/IBM/fp-go/v2/either" "github.com/IBM/fp-go/v2/result"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
) )
var ( func TestEqual(t *testing.T) {
errTest = fmt.Errorf("test failure") t.Run("should pass when values are equal", func(t *testing.T) {
result := Equal(42)(42)(t)
// Eq is the equal predicate checking if objects are equal if !result {
Eq = EQ.FromEquals(assert.ObjectsAreEqual) t.Error("Expected Equal to pass for equal values")
)
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)
} }
return E.Left[T](errTest) })
}
}
// NotEqual tests if the expected and the actual values are not equal t.Run("should fail when values are not equal", func(t *testing.T) {
func NotEqual[T any](t *testing.T, expected T) func(actual T) E.Either[error, T] { mockT := &testing.T{}
return wrap1(assert.NotEqual, t, expected) result := Equal(42)(43)(mockT)
} if result {
t.Error("Expected Equal to fail for different values")
// 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)
} }
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 TestNotEqual(t *testing.T) {
func NoError[T any](t *testing.T) func(actual E.Either[error, T]) E.Either[error, T] { t.Run("should pass when values are not equal", func(t *testing.T) {
return func(actual E.Either[error, T]) E.Either[error, T] { result := NotEqual(42)(43)(t)
return E.MonadFold(actual, func(e error) E.Either[error, T] { if !result {
assert.NoError(t, e) t.Error("Expected NotEqual to pass for different values")
return E.Left[T](e) }
}, func(value T) E.Either[error, T] { })
assert.NoError(t, nil)
return E.Right[error](value) 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[int](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[int](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 TestRunAll(t *testing.T) {
func ArrayContains[T any](t *testing.T, expected T) func(actual []T) E.Either[error, []T] { t.Run("should run all named test cases", func(t *testing.T) {
return func(actual []T) E.Either[error, []T] { testcases := map[string]Reader{
ok := assert.Contains(t, actual, expected) "equality": Equal(42)(42),
if ok { "string_check": Equal("test")("test"),
return E.Of[error](actual) "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 TestEq(t *testing.T) {
func ContainsKey[T any, K comparable](t *testing.T, expected K) func(actual map[K]T) E.Either[error, map[K]T] { t.Run("should return true for equal values", func(t *testing.T) {
return func(actual map[K]T) E.Either[error, map[K]T] { if !Eq.Equals(42, 42) {
ok := assert.Contains(t, actual, expected) t.Error("Expected Eq to return true for equal integers")
if ok {
return E.Of[error](actual)
} }
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 TestIntegration(t *testing.T) {
func NotContainsKey[T any, K comparable](t *testing.T, expected K) func(actual map[K]T) E.Either[error, map[K]T] { t.Run("complex assertion composition", func(t *testing.T) {
return func(actual map[K]T) E.Either[error, map[K]T] { type User struct {
ok := assert.NotContains(t, actual, expected) Name string
if ok { Age int
return E.Of[error](actual) Email string
} }
return E.Left[map[K]T](errTest)
} user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
assertions := AllOf([]Reader{
Equal("Alice")(user.Name),
Equal(30)(user.Age),
That(func(s string) bool { return len(s) > 0 })(user.Email),
})
result := assertions(t)
if !result {
t.Error("Expected complex assertion composition to pass")
}
})
t.Run("test suite with RunAll", func(t *testing.T) {
data := []int{1, 2, 3, 4, 5}
suite := RunAll(map[string]Reader{
"not_empty": ArrayNotEmpty(data),
"correct_size": ArrayLength[int](5)(data),
"contains_one": ArrayContains(1)(data),
"contains_five": ArrayContains(5)(data),
})
result := suite(t)
if !result {
t.Error("Expected test suite to pass")
}
})
} }

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

@@ -0,0 +1,16 @@
package assert
import (
"testing"
"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]
)

View File

@@ -15,14 +15,163 @@
package bytes 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 { func Empty() []byte {
return Monoid.Empty() 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 { func ToString(a []byte) string {
return string(a) 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 { func Size(as []byte) int {
return len(as) 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 // Example tests
func ExampleEmpty() { func ExampleEmpty() {
empty := Empty() empty := Empty()
@@ -219,3 +512,17 @@ func ExampleConcatAll() {
// Output: // 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 ( 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]() 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] 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) Ord = O.MakeOrd(bytes.Compare, bytes.Equal)
) )

View File

@@ -53,17 +53,20 @@ var (
// structInfo holds information about a struct that needs lens generation // structInfo holds information about a struct that needs lens generation
type structInfo struct { type structInfo struct {
Name string Name string
Fields []fieldInfo TypeParams string // e.g., "[T any]" or "[K comparable, V any]" - for type declarations
Imports map[string]string // package path -> alias 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 // fieldInfo holds information about a struct field
type fieldInfo struct { type fieldInfo struct {
Name string Name string
TypeName string TypeName string
BaseType string // TypeName without leading * for pointer types BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag 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 // templateData holds data for template rendering
@@ -74,64 +77,95 @@ type templateData struct {
const lensStructTemplate = ` const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}} // {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
type {{.Name}}Lenses struct { type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields
{{- range .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}} {{- end}}
} }
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}} // {{.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}} {{- 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}} {{- end}}
} }
` `
const lensConstructorTemplate = ` const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields // 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}} {{- range .Fields}}
{{- if .IsOptional}} lens{{.Name}} := L.MakeLens(
iso{{.Name}} := I.FromZero[{{.TypeName}}]() 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}}
{{- end}} {{- end}}
return {{.Name}}Lenses{ return {{.Name}}Lenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}} {{- range .Fields}}
{{- if .IsOptional}} {{.Name}}: lens{{.Name}},
{{.Name}}: L.MakeLens( {{- end}}
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) }, // optional lenses
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s }, {{- range .Fields}}
), {{- if .IsComparable}}
{{- else}} {{.Name}}O: lens{{.Name}}O,
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
),
{{- end}} {{- end}}
{{- end}} {{- end}}
} }
} }
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields // 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}} {{- range .Fields}}
{{- if .IsOptional}} {{- if .IsComparable}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]() lens{{.Name}} := L.MakeLensStrict(
{{- end}} func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
{{- end}} func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
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 },
),
{{- else}} {{- else}}
{{.Name}}: L.MakeLensRef( lens{{.Name}} := L.MakeLensRef(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} }, func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s }, 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}}
{{- end}} {{- end}}
} }
@@ -257,6 +291,259 @@ func isPointerType(expr ast.Expr) bool {
return ok 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 // parseFile parses a Go file and extracts structs with lens annotations
func parseFile(filename string) ([]structInfo, string, error) { func parseFile(filename string) ([]structInfo, string, error) {
fset := token.NewFileSet() fset := token.NewFileSet()
@@ -320,9 +607,27 @@ func parseFile(filename string) ([]structInfo, string, error) {
var fields []fieldInfo var fields []fieldInfo
structImports := make(map[string]string) structImports := make(map[string]string)
// Build type parameters map for this struct
typeParamsMap := buildTypeParamsMap(typeSpec)
for _, field := range structType.Fields.List { for _, field := range structType.Fields.List {
if len(field.Names) == 0 { 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 continue
} }
for _, name := range field.Names { for _, name := range field.Names {
@@ -331,6 +636,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
typeName := getTypeName(field.Type) typeName := getTypeName(field.Type)
isOptional := false isOptional := false
baseType := typeName baseType := typeName
isComparable := false
// Check if field is optional: // Check if field is optional:
// 1. Pointer types are always optional // 1. Pointer types are always optional
@@ -344,6 +650,11 @@ func parseFile(filename string) ([]structInfo, string, error) {
isOptional = true 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 // Extract imports from this field's type
fieldImports := make(map[string]string) fieldImports := make(map[string]string)
extractImports(field.Type, fieldImports) extractImports(field.Type, fieldImports)
@@ -356,20 +667,24 @@ func parseFile(filename string) ([]structInfo, string, error) {
} }
fields = append(fields, fieldInfo{ fields = append(fields, fieldInfo{
Name: name.Name, Name: name.Name,
TypeName: typeName, TypeName: typeName,
BaseType: baseType, BaseType: baseType,
IsOptional: isOptional, IsOptional: isOptional,
IsComparable: isComparable,
}) })
} }
} }
} }
if len(fields) > 0 { if len(fields) > 0 {
typeParams, typeParamNames := extractTypeParams(typeSpec)
structs = append(structs, structInfo{ structs = append(structs, structInfo{
Name: typeSpec.Name.Name, Name: typeSpec.Name.Name,
Fields: fields, TypeParams: typeParams,
Imports: structImports, TypeParamNames: typeParamNames,
Fields: fields,
Imports: structImports,
}) })
} }
@@ -469,8 +784,8 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
// Standard fp-go imports always needed // Standard fp-go imports always needed
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n") 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("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\tO \"github.com/IBM/fp-go/v2/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("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// Add additional imports collected from field types // Add additional imports collected from field types
for importPath, alias := range allImports { 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) { func TestHasOmitEmpty(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -337,6 +422,167 @@ type Config struct {
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional") 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) { func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files // Create a temporary directory with test files
tmpDir := t.TempDir() tmpDir := t.TempDir()
@@ -373,11 +619,11 @@ type TestStruct struct {
// Check for expected content // Check for expected content
assert.Contains(t, contentStr, "package testpkg") assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "Code generated by go generate") assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLens") assert.Contains(t, contentStr, "TestStructLenses")
assert.Contains(t, contentStr, "MakeTestStructLens") assert.Contains(t, contentStr, "MakeTestStructLenses")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]") assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]") 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) { func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
@@ -411,8 +657,8 @@ func TestLensTemplates(t *testing.T) {
s := structInfo{ s := structInfo{
Name: "TestStruct", Name: "TestStruct",
Fields: []fieldInfo{ Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false}, {Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "*int", IsOptional: true}, {Name: "Value", TypeName: "*int", IsOptional: true, IsComparable: true},
}, },
} }
@@ -424,7 +670,9 @@ func TestLensTemplates(t *testing.T) {
structStr := structBuf.String() structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct") assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]") 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 // Test constructor template
var constructorBuf bytes.Buffer var constructorBuf bytes.Buffer
@@ -434,19 +682,21 @@ func TestLensTemplates(t *testing.T) {
constructorStr := constructorBuf.String() constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses") assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
assert.Contains(t, constructorStr, "return TestStructLenses{") assert.Contains(t, constructorStr, "return TestStructLenses{")
assert.Contains(t, constructorStr, "Name: L.MakeLens(") assert.Contains(t, constructorStr, "Name: lensName,")
assert.Contains(t, constructorStr, "Value: L.MakeLens(") assert.Contains(t, constructorStr, "NameO: lensNameO,")
assert.Contains(t, constructorStr, "I.FromZero") assert.Contains(t, constructorStr, "Value: lensValue,")
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
assert.Contains(t, constructorStr, "IO.FromZero")
} }
func TestLensTemplatesWithOmitEmpty(t *testing.T) { func TestLensTemplatesWithOmitEmpty(t *testing.T) {
s := structInfo{ s := structInfo{
Name: "ConfigStruct", Name: "ConfigStruct",
Fields: []fieldInfo{ Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false}, {Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty {Name: "Value", TypeName: "string", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty {Name: "Count", TypeName: "int", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer {Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: true}, // pointer
}, },
} }
@@ -458,9 +708,13 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
structStr := structBuf.String() structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct") assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]") 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, "NameO LO.LensO[ConfigStruct, string]")
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO") assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "Pointer LO.LensO[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 // Test constructor template
var constructorBuf bytes.Buffer var constructorBuf bytes.Buffer
@@ -469,9 +723,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
constructorStr := constructorBuf.String() constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses") assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()") assert.Contains(t, constructorStr, "IO.FromZero[string]()")
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()") assert.Contains(t, constructorStr, "IO.FromZero[int]()")
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()") assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
} }
func TestLensCommandFlags(t *testing.T) { func TestLensCommandFlags(t *testing.T) {
@@ -480,7 +734,7 @@ func TestLensCommandFlags(t *testing.T) {
assert.Equal(t, "lens", cmd.Name) assert.Equal(t, "lens", cmd.Name)
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage) 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), "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 // Check flags
assert.Len(t, cmd.Flags, 3) 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, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag") assert.True(t, hasVerbose, "should have verbose flag")
} }
func TestParseFileWithEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Base struct {
ID int
Name string
}
// fp-go:Lens
type Extended struct {
Base
Extra string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Extended struct
extended := structs[0]
assert.Equal(t, "Extended", extended.Name)
assert.Len(t, extended.Fields, 3, "Should have 3 fields: ID, Name (from Base), and Extra")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range extended.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["ID"], "Should have promoted ID field from Base")
assert.True(t, fieldNames["Name"], "Should have promoted Name field from Base")
assert.True(t, fieldNames["Extra"], "Should have Extra field")
}
func TestGenerateLensHelpersWithEmbeddedStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// Base struct to be embedded
type Address struct {
Street string
City string
}
// fp-go:Lens
type Person struct {
Address
Name string
Age int
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "PersonLenses")
assert.Contains(t, contentStr, "MakePersonLenses")
// Check that embedded fields are included
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")
// Check that optional lenses are also generated for embedded fields
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
}
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Metadata struct {
CreatedAt string
UpdatedAt string
}
// fp-go:Lens
type Document struct {
*Metadata
Title string
Content string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Document struct
doc := structs[0]
assert.Equal(t, "Document", doc.Name)
assert.Len(t, doc.Fields, 4, "Should have 4 fields: CreatedAt, UpdatedAt (from *Metadata), Title, and Content")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range doc.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["CreatedAt"], "Should have promoted CreatedAt field from *Metadata")
assert.True(t, fieldNames["UpdatedAt"], "Should have promoted UpdatedAt field from *Metadata")
assert.True(t, fieldNames["Title"], "Should have Title field")
assert.True(t, fieldNames["Content"], "Should have Content field")
}
func TestParseFileWithGenericStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Container[T any] struct {
Value T
Count int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Container struct
container := structs[0]
assert.Equal(t, "Container", container.Name)
assert.Equal(t, "[T any]", container.TypeParams, "Should have type parameter [T any]")
assert.Len(t, container.Fields, 2)
assert.Equal(t, "Value", container.Fields[0].Name)
assert.Equal(t, "T", container.Fields[0].TypeName)
assert.Equal(t, "Count", container.Fields[1].Name)
assert.Equal(t, "int", container.Fields[1].TypeName)
}
func TestParseFileWithMultipleTypeParams(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Pair[K comparable, V any] struct {
Key K
Value V
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Pair struct
pair := structs[0]
assert.Equal(t, "Pair", pair.Name)
assert.Equal(t, "[K comparable, V any]", pair.TypeParams, "Should have type parameters [K comparable, V any]")
assert.Len(t, pair.Fields, 2)
assert.Equal(t, "Key", pair.Fields[0].Name)
assert.Equal(t, "K", pair.Fields[0].TypeName)
assert.Equal(t, "Value", pair.Fields[1].Name)
assert.Equal(t, "V", pair.Fields[1].TypeName)
}
func TestGenerateLensHelpersWithGenericStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type Box[T any] struct {
Content T
Label string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type BoxLenses[T any] struct", "Should have generic BoxLenses type")
assert.Contains(t, contentStr, "type BoxRefLenses[T any] struct", "Should have generic BoxRefLenses type")
assert.Contains(t, contentStr, "func MakeBoxLenses[T any]() BoxLenses[T]", "Should have generic constructor")
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
// Check that fields use the generic type parameter
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")
// Check optional lenses - only for comparable types
// T any is not comparable, so ContentO should NOT be generated
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
// string is comparable, so LabelO should be generated
assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens")
}
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type ComparableBox[T comparable] struct {
Key T
Value string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type ComparableBoxLenses[T comparable] struct", "Should have generic ComparableBoxLenses type")
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict")
// Check that Value field (string, always comparable) also uses MakeLensStrict
assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict")
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable")
}

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

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

View File

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

View File

@@ -19,13 +19,17 @@ import (
"context" "context"
"time" "time"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either" "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors" "github.com/IBM/fp-go/v2/errors"
"github.com/IBM/fp-go/v2/function" "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither" "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult" "github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult" RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
) )
const ( const (
@@ -176,6 +180,11 @@ func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIORe
return RIOR.MonadChainFirst(ma, f) return RIOR.MonadChainFirst(ma, f)
} }
//go:inline
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTap(ma, f)
}
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first. // ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
// This is the curried version of [MonadChainFirst]. // This is the curried version of [MonadChainFirst].
// //
@@ -189,6 +198,11 @@ func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirst(f) return RIOR.ChainFirst(f)
} }
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.Tap(f)
}
// Of creates a [ReaderIOResult] that always succeeds with the given value. // Of creates a [ReaderIOResult] that always succeeds with the given value.
// This is the same as [Right] and represents the monadic return operation. // This is the same as [Right] and represents the monadic return operation.
// //
@@ -399,6 +413,11 @@ func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B])
return RIOR.MonadChainFirstEitherK(ma, f) return RIOR.MonadChainFirstEitherK(ma, f)
} }
//go:inline
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
return RIOR.MonadTapEitherK(ma, f)
}
// ChainFirstEitherK chains a function that returns an [Either] but keeps the original value. // ChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
// This is the curried version of [MonadChainFirstEitherK]. // This is the curried version of [MonadChainFirstEitherK].
// //
@@ -412,6 +431,11 @@ func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.ChainFirstEitherK[context.Context](f) return RIOR.ChainFirstEitherK[context.Context](f)
} }
//go:inline
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.TapEitherK[context.Context](f)
}
// ChainOptionK chains a function that returns an [Option] into a [ReaderIOResult] computation. // ChainOptionK chains a function that returns an [Option] into a [ReaderIOResult] computation.
// If the Option is None, the provided error function is called. // If the Option is None, the provided error function is called.
// //
@@ -534,6 +558,11 @@ func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderI
return RIOR.MonadChainFirstIOK(ma, f) return RIOR.MonadChainFirstIOK(ma, f)
} }
//go:inline
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
return RIOR.MonadTapIOK(ma, f)
}
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value. // ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// This is the curried version of [MonadChainFirstIOK]. // This is the curried version of [MonadChainFirstIOK].
// //
@@ -547,6 +576,11 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.ChainFirstIOK[context.Context](f) return RIOR.ChainFirstIOK[context.Context](f)
} }
//go:inline
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.TapIOK[context.Context](f)
}
// ChainIOEitherK chains a function that returns an [IOResult] into a [ReaderIOResult] computation. // ChainIOEitherK chains a function that returns an [IOResult] into a [ReaderIOResult] computation.
// This is useful for integrating IOResult-returning functions into ReaderIOResult workflows. // This is useful for integrating IOResult-returning functions into ReaderIOResult workflows.
// //
@@ -624,7 +658,7 @@ func Defer[A any](gen Lazy[ReaderIOResult[A]]) ReaderIOResult[A] {
// //
//go:inline //go:inline
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOResult[A] { func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOResult[A] {
return RIOR.TryCatch(f, errors.IdentityError) return RIOR.TryCatch(f, errors.Identity)
} }
// MonadAlt provides an alternative [ReaderIOResult] if the first one fails. // MonadAlt provides an alternative [ReaderIOResult] if the first one fails.
@@ -747,3 +781,180 @@ func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) Re
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] { func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
return RIOR.OrLeft[A](onLeft) return RIOR.OrLeft[A](onLeft)
} }
//go:inline
func FromReaderEither[A any](ma ReaderEither[context.Context, error, A]) ReaderIOResult[A] {
return RIOR.FromReaderEither(ma)
}
//go:inline
func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
return RIOR.FromReaderEither(ma)
}
//go:inline
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
return RIOR.FromReaderOption[context.Context, A](onNone)
}
//go:inline
func MonadChainReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderK(ma, f)
}
//go:inline
func ChainReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderK(f)
}
//go:inline
func MonadChainFirstReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderK(ma, f)
}
//go:inline
func MonadTapReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderK(ma, f)
}
//go:inline
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderK(f)
}
//go:inline
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderK(f)
}
//go:inline
func MonadChainReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderResultK(ma, f)
}
//go:inline
func ChainReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainReaderResultK(f)
}
//go:inline
func MonadChainFirstReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderResultK(ma, f)
}
//go:inline
func MonadTapReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderResultK(ma, f)
}
//go:inline
func ChainFirstReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderResultK(f)
}
//go:inline
func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapReaderResultK(f)
}
//go:inline
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderIOK(ma, f)
}
//go:inline
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderIOK(f)
}
//go:inline
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(f)
}
//go:inline
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderIOK(f)
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type. If the input is a Right value, it passes through unchanged.
//
//go:inline
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
return RIOR.MonadChainLeft(fa, f)
}
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
return RIOR.ChainLeft(f)
}
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
// but always returns the original Left error regardless of what f returns (Left or Right).
// If the input is a Right value, it passes through unchanged without calling f.
//
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
//
//go:inline
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstLeft(ma, f)
}
//go:inline
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadTapLeft(ma, f)
}
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
//
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
// ensuring the error path is preserved.
//
//go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.ChainFirstLeft[A](f)
}
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](f)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -19,14 +19,17 @@ import (
"context" "context"
"github.com/IBM/fp-go/v2/context/ioresult" "github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either" "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither" "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/lazy" "github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio" "github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult" RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result" "github.com/IBM/fp-go/v2/result"
) )
@@ -119,4 +122,8 @@ type (
// // Apply the transformation // // Apply the transformation
// result := toUpper(computation) // result := toUpper(computation)
Operator[A, B any] = Kleisli[ReaderIOResult[A], B] Operator[A, B any] = Kleisli[ReaderIOResult[A], B]
ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
) )

View File

@@ -92,3 +92,8 @@ func MonadFlap[B, A any](fab ReaderResult[func(A) B], a A) ReaderResult[B] {
func Flap[B, A any](a A) Operator[func(A) B, B] { func Flap[B, A any](a A) Operator[func(A) B, B] {
return readereither.Flap[context.Context, error, B](a) return readereither.Flap[context.Context, error, B](a)
} }
//go:inline
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
return readereither.Read[error, A](r)
}

View File

@@ -23,11 +23,13 @@ import (
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither" "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/result"
) )
type ( type (
Option[A any] = option.Option[A] Option[A any] = option.Option[A]
Either[A any] = either.Either[error, A] Either[A any] = either.Either[error, A]
Result[A any] = result.Result[A]
// ReaderResult is a specialization of the Reader monad for the typical golang scenario // ReaderResult is a specialization of the Reader monad for the typical golang scenario
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A] ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

8340
v2/coverage.out Normal file

File diff suppressed because it is too large Load Diff

67
v2/coverage.txt Normal file
View File

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

View File

@@ -19,7 +19,7 @@ import (
DIE "github.com/IBM/fp-go/v2/di/erasure" DIE "github.com/IBM/fp-go/v2/di/erasure"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
IO "github.com/IBM/fp-go/v2/io" IO "github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither" IOR "github.com/IBM/fp-go/v2/ioresult"
) )
var ( var (
@@ -34,5 +34,5 @@ var (
var RunMain = F.Flow3( var RunMain = F.Flow3(
DIE.MakeInjector, DIE.MakeInjector,
Main, Main,
IOE.Fold(IO.Of[error], F.Constant1[any](IO.Of[error](nil))), IOR.Fold(IO.Of[error], F.Constant1[any](IO.Of[error](nil))),
) )

View File

@@ -64,8 +64,8 @@ Creating and using dependencies:
dbProvider := di.MakeProvider1( dbProvider := di.MakeProvider1(
DBToken, DBToken,
ConfigToken.Identity(), ConfigToken.Identity(),
func(cfg Config) IOE.IOEither[error, Database] { func(cfg Config) IOResult[Database] {
return IOE.Of[error](NewDatabase(cfg)) return ioresult.Of(NewDatabase(cfg))
}, },
) )
@@ -73,8 +73,8 @@ Creating and using dependencies:
APIToken, APIToken,
ConfigToken.Identity(), ConfigToken.Identity(),
DBToken.Identity(), DBToken.Identity(),
func(cfg Config, db Database) IOE.IOEither[error, APIService] { func(cfg Config, db Database) IOResult[APIService] {
return IOE.Of[error](NewAPIService(cfg, db)) return ioresult.Of(NewAPIService(cfg, db))
}, },
) )
@@ -116,7 +116,7 @@ MakeProvider0 - No dependencies:
provider := di.MakeProvider0( provider := di.MakeProvider0(
token, token,
IOE.Of[error](value), ioresult.Of(value),
) )
MakeProvider1 - One dependency: MakeProvider1 - One dependency:
@@ -124,8 +124,8 @@ MakeProvider1 - One dependency:
provider := di.MakeProvider1( provider := di.MakeProvider1(
resultToken, resultToken,
dep1Token.Identity(), dep1Token.Identity(),
func(dep1 Dep1Type) IOE.IOEither[error, ResultType] { func(dep1 Dep1Type) IOResult[ResultType] {
return IOE.Of[error](createResult(dep1)) return ioresult.Of(createResult(dep1))
}, },
) )
@@ -135,8 +135,8 @@ MakeProvider2 - Two dependencies:
resultToken, resultToken,
dep1Token.Identity(), dep1Token.Identity(),
dep2Token.Identity(), dep2Token.Identity(),
func(dep1 Dep1Type, dep2 Dep2Type) IOE.IOEither[error, ResultType] { func(dep1 Dep1Type, dep2 Dep2Type) IOResult[ResultType] {
return IOE.Of[error](createResult(dep1, dep2)) return ioresult.Of(createResult(dep1, dep2))
}, },
) )
@@ -153,7 +153,7 @@ provider is registered:
token := di.MakeTokenWithDefault0( token := di.MakeTokenWithDefault0(
"ServiceName", "ServiceName",
IOE.Of[error](defaultImplementation), ioresult.Of(defaultImplementation),
) )
// Or with dependencies // Or with dependencies
@@ -161,8 +161,8 @@ provider is registered:
"ServiceName", "ServiceName",
dep1Token.Identity(), dep1Token.Identity(),
dep2Token.Identity(), dep2Token.Identity(),
func(dep1 Dep1Type, dep2 Dep2Type) IOE.IOEither[error, ResultType] { func(dep1 Dep1Type, dep2 Dep2Type) IOResult[ResultType] {
return IOE.Of[error](createDefault(dep1, dep2)) return ioresult.Of(createDefault(dep1, dep2))
}, },
) )
@@ -208,8 +208,8 @@ The framework provides a convenient pattern for running applications:
mainProvider := di.MakeProvider1( mainProvider := di.MakeProvider1(
di.InjMain, di.InjMain,
APIToken.Identity(), APIToken.Identity(),
func(api APIService) IOE.IOEither[error, any] { func(api APIService) IOResult[any] {
return IOE.Of[error](api.Start()) return ioresult.Of(api.Start())
}, },
) )
@@ -247,8 +247,8 @@ Example 1: Configuration-based Service
clientProvider := di.MakeProvider1( clientProvider := di.MakeProvider1(
ClientToken, ClientToken,
ConfigToken.Identity(), ConfigToken.Identity(),
func(cfg Config) IOE.IOEither[error, HTTPClient] { func(cfg Config) IOResult[HTTPClient] {
return IOE.Of[error](HTTPClient{config: cfg}) return ioresult.Of(HTTPClient{config: cfg})
}, },
) )
@@ -263,8 +263,8 @@ Example 2: Optional Dependencies
serviceProvider := di.MakeProvider1( serviceProvider := di.MakeProvider1(
ServiceToken, ServiceToken,
CacheToken.Option(), // Optional dependency CacheToken.Option(), // Optional dependency
func(cache O.Option[Cache]) IOE.IOEither[error, Service] { func(cache Option[Cache]) IOResult[Service] {
return IOE.Of[error](NewService(cache)) return ioresult.Of(NewService(cache))
}, },
) )
@@ -279,8 +279,8 @@ Example 3: Lazy Dependencies
reporterProvider := di.MakeProvider1( reporterProvider := di.MakeProvider1(
ReporterToken, ReporterToken,
DBToken.IOEither(), // Lazy dependency DBToken.IOEither(), // Lazy dependency
func(dbIO IOE.IOEither[error, Database]) IOE.IOEither[error, Reporter] { func(dbIO IOResult[Database]) IOResult[Reporter] {
return IOE.Of[error](NewReporter(dbIO)) return ioresult.Of(NewReporter(dbIO))
}, },
) )

View File

@@ -20,7 +20,7 @@ import (
"github.com/IBM/fp-go/v2/errors" "github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity" I "github.com/IBM/fp-go/v2/identity"
IOE "github.com/IBM/fp-go/v2/ioeither" IOR "github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/lazy" L "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record" R "github.com/IBM/fp-go/v2/record"
@@ -42,8 +42,8 @@ var (
missingProviderError = F.Flow4( missingProviderError = F.Flow4(
Dependency.String, Dependency.String,
errors.OnSome[string]("no provider for dependency [%s]"), errors.OnSome[string]("no provider for dependency [%s]"),
IOE.Left[any, error], IOR.Left[any],
F.Constant1[InjectableFactory, IOE.IOEither[error, any]], F.Constant1[InjectableFactory, IOResult[any]],
) )
// missingProviderErrorOrDefault returns the default [ProviderFactory] or an error // missingProviderErrorOrDefault returns the default [ProviderFactory] or an error
@@ -56,7 +56,7 @@ var (
emptyMulti any = A.Empty[any]() emptyMulti any = A.Empty[any]()
// emptyMultiDependency returns a [ProviderFactory] for an empty, multi dependency // emptyMultiDependency returns a [ProviderFactory] for an empty, multi dependency
emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOE.Of[error](emptyMulti))) emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOR.Of(emptyMulti)))
// handleMissingProvider covers the case of a missing provider. It either // handleMissingProvider covers the case of a missing provider. It either
// returns an error or an empty multi value provider // returns an error or an empty multi value provider
@@ -93,21 +93,21 @@ var (
// isMultiDependency tests if a dependency is a container dependency // isMultiDependency tests if a dependency is a container dependency
func isMultiDependency(dep Dependency) bool { func isMultiDependency(dep Dependency) bool {
return dep.Flag()&Multi == Multi return dep.Flag()&MULTI == MULTI
} }
// isItemProvider tests if a provivder provides a single item // isItemProvider tests if a provivder provides a single item
func isItemProvider(provider Provider) bool { func isItemProvider(provider Provider) bool {
return provider.Provides().Flag()&Item == Item return provider.Provides().Flag()&ITEM == ITEM
} }
// itemProviderFactory combines multiple factories into one, returning an array // itemProviderFactory combines multiple factories into one, returning an array
func itemProviderFactory(fcts []ProviderFactory) ProviderFactory { func itemProviderFactory(fcts []ProviderFactory) ProviderFactory {
return func(inj InjectableFactory) IOE.IOEither[error, any] { return func(inj InjectableFactory) IOResult[any] {
return F.Pipe2( return F.Pipe2(
fcts, fcts,
IOE.TraverseArray(I.Flap[IOE.IOEither[error, any]](inj)), IOR.TraverseArray(I.Flap[IOResult[any]](inj)),
IOE.Map[error](F.ToAny[[]any]), IOR.Map(F.ToAny[[]any]),
) )
} }
} }
@@ -118,7 +118,7 @@ func itemProviderFactory(fcts []ProviderFactory) ProviderFactory {
// makes sure to transitively resolve the required dependencies. // makes sure to transitively resolve the required dependencies.
func MakeInjector(providers []Provider) InjectableFactory { func MakeInjector(providers []Provider) InjectableFactory {
type Result = IOE.IOEither[error, any] type Result = IOResult[any]
type LazyResult = L.Lazy[Result] type LazyResult = L.Lazy[Result]
// resolved stores the values resolved so far, key is the string ID // resolved stores the values resolved so far, key is the string ID
@@ -148,11 +148,11 @@ func MakeInjector(providers []Provider) InjectableFactory {
T.Map2(F.Flow3( T.Map2(F.Flow3(
Dependency.Id, Dependency.Id,
R.Lookup[ProviderFactory, string], R.Lookup[ProviderFactory, string],
I.Ap[O.Option[ProviderFactory]](factoryByID), I.Ap[Option[ProviderFactory]](factoryByID),
), handleMissingProvider), ), handleMissingProvider),
T.Tupled2(O.MonadGetOrElse[ProviderFactory]), T.Tupled2(O.MonadGetOrElse[ProviderFactory]),
I.Ap[IOE.IOEither[error, any]](injFct), I.Ap[IOResult[any]](injFct),
IOE.Memoize[error, any], IOR.Memoize[any],
) )
} }

View File

@@ -19,25 +19,23 @@ import (
"fmt" "fmt"
A "github.com/IBM/fp-go/v2/array" A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity" I "github.com/IBM/fp-go/v2/identity"
IO "github.com/IBM/fp-go/v2/io" IO "github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither" IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption"
Int "github.com/IBM/fp-go/v2/number/integer" Int "github.com/IBM/fp-go/v2/number/integer"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record" R "github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result"
) )
type ( type (
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier // InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
InjectableFactory = func(Dependency) IOE.IOEither[error, any] InjectableFactory = func(Dependency) IOResult[any]
ProviderFactory = func(InjectableFactory) IOE.IOEither[error, any] ProviderFactory = func(InjectableFactory) IOResult[any]
paramIndex = map[int]int paramIndex = map[int]int
paramValue = map[int]any paramValue = map[int]any
handler = func(paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] handler = func(paramIndex) func([]IOResult[any]) IOResult[paramValue]
mapping = map[int]paramIndex mapping = map[int]paramIndex
Provider interface { Provider interface {
@@ -83,50 +81,50 @@ var (
mergeMaps = R.UnionLastMonoid[int, any]() mergeMaps = R.UnionLastMonoid[int, any]()
collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any]) collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any])
mapDeps = F.Curry2(A.MonadMap[Dependency, IOE.IOEither[error, any]]) mapDeps = F.Curry2(A.MonadMap[Dependency, IOResult[any]])
handlers = map[int]handler{ handlers = map[int]handler{
Identity: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { IDENTITY: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe1( return F.Pipe1(
mp, mp,
IOE.TraverseRecord[int](getAt(res)), IOE.TraverseRecord[int](getAt(res)),
) )
} }
}, },
Option: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { OPTION: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe3( return F.Pipe3(
mp, mp,
IO.TraverseRecord[int](getAt(res)), IO.TraverseRecord[int](getAt(res)),
IO.Map(R.Map[int](F.Flow2( IO.Map(R.Map[int](F.Flow2(
E.ToOption[error, any], result.ToOption[any],
F.ToAny[O.Option[any]], F.ToAny[Option[any]],
))), ))),
IOE.FromIO[error, paramValue], IOE.FromIO[error, paramValue],
) )
} }
}, },
IOEither: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { IOEITHER: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe2( return F.Pipe2(
mp, mp,
R.Map[int](F.Flow2( R.Map[int](F.Flow2(
getAt(res), getAt(res),
F.ToAny[IOE.IOEither[error, any]], F.ToAny[IOResult[any]],
)), )),
IOE.Of[error, paramValue], IOE.Of[error, paramValue],
) )
} }
}, },
IOOption: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { IOOPTION: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe2( return F.Pipe2(
mp, mp,
R.Map[int](F.Flow3( R.Map[int](F.Flow3(
getAt(res), getAt(res),
IOE.ToIOOption[error, any], IOE.ToIOOption[error, any],
F.ToAny[IOO.IOOption[any]], F.ToAny[IOOption[any]],
)), )),
IOE.Of[error, paramValue], IOE.Of[error, paramValue],
) )
@@ -141,23 +139,23 @@ func getAt[T any](ar []T) func(idx int) T {
} }
} }
func handleMapping(mp mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] { func handleMapping(mp mapping) func(res []IOResult[any]) IOResult[[]any] {
preFct := F.Pipe1( preFct := F.Pipe1(
mp, mp,
R.Collect(func(idx int, p paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] { R.Collect(func(idx int, p paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return handlers[idx](p) return handlers[idx](p)
}), }),
) )
doFct := F.Flow2( doFct := F.Flow2(
I.Flap[IOE.IOEither[error, paramValue], []IOE.IOEither[error, any]], I.Flap[IOResult[paramValue], []IOResult[any]],
IOE.TraverseArray[error, func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue], paramValue], IOE.TraverseArray[error, func([]IOResult[any]) IOResult[paramValue], paramValue],
) )
postFct := IOE.Map[error](F.Flow2( postFct := IOE.Map[error](F.Flow2(
A.Fold(mergeMaps), A.Fold(mergeMaps),
collectParams, collectParams,
)) ))
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] { return func(res []IOResult[any]) IOResult[[]any] {
return F.Pipe2( return F.Pipe2(
preFct, preFct,
doFct(res), doFct(res),
@@ -170,7 +168,7 @@ func handleMapping(mp mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither
// a function that accepts the resolved dependencies to return a result // a function that accepts the resolved dependencies to return a result
func MakeProviderFactory( func MakeProviderFactory(
deps []Dependency, deps []Dependency,
fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory { fct func(param ...any) IOResult[any]) ProviderFactory {
return F.Flow3( return F.Flow3(
mapDeps(deps), mapDeps(deps),

View File

@@ -17,20 +17,18 @@ package erasure
import ( import (
"fmt" "fmt"
O "github.com/IBM/fp-go/v2/option"
) )
const ( const (
BehaviourMask = 0x0f BehaviourMask = 0x0f
Identity = 0 // required dependency IDENTITY = 0 // required dependency
Option = 1 // optional dependency OPTION = 1 // optional dependency
IOEither = 2 // lazy and required IOEITHER = 2 // lazy and required
IOOption = 3 // lazy and optional IOOPTION = 3 // lazy and optional
TypeMask = 0xf0 TypeMask = 0xf0
Multi = 1 << 4 // array of implementations MULTI = 1 << 4 // array of implementations
Item = 2 << 4 // item of a multi token ITEM = 2 << 4 // item of a multi token
) )
// Dependency describes the relationship to a service // Dependency describes the relationship to a service
@@ -41,5 +39,5 @@ type Dependency interface {
// Flag returns a tag that identifies the behaviour of the dependency // Flag returns a tag that identifies the behaviour of the dependency
Flag() int Flag() int
// ProviderFactory optionally returns an attached [ProviderFactory] that represents the default for this dependency // ProviderFactory optionally returns an attached [ProviderFactory] that represents the default for this dependency
ProviderFactory() O.Option[ProviderFactory] ProviderFactory() Option[ProviderFactory]
} }

13
v2/di/erasure/types.go Normal file
View File

@@ -0,0 +1,13 @@
package erasure
import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
)
type (
Option[T any] = option.Option[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
)

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,14 @@ import (
DIE "github.com/IBM/fp-go/v2/di/erasure" DIE "github.com/IBM/fp-go/v2/di/erasure"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity" "github.com/IBM/fp-go/v2/identity"
IOE "github.com/IBM/fp-go/v2/ioeither" IOR "github.com/IBM/fp-go/v2/ioresult"
RIOE "github.com/IBM/fp-go/v2/readerioeither" RIOR "github.com/IBM/fp-go/v2/readerioresult"
) )
// Resolve performs a type safe resolution of a dependency // Resolve performs a type safe resolution of a dependency
func Resolve[T any](token InjectionToken[T]) RIOE.ReaderIOEither[DIE.InjectableFactory, error, T] { func Resolve[T any](token InjectionToken[T]) RIOR.ReaderIOResult[DIE.InjectableFactory, T] {
return F.Flow2( return F.Flow2(
identity.Ap[IOE.IOEither[error, any]](asDependency(token)), identity.Ap[IOResult[any]](asDependency(token)),
IOE.ChainEitherK(token.Unerase), IOR.ChainResultK(token.Unerase),
) )
} }

View File

@@ -22,9 +22,10 @@ import (
"github.com/IBM/fp-go/v2/errors" "github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither" IOE "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
) )
func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[error, T] { func lookupAt[T any](idx int, token Dependency[T]) func(params []any) Result[T] {
return F.Flow3( return F.Flow3(
A.Lookup[any](idx), A.Lookup[any](idx),
E.FromOption[any](errors.OnNone("No parameter at position %d", idx)), E.FromOption[any](errors.OnNone("No parameter at position %d", idx)),
@@ -32,7 +33,7 @@ func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[e
) )
} }
func eraseTuple[A, R any](f func(A) IOE.IOEither[error, R]) func(E.Either[error, A]) IOE.IOEither[error, any] { func eraseTuple[A, R any](f func(A) IOResult[R]) func(Result[A]) IOResult[any] {
return F.Flow3( return F.Flow3(
IOE.FromEither[error, A], IOE.FromEither[error, A],
IOE.Chain(f), IOE.Chain(f),
@@ -40,8 +41,8 @@ func eraseTuple[A, R any](f func(A) IOE.IOEither[error, R]) func(E.Either[error,
) )
} }
func eraseProviderFactory0[R any](f IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] { func eraseProviderFactory0[R any](f IOResult[R]) func(params ...any) IOResult[any] {
return func(_ ...any) IOE.IOEither[error, any] { return func(_ ...any) IOResult[any] {
return F.Pipe1( return F.Pipe1(
f, f,
IOE.Map[error](F.ToAny[R]), IOE.Map[error](F.ToAny[R]),
@@ -50,7 +51,7 @@ func eraseProviderFactory0[R any](f IOE.IOEither[error, R]) func(params ...any)
} }
func MakeProviderFactory0[R any]( func MakeProviderFactory0[R any](
fct IOE.IOEither[error, R], fct IOResult[R],
) DIE.ProviderFactory { ) DIE.ProviderFactory {
return DIE.MakeProviderFactory( return DIE.MakeProviderFactory(
A.Empty[DIE.Dependency](), A.Empty[DIE.Dependency](),
@@ -59,13 +60,13 @@ func MakeProviderFactory0[R any](
} }
// MakeTokenWithDefault0 creates a unique [InjectionToken] for a specific type with an attached default [DIE.Provider] // MakeTokenWithDefault0 creates a unique [InjectionToken] for a specific type with an attached default [DIE.Provider]
func MakeTokenWithDefault0[R any](name string, fct IOE.IOEither[error, R]) InjectionToken[R] { func MakeTokenWithDefault0[R any](name string, fct IOResult[R]) InjectionToken[R] {
return MakeTokenWithDefault[R](name, MakeProviderFactory0(fct)) return MakeTokenWithDefault[R](name, MakeProviderFactory0(fct))
} }
func MakeProvider0[R any]( func MakeProvider0[R any](
token InjectionToken[R], token InjectionToken[R],
fct IOE.IOEither[error, R], fct IOResult[R],
) DIE.Provider { ) DIE.Provider {
return DIE.MakeProvider( return DIE.MakeProvider(
token, token,
@@ -75,5 +76,5 @@ func MakeProvider0[R any](
// ConstProvider simple implementation for a provider with a constant value // ConstProvider simple implementation for a provider with a constant value
func ConstProvider[R any](token InjectionToken[R], value R) DIE.Provider { func ConstProvider[R any](token InjectionToken[R], value R) DIE.Provider {
return MakeProvider0(token, IOE.Of[error](value)) return MakeProvider0(token, ioresult.Of(value))
} }

View File

@@ -25,7 +25,8 @@ import (
E "github.com/IBM/fp-go/v2/either" E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither" IOE "github.com/IBM/fp-go/v2/ioeither"
O "github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -39,19 +40,19 @@ func TestSimpleProvider(t *testing.T) {
var staticCount int var staticCount int
staticValue := func(value string) IOE.IOEither[error, string] { staticValue := func(value string) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
staticCount++ staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
} }
} }
var dynamicCount int var dynamicCount int
dynamicValue := func(value string) IOE.IOEither[error, string] { dynamicValue := func(value string) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
dynamicCount++ dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
} }
} }
@@ -81,19 +82,19 @@ func TestOptionalProvider(t *testing.T) {
var staticCount int var staticCount int
staticValue := func(value string) IOE.IOEither[error, string] { staticValue := func(value string) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
staticCount++ staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
} }
} }
var dynamicCount int var dynamicCount int
dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] { dynamicValue := func(value Option[string]) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
dynamicCount++ dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
} }
} }
@@ -123,10 +124,10 @@ func TestOptionalProviderMissingDependency(t *testing.T) {
var dynamicCount int var dynamicCount int
dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] { dynamicValue := func(value Option[string]) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
dynamicCount++ dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
} }
} }
@@ -151,10 +152,10 @@ func TestProviderMissingDependency(t *testing.T) {
var dynamicCount int var dynamicCount int
dynamicValue := func(value string) IOE.IOEither[error, string] { dynamicValue := func(value string) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
dynamicCount++ dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
} }
} }
@@ -179,31 +180,31 @@ func TestEagerAndLazyProvider(t *testing.T) {
var staticCount int var staticCount int
staticValue := func(value string) IOE.IOEither[error, string] { staticValue := func(value string) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
staticCount++ staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
} }
} }
var dynamicCount int var dynamicCount int
dynamicValue := func(value string) IOE.IOEither[error, string] { dynamicValue := func(value string) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
dynamicCount++ dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now())) return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
} }
} }
var lazyEagerCount int var lazyEagerCount int
lazyEager := func(laz IOE.IOEither[error, string], eager string) IOE.IOEither[error, string] { lazyEager := func(laz IOResult[string], eager string) IOResult[string] {
return F.Pipe1( return F.Pipe1(
laz, laz,
IOE.Chain(func(lazValue string) IOE.IOEither[error, string] { IOE.Chain(func(lazValue string) IOResult[string] {
return func() E.Either[error, string] { return func() Result[string] {
lazyEagerCount++ lazyEagerCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s], [%s] at [%s]", lazValue, eager, time.Now())) return result.Of(fmt.Sprintf("Dynamic based on [%s], [%s] at [%s]", lazValue, eager, time.Now()))
} }
}), }),
) )
@@ -248,7 +249,7 @@ func TestItemProvider(t *testing.T) {
value := multiInj() value := multiInj()
assert.Equal(t, E.Of[error](A.From("Value1", "Value2")), value) assert.Equal(t, result.Of(A.From("Value1", "Value2")), value)
} }
func TestEmptyItemProvider(t *testing.T) { func TestEmptyItemProvider(t *testing.T) {
@@ -269,7 +270,7 @@ func TestEmptyItemProvider(t *testing.T) {
value := multiInj() value := multiInj()
assert.Equal(t, E.Of[error](A.Empty[string]()), value) assert.Equal(t, result.Of(A.Empty[string]()), value)
} }
func TestDependencyOnMultiProvider(t *testing.T) { func TestDependencyOnMultiProvider(t *testing.T) {
@@ -283,8 +284,8 @@ func TestDependencyOnMultiProvider(t *testing.T) {
p1 := ConstProvider(INJ_KEY1, "Value3") p1 := ConstProvider(INJ_KEY1, "Value3")
p2 := ConstProvider(INJ_KEY2, "Value4") p2 := ConstProvider(INJ_KEY2, "Value4")
fromMulti := func(val string, multi []string) IOE.IOEither[error, string] { fromMulti := func(val string, multi []string) IOResult[string] {
return IOE.Of[error](fmt.Sprintf("Val: %s, Multi: %s", val, multi)) return ioresult.Of(fmt.Sprintf("Val: %s, Multi: %s", val, multi))
} }
p3 := MakeProvider2(INJ_KEY3, INJ_KEY1.Identity(), injMulti.Container().Identity(), fromMulti) p3 := MakeProvider2(INJ_KEY3, INJ_KEY1.Identity(), injMulti.Container().Identity(), fromMulti)
@@ -295,19 +296,19 @@ func TestDependencyOnMultiProvider(t *testing.T) {
v := r3(inj)() v := r3(inj)()
assert.Equal(t, E.Of[error]("Val: Value3, Multi: [Value1 Value2]"), v) assert.Equal(t, result.Of("Val: Value3, Multi: [Value1 Value2]"), v)
} }
func TestTokenWithDefaultProvider(t *testing.T) { func TestTokenWithDefaultProvider(t *testing.T) {
// token without a default // token without a default
injToken1 := MakeToken[string]("Token1") injToken1 := MakeToken[string]("Token1")
// token with a default // token with a default
injToken2 := MakeTokenWithDefault0("Token2", IOE.Of[error]("Carsten")) injToken2 := MakeTokenWithDefault0("Token2", ioresult.Of("Carsten"))
// dependency // dependency
injToken3 := MakeToken[string]("Token3") injToken3 := MakeToken[string]("Token3")
p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOE.IOEither[error, string] { p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOResult[string] {
return IOE.Of[error](fmt.Sprintf("Token: %s", data)) return ioresult.Of(fmt.Sprintf("Token: %s", data))
}) })
// populate the injector // populate the injector
@@ -320,19 +321,19 @@ func TestTokenWithDefaultProvider(t *testing.T) {
// inj1 should not be available // inj1 should not be available
assert.True(t, E.IsLeft(r1(inj)())) assert.True(t, E.IsLeft(r1(inj)()))
// r3 should work // r3 should work
assert.Equal(t, E.Of[error]("Token: Carsten"), r3(inj)()) assert.Equal(t, result.Of("Token: Carsten"), r3(inj)())
} }
func TestTokenWithDefaultProviderAndOverride(t *testing.T) { func TestTokenWithDefaultProviderAndOverride(t *testing.T) {
// token with a default // token with a default
injToken2 := MakeTokenWithDefault0("Token2", IOE.Of[error]("Carsten")) injToken2 := MakeTokenWithDefault0("Token2", ioresult.Of("Carsten"))
// dependency // dependency
injToken3 := MakeToken[string]("Token3") injToken3 := MakeToken[string]("Token3")
p2 := ConstProvider(injToken2, "Override") p2 := ConstProvider(injToken2, "Override")
p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOE.IOEither[error, string] { p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOResult[string] {
return IOE.Of[error](fmt.Sprintf("Token: %s", data)) return ioresult.Of(fmt.Sprintf("Token: %s", data))
}) })
// populate the injector // populate the injector
@@ -342,5 +343,5 @@ func TestTokenWithDefaultProviderAndOverride(t *testing.T) {
r3 := Resolve(injToken3) r3 := Resolve(injToken3)
// r3 should work // r3 should work
assert.Equal(t, E.Of[error]("Token: Override"), r3(inj)()) assert.Equal(t, result.Of("Token: Override"), r3(inj)())
} }

View File

@@ -21,10 +21,7 @@ import (
"sync/atomic" "sync/atomic"
DIE "github.com/IBM/fp-go/v2/di/erasure" DIE "github.com/IBM/fp-go/v2/di/erasure"
E "github.com/IBM/fp-go/v2/either"
IO "github.com/IBM/fp-go/v2/io" IO "github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
) )
@@ -33,7 +30,7 @@ import (
type Dependency[T any] interface { type Dependency[T any] interface {
DIE.Dependency DIE.Dependency
// Unerase converts a value with erased type signature into a strongly typed value // Unerase converts a value with erased type signature into a strongly typed value
Unerase(val any) E.Either[error, T] Unerase(val any) Result[T]
} }
// InjectionToken uniquely identifies a dependency by giving it an Id, Type and name // InjectionToken uniquely identifies a dependency by giving it an Id, Type and name
@@ -42,17 +39,17 @@ type InjectionToken[T any] interface {
// Identity idenifies this dependency as a mandatory, required dependency, it will be resolved eagerly and injected as `T`. // Identity idenifies this dependency as a mandatory, required dependency, it will be resolved eagerly and injected as `T`.
// If the dependency cannot be resolved, the resolution process fails // If the dependency cannot be resolved, the resolution process fails
Identity() Dependency[T] Identity() Dependency[T]
// Option identifies this dependency as optional, it will be resolved eagerly and injected as [O.Option[T]]. // Option identifies this dependency as optional, it will be resolved eagerly and injected as [Option[T]].
// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as [O.None[T]] // If the dependency cannot be resolved, the resolution process continues and the dependency is represented as [O.None[T]]
Option() Dependency[O.Option[T]] Option() Dependency[Option[T]]
// IOEither identifies this dependency as mandatory but it will be resolved lazily as a [IOE.IOEither[error, T]]. This // IOEither identifies this dependency as mandatory but it will be resolved lazily as a [IOResult[T]]. This
// value is memoized to make sure the dependency is a singleton. // value is memoized to make sure the dependency is a singleton.
// If the dependency cannot be resolved, the resolution process fails // If the dependency cannot be resolved, the resolution process fails
IOEither() Dependency[IOE.IOEither[error, T]] IOEither() Dependency[IOResult[T]]
// IOOption identifies this dependency as optional but it will be resolved lazily as a [IOO.IOOption[T]]. This // IOOption identifies this dependency as optional but it will be resolved lazily as a [IOOption[T]]. This
// value is memoized to make sure the dependency is a singleton. // value is memoized to make sure the dependency is a singleton.
// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as the none value. // If the dependency cannot be resolved, the resolution process continues and the dependency is represented as the none value.
IOOption() Dependency[IOO.IOOption[T]] IOOption() Dependency[IOOption[T]]
} }
// MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name that can have multiple implementations. // MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name that can have multiple implementations.
@@ -79,12 +76,12 @@ type tokenBase struct {
name string name string
id string id string
flag int flag int
providerFactory O.Option[DIE.ProviderFactory] providerFactory Option[DIE.ProviderFactory]
} }
type token[T any] struct { type token[T any] struct {
base *tokenBase base *tokenBase
toType func(val any) E.Either[error, T] toType func(val any) Result[T]
} }
func (t *token[T]) Id() string { func (t *token[T]) Id() string {
@@ -99,26 +96,26 @@ func (t *token[T]) String() string {
return t.base.name return t.base.name
} }
func (t *token[T]) Unerase(val any) E.Either[error, T] { func (t *token[T]) Unerase(val any) Result[T] {
return t.toType(val) return t.toType(val)
} }
func (t *token[T]) ProviderFactory() O.Option[DIE.ProviderFactory] { func (t *token[T]) ProviderFactory() Option[DIE.ProviderFactory] {
return t.base.providerFactory return t.base.providerFactory
} }
func makeTokenBase(name string, id string, typ int, providerFactory O.Option[DIE.ProviderFactory]) *tokenBase { func makeTokenBase(name string, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
return &tokenBase{name, id, typ, providerFactory} return &tokenBase{name, id, typ, providerFactory}
} }
func makeToken[T any](name string, id string, typ int, unerase func(val any) E.Either[error, T], providerFactory O.Option[DIE.ProviderFactory]) Dependency[T] { func makeToken[T any](name string, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase} return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
} }
type injectionToken[T any] struct { type injectionToken[T any] struct {
token[T] token[T]
option Dependency[O.Option[T]] option Dependency[Option[T]]
ioeither Dependency[IOE.IOEither[error, T]] ioeither Dependency[IOResult[T]]
iooption Dependency[IOO.IOOption[T]] iooption Dependency[IOOption[T]]
} }
type multiInjectionToken[T any] struct { type multiInjectionToken[T any] struct {
@@ -130,19 +127,19 @@ func (i *injectionToken[T]) Identity() Dependency[T] {
return i return i
} }
func (i *injectionToken[T]) Option() Dependency[O.Option[T]] { func (i *injectionToken[T]) Option() Dependency[Option[T]] {
return i.option return i.option
} }
func (i *injectionToken[T]) IOEither() Dependency[IOE.IOEither[error, T]] { func (i *injectionToken[T]) IOEither() Dependency[IOResult[T]] {
return i.ioeither return i.ioeither
} }
func (i *injectionToken[T]) IOOption() Dependency[IOO.IOOption[T]] { func (i *injectionToken[T]) IOOption() Dependency[IOOption[T]] {
return i.iooption return i.iooption
} }
func (i *injectionToken[T]) ProviderFactory() O.Option[DIE.ProviderFactory] { func (i *injectionToken[T]) ProviderFactory() Option[DIE.ProviderFactory] {
return i.base.providerFactory return i.base.providerFactory
} }
@@ -155,14 +152,14 @@ func (m *multiInjectionToken[T]) Item() InjectionToken[T] {
} }
// makeToken create a unique [InjectionToken] for a specific type // makeToken create a unique [InjectionToken] for a specific type
func makeInjectionToken[T any](name string, providerFactory O.Option[DIE.ProviderFactory]) InjectionToken[T] { func makeInjectionToken[T any](name string, providerFactory Option[DIE.ProviderFactory]) InjectionToken[T] {
id := genID() id := genID()
toIdentity := toType[T]() toIdentity := toType[T]()
return &injectionToken[T]{ return &injectionToken[T]{
token[T]{makeTokenBase(name, id, DIE.Identity, providerFactory), toIdentity}, token[T]{makeTokenBase(name, id, DIE.IDENTITY, providerFactory), toIdentity},
makeToken(fmt.Sprintf("Option[%s]", name), id, DIE.Option, toOptionType(toIdentity), providerFactory), makeToken(fmt.Sprintf("Option[%s]", name), id, DIE.OPTION, toOptionType(toIdentity), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither, toIOEitherType(toIdentity), providerFactory), makeToken(fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEITHER, toIOEitherType(toIdentity), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption, toIOOptionType(toIdentity), providerFactory), makeToken(fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOPTION, toIOOptionType(toIdentity), providerFactory),
} }
} }
@@ -187,17 +184,17 @@ func MakeMultiToken[T any](name string) MultiInjectionToken[T] {
providerFactory := O.None[DIE.ProviderFactory]() providerFactory := O.None[DIE.ProviderFactory]()
// container // container
container := &injectionToken[[]T]{ container := &injectionToken[[]T]{
token[[]T]{makeTokenBase(containerName, id, DIE.Multi|DIE.Identity, providerFactory), toContainer}, token[[]T]{makeTokenBase(containerName, id, DIE.MULTI|DIE.IDENTITY, providerFactory), toContainer},
makeToken(fmt.Sprintf("Option[%s]", containerName), id, DIE.Multi|DIE.Option, toOptionType(toContainer), providerFactory), makeToken(fmt.Sprintf("Option[%s]", containerName), id, DIE.MULTI|DIE.OPTION, toOptionType(toContainer), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", containerName), id, DIE.Multi|DIE.IOEither, toIOEitherType(toContainer), providerFactory), makeToken(fmt.Sprintf("IOEither[%s]", containerName), id, DIE.OPTION|DIE.IOEITHER, toIOEitherType(toContainer), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Multi|DIE.IOOption, toIOOptionType(toContainer), providerFactory), makeToken(fmt.Sprintf("IOOption[%s]", containerName), id, DIE.OPTION|DIE.IOOPTION, toIOOptionType(toContainer), providerFactory),
} }
// item // item
item := &injectionToken[T]{ item := &injectionToken[T]{
token[T]{makeTokenBase(itemName, id, DIE.Item|DIE.Identity, providerFactory), toItem}, token[T]{makeTokenBase(itemName, id, DIE.ITEM|DIE.IDENTITY, providerFactory), toItem},
makeToken(fmt.Sprintf("Option[%s]", itemName), id, DIE.Item|DIE.Option, toOptionType(toItem), providerFactory), makeToken(fmt.Sprintf("Option[%s]", itemName), id, DIE.ITEM|DIE.OPTION, toOptionType(toItem), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Item|DIE.IOEither, toIOEitherType(toItem), providerFactory), makeToken(fmt.Sprintf("IOEither[%s]", itemName), id, DIE.ITEM|DIE.IOEITHER, toIOEitherType(toItem), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Item|DIE.IOOption, toIOOptionType(toItem), providerFactory), makeToken(fmt.Sprintf("IOOption[%s]", itemName), id, DIE.ITEM|DIE.IOOPTION, toIOOptionType(toItem), providerFactory),
} }
// returns the token // returns the token
return &multiInjectionToken[T]{container, item} return &multiInjectionToken[T]{container, item}

View File

@@ -23,7 +23,9 @@ import (
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither" IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption" IOO "github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -75,9 +77,9 @@ func TestTokenUnerase(t *testing.T) {
token := MakeToken[int]("IntToken") token := MakeToken[int]("IntToken")
// Test successful unerase // Test successful unerase
result := token.Unerase(42) res := token.Unerase(42)
assert.True(t, E.IsRight(result)) assert.True(t, E.IsRight(res))
assert.Equal(t, E.Of[error](42), result) assert.Equal(t, result.Of(42), res)
// Test failed unerase (wrong type) // Test failed unerase (wrong type)
result2 := token.Unerase("not an int") result2 := token.Unerase("not an int")
@@ -104,7 +106,7 @@ func TestTokenProviderFactory(t *testing.T) {
assert.True(t, O.IsNone(token1.ProviderFactory())) assert.True(t, O.IsNone(token1.ProviderFactory()))
// Token with default // Token with default
token2 := MakeTokenWithDefault0("Token2", IOE.Of[error](42)) token2 := MakeTokenWithDefault0("Token2", ioresult.Of(42))
assert.True(t, O.IsSome(token2.ProviderFactory())) assert.True(t, O.IsSome(token2.ProviderFactory()))
} }
@@ -148,13 +150,13 @@ func TestOptionTokenUnerase(t *testing.T) {
optionToken := token.Option() optionToken := token.Option()
// Test successful unerase with Some // Test successful unerase with Some
result := optionToken.Unerase(O.Of[any](42)) res := optionToken.Unerase(O.Of[any](42))
assert.True(t, E.IsRight(result)) assert.True(t, E.IsRight(res))
// Test successful unerase with None // Test successful unerase with None
noneResult := optionToken.Unerase(O.None[any]()) noneResult := optionToken.Unerase(O.None[any]())
assert.True(t, E.IsRight(noneResult)) assert.True(t, E.IsRight(noneResult))
assert.Equal(t, E.Of[error](O.None[int]()), noneResult) assert.Equal(t, result.Of(O.None[int]()), noneResult)
// Test failed unerase (wrong type) // Test failed unerase (wrong type)
badResult := optionToken.Unerase(42) // Not an Option badResult := optionToken.Unerase(42) // Not an Option
@@ -166,7 +168,7 @@ func TestIOEitherTokenUnerase(t *testing.T) {
ioeitherToken := token.IOEither() ioeitherToken := token.IOEither()
// Test successful unerase // Test successful unerase
ioValue := IOE.Of[error](any(42)) ioValue := ioresult.Of(any(42))
result := ioeitherToken.Unerase(ioValue) result := ioeitherToken.Unerase(ioValue)
assert.True(t, E.IsRight(result)) assert.True(t, E.IsRight(result))
@@ -222,7 +224,7 @@ func TestMultiTokenContainerUnerase(t *testing.T) {
} }
func TestMakeTokenWithDefault(t *testing.T) { func TestMakeTokenWithDefault(t *testing.T) {
factory := MakeProviderFactory0(IOE.Of[error](42)) factory := MakeProviderFactory0(ioresult.Of(42))
token := MakeTokenWithDefault[int]("TokenWithDefault", factory) token := MakeTokenWithDefault[int]("TokenWithDefault", factory)
assert.NotNil(t, token) assert.NotNil(t, token)
@@ -247,7 +249,7 @@ func TestMultiTokenStringRepresentation(t *testing.T) {
// Benchmark tests // Benchmark tests
func BenchmarkMakeToken(b *testing.B) { func BenchmarkMakeToken(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
MakeToken[int]("BenchToken") MakeToken[int]("BenchToken")
} }
} }
@@ -257,13 +259,13 @@ func BenchmarkTokenUnerase(b *testing.B) {
value := any(42) value := any(42)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for b.Loop() {
token.Unerase(value) token.Unerase(value)
} }
} }
func BenchmarkMakeMultiToken(b *testing.B) { func BenchmarkMakeMultiToken(b *testing.B) {
for i := 0; i < b.N; i++ { for b.Loop() {
MakeMultiToken[int]("BenchMulti") MakeMultiToken[int]("BenchMulti")
} }
} }

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

@@ -0,0 +1,15 @@
package di
import (
"github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
)

View File

@@ -23,12 +23,13 @@ import (
IOE "github.com/IBM/fp-go/v2/ioeither" IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption" IOO "github.com/IBM/fp-go/v2/iooption"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
) )
var ( var (
toOptionAny = toType[O.Option[any]]() toOptionAny = toType[Option[any]]()
toIOEitherAny = toType[IOE.IOEither[error, any]]() toIOEitherAny = toType[IOResult[any]]()
toIOOptionAny = toType[IOO.IOOption[any]]() toIOOptionAny = toType[IOOption[any]]()
toArrayAny = toType[[]any]() toArrayAny = toType[[]any]()
) )
@@ -38,45 +39,45 @@ func asDependency[T DIE.Dependency](t T) DIE.Dependency {
} }
// toType converts an any to a T // toType converts an any to a T
func toType[T any]() func(t any) E.Either[error, T] { func toType[T any]() result.Kleisli[any, T] {
return E.ToType[T](errors.OnSome[any]("Value of type [%T] cannot be converted.")) return E.ToType[T](errors.OnSome[any]("Value of type [%T] cannot be converted."))
} }
// toOptionType converts an any to an Option[any] and then to an Option[T] // toOptionType converts an any to an Option[any] and then to an Option[T]
func toOptionType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, O.Option[T]] { func toOptionType[T any](item result.Kleisli[any, T]) result.Kleisli[any, Option[T]] {
return F.Flow2( return F.Flow2(
toOptionAny, toOptionAny,
E.Chain(O.Fold( E.Chain(O.Fold(
F.Nullary2(O.None[T], E.Of[error, O.Option[T]]), F.Nullary2(O.None[T], E.Of[error, Option[T]]),
F.Flow2( F.Flow2(
item, item,
E.Map[error](O.Of[T]), result.Map(O.Of[T]),
), ),
)), )),
) )
} }
// toIOEitherType converts an any to an IOEither[error, any] and then to an IOEither[error, T] // toIOEitherType converts an any to an IOEither[error, any] and then to an IOEither[error, T]
func toIOEitherType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, IOE.IOEither[error, T]] { func toIOEitherType[T any](item result.Kleisli[any, T]) result.Kleisli[any, IOResult[T]] {
return F.Flow2( return F.Flow2(
toIOEitherAny, toIOEitherAny,
E.Map[error](IOE.ChainEitherK(item)), result.Map(IOE.ChainEitherK(item)),
) )
} }
// toIOOptionType converts an any to an IOOption[any] and then to an IOOption[T] // toIOOptionType converts an any to an IOOption[any] and then to an IOOption[T]
func toIOOptionType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, IOO.IOOption[T]] { func toIOOptionType[T any](item result.Kleisli[any, T]) result.Kleisli[any, IOOption[T]] {
return F.Flow2( return F.Flow2(
toIOOptionAny, toIOOptionAny,
E.Map[error](IOO.ChainOptionK(F.Flow2( result.Map(IOO.ChainOptionK(F.Flow2(
item, item,
E.ToOption[error, T], result.ToOption[T],
))), ))),
) )
} }
// toArrayType converts an any to a []T // toArrayType converts an any to a []T
func toArrayType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, []T] { func toArrayType[T any](item result.Kleisli[any, T]) result.Kleisli[any, []T] {
return F.Flow2( return F.Flow2(
toArrayAny, toArrayAny,
E.Chain(E.TraverseArray(item)), E.Chain(E.TraverseArray(item)),

View File

@@ -21,8 +21,9 @@ import (
A "github.com/IBM/fp-go/v2/array" A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either" E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither" "github.com/IBM/fp-go/v2/ioresult"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -33,13 +34,13 @@ var (
func TestToType(t *testing.T) { func TestToType(t *testing.T) {
// good cases // good cases
assert.Equal(t, E.Of[error](10), toInt(any(10))) assert.Equal(t, result.Of(10), toInt(any(10)))
assert.Equal(t, E.Of[error]("Carsten"), toString(any("Carsten"))) assert.Equal(t, result.Of("Carsten"), toString(any("Carsten")))
assert.Equal(t, E.Of[error](O.Of("Carsten")), toType[O.Option[string]]()(any(O.Of("Carsten")))) assert.Equal(t, result.Of(O.Of("Carsten")), toType[Option[string]]()(any(O.Of("Carsten"))))
assert.Equal(t, E.Of[error](O.Of(any("Carsten"))), toType[O.Option[any]]()(any(O.Of(any("Carsten"))))) assert.Equal(t, result.Of(O.Of(any("Carsten"))), toType[Option[any]]()(any(O.Of(any("Carsten")))))
// failure // failure
assert.False(t, E.IsRight(toInt(any("Carsten")))) assert.False(t, E.IsRight(toInt(any("Carsten"))))
assert.False(t, E.IsRight(toType[O.Option[string]]()(O.Of(any("Carsten"))))) assert.False(t, E.IsRight(toType[Option[string]]()(O.Of(any("Carsten")))))
} }
func TestToOptionType(t *testing.T) { func TestToOptionType(t *testing.T) {
@@ -47,17 +48,17 @@ func TestToOptionType(t *testing.T) {
toOptInt := toOptionType(toInt) toOptInt := toOptionType(toInt)
toOptString := toOptionType(toString) toOptString := toOptionType(toString)
// good cases // good cases
assert.Equal(t, E.Of[error](O.Of(10)), toOptInt(any(O.Of(any(10))))) assert.Equal(t, result.Of(O.Of(10)), toOptInt(any(O.Of(any(10)))))
assert.Equal(t, E.Of[error](O.Of("Carsten")), toOptString(any(O.Of(any("Carsten"))))) assert.Equal(t, result.Of(O.Of("Carsten")), toOptString(any(O.Of(any("Carsten")))))
// bad cases // bad cases
assert.False(t, E.IsRight(toOptInt(any(10)))) assert.False(t, E.IsRight(toOptInt(any(10))))
assert.False(t, E.IsRight(toOptInt(any(O.Of(10))))) assert.False(t, E.IsRight(toOptInt(any(O.Of(10)))))
} }
func invokeIOEither[T any](e E.Either[error, IOE.IOEither[error, T]]) E.Either[error, T] { func invokeIOEither[T any](e Result[IOResult[T]]) Result[T] {
return F.Pipe1( return F.Pipe1(
e, e,
E.Chain(func(ioe IOE.IOEither[error, T]) E.Either[error, T] { E.Chain(func(ioe IOResult[T]) Result[T] {
return ioe() return ioe()
}), }),
) )
@@ -68,11 +69,11 @@ func TestToIOEitherType(t *testing.T) {
toIOEitherInt := toIOEitherType(toInt) toIOEitherInt := toIOEitherType(toInt)
toIOEitherString := toIOEitherType(toString) toIOEitherString := toIOEitherType(toString)
// good cases // good cases
assert.Equal(t, E.Of[error](10), invokeIOEither(toIOEitherInt(any(IOE.Of[error](any(10)))))) assert.Equal(t, result.Of(10), invokeIOEither(toIOEitherInt(any(ioresult.Of(any(10))))))
assert.Equal(t, E.Of[error]("Carsten"), invokeIOEither(toIOEitherString(any(IOE.Of[error](any("Carsten")))))) assert.Equal(t, result.Of("Carsten"), invokeIOEither(toIOEitherString(any(ioresult.Of(any("Carsten"))))))
// bad cases // bad cases
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(IOE.Of[error](any(10))))))) assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(ioresult.Of(any(10)))))))
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(IOE.Of[error]("Carsten")))))) assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(ioresult.Of("Carsten"))))))
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any("Carsten"))))) assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any("Carsten")))))
} }
@@ -80,5 +81,5 @@ func TestToArrayType(t *testing.T) {
// shortcuts // shortcuts
toArrayString := toArrayType(toString) toArrayString := toArrayType(toString)
// good cases // good cases
assert.Equal(t, E.Of[error](A.From("a", "b")), toArrayString(any(A.From(any("a"), any("b"))))) assert.Equal(t, result.Of(A.From("a", "b")), toArrayString(any(A.From(any("a"), any("b")))))
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,7 +66,7 @@ func TestUnwrapError(t *testing.T) {
func TestReduce(t *testing.T) { func TestReduce(t *testing.T) {
s := S.Semigroup() s := S.Semigroup
assert.Equal(t, "foobar", F.Pipe1(Right[string]("bar"), Reduce[string](s.Concat, "foo"))) assert.Equal(t, "foobar", F.Pipe1(Right[string]("bar"), Reduce[string](s.Concat, "foo")))
assert.Equal(t, "foo", F.Pipe1(Left[string]("bar"), Reduce[string](s.Concat, "foo"))) assert.Equal(t, "foo", F.Pipe1(Left[string]("bar"), Reduce[string](s.Concat, "foo")))
@@ -76,8 +76,8 @@ func TestAp(t *testing.T) {
f := S.Size f := S.Size
assert.Equal(t, Right[string](3), F.Pipe1(Right[string](f), Ap[int](Right[string]("abc")))) assert.Equal(t, Right[string](3), F.Pipe1(Right[string](f), Ap[int](Right[string]("abc"))))
assert.Equal(t, Left[int]("maError"), F.Pipe1(Right[string](f), Ap[int](Left[string, string]("maError")))) assert.Equal(t, Left[int]("maError"), F.Pipe1(Right[string](f), Ap[int](Left[string]("maError"))))
assert.Equal(t, Left[int]("mabError"), F.Pipe1(Left[func(string) int]("mabError"), Ap[int](Left[string, string]("maError")))) assert.Equal(t, Left[int]("mabError"), F.Pipe1(Left[func(string) int]("mabError"), Ap[int](Left[string]("maError"))))
} }
func TestAlt(t *testing.T) { func TestAlt(t *testing.T) {

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

@@ -0,0 +1,33 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package either
// Test functions to analyze escape behavior
//go:noinline
func testOf(x int) Either[error, int] {
return Of[error](x)
}
//go:noinline
func testRight(x int) Either[error, int] {
return Right[error](x)
}
//go:noinline
func testLeft(x int) Either[int, string] {
return Left[string](x)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,144 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package either
import (
F "github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
// MonadApV is the applicative validation functor that combines errors using a semigroup.
//
// Unlike the standard [MonadAp] which short-circuits on the first Left (error),
// MonadApV accumulates all errors using the provided semigroup's Concat operation.
// This is particularly useful for validation scenarios where you want to collect
// all validation errors rather than stopping at the first one.
//
// The function takes a semigroup for combining errors and returns a function that
// applies a wrapped function to a wrapped value, accumulating errors if both are Left.
//
// Behavior:
// - If both fab and fa are Left, combines their errors using sg.Concat
// - If only fab is Left, returns Left with fab's error
// - If only fa is Left, returns Left with fa's error
// - If both are Right, applies the function and returns Right with the result
//
// Type Parameters:
// - B: The result type after applying the function
// - E: The error type (must support the semigroup operation)
// - A: The input type to the function
//
// Parameters:
// - sg: A semigroup that defines how to combine two error values
//
// Returns:
// - A function that takes a wrapped function and a wrapped value, returning
// Either[E, B] with accumulated errors or the computed result
//
// Example:
//
// // Define a semigroup that concatenates error messages
// errorSemigroup := semigroup.MakeSemigroup(func(e1, e2 string) string {
// return e1 + "; " + e2
// })
//
// // Create the validation applicative
// applyV := either.MonadApV[int](errorSemigroup)
//
// // Both are errors - errors get combined
// fab := either.Left[func(int) int]("error1")
// fa := either.Left[int]("error2")
// result := applyV(fab, fa) // Left("error1; error2")
//
// // One error - returns that error
// fab2 := either.Right[string](N.Mul(2))
// fa2 := either.Left[int]("validation failed")
// result2 := applyV(fab2, fa2) // Left("validation failed")
//
// // Both success - applies function
// fab3 := either.Right[string](N.Mul(2))
// fa3 := either.Right[string](21)
// result3 := applyV(fab3, fa3) // Right(42)
func MonadApV[B, A, E any](sg S.Semigroup[E]) func(fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
return func(fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
if fab.isLeft {
if fa.isLeft {
return Left[B](sg.Concat(fab.l, fa.l))
}
return Left[B](fab.l)
}
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](fab.r(fa.r))
}
}
// ApV is the curried version of [MonadApV] that combines errors using a semigroup.
//
// This function provides a more convenient API for validation scenarios by currying
// the arguments. It first takes the value to validate, then returns a function that
// takes the validation function. This allows for a more natural composition style.
//
// Like [MonadApV], this accumulates all errors using the provided semigroup instead
// of short-circuiting on the first error. This is the key difference from the
// standard [Ap] function.
//
// Type Parameters:
// - B: The result type after applying the function
// - E: The error type (must support the semigroup operation)
// - A: The input type to the function
//
// Parameters:
// - sg: A semigroup that defines how to combine two error values
//
// Returns:
// - A function that takes a value Either[E, A] and returns an Operator that
// applies validation functions while accumulating errors
//
// Example:
//
// // Define a semigroup for combining validation errors
// type ValidationError struct {
// Errors []string
// }
// errorSemigroup := semigroup.MakeSemigroup(func(e1, e2 ValidationError) ValidationError {
// return ValidationError{Errors: append(e1.Errors, e2.Errors...)}
// })
//
// // Create validators
// validatePositive := func(x int) either.Either[ValidationError, int] {
// if x > 0 {
// return either.Right[ValidationError](x)
// }
// return either.Left[int](ValidationError{Errors: []string{"must be positive"}})
// }
//
// // Use ApV for validation
// applyValidation := either.ApV[int](errorSemigroup)
// value := either.Left[int](ValidationError{Errors: []string{"invalid input"}})
// validator := either.Left[func(int) int](ValidationError{Errors: []string{"invalid validator"}})
//
// result := applyValidation(value)(validator)
// // Left(ValidationError{Errors: []string{"invalid validator", "invalid input"}})
//
//go:inline
func ApV[B, A, E any](sg S.Semigroup[E]) func(Either[E, A]) Operator[E, func(A) B, B] {
apv := MonadApV[B, A](sg)
return func(e Either[E, A]) Operator[E, func(A) B, B] {
return F.Bind2nd(apv, e)
}
}

View File

@@ -0,0 +1,362 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package either
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
SG "github.com/IBM/fp-go/v2/semigroup"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestMonadApV_BothRight tests MonadApV when both function and value are Right
func TestMonadApV_BothRight(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Both are Right - should apply function
fab := Right[string](N.Mul(2))
fa := Right[string](21)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string](42), result)
}
// TestMonadApV_BothLeft tests MonadApV when both function and value are Left
func TestMonadApV_BothLeft(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Both are Left - should combine errors
fab := Left[func(int) int]("error1")
fa := Left[int]("error2")
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
assert.Equal(t, Left[int]("error1; error2"), result)
}
// TestMonadApV_LeftFunction tests MonadApV when function is Left and value is Right
func TestMonadApV_LeftFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Function is Left, value is Right - should return function's error
fab := Left[func(int) int]("function error")
fa := Right[string](21)
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("function error"), result)
}
// TestMonadApV_LeftValue tests MonadApV when function is Right and value is Left
func TestMonadApV_LeftValue(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := MonadApV[int, int](sg)
// Function is Right, value is Left - should return value's error
fab := Right[string](N.Mul(2))
fa := Left[int]("value error")
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("value error"), result)
}
// TestMonadApV_WithSliceSemigroup tests MonadApV with a slice-based semigroup
func TestMonadApV_WithSliceSemigroup(t *testing.T) {
// Create a semigroup that concatenates slices
sg := SG.MakeSemigroup(func(a, b []string) []string {
return append(a, b...)
})
// Create the validation applicative
applyV := MonadApV[string, string](sg)
// Both are Left with slice errors
fab := Left[func(string) string]([]string{"error1", "error2"})
fa := Left[string]([]string{"error3", "error4"})
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa errors + fab errors
expected := Left[string]([]string{"error1", "error2", "error3", "error4"})
assert.Equal(t, expected, result)
}
// TestMonadApV_ComplexFunction tests MonadApV with a more complex function
func TestMonadApV_ComplexFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := SG.MakeSemigroup(func(a, b string) string {
return a + " | " + b
})
// Create the validation applicative
applyV := MonadApV[string, int](sg)
// Test with a function that transforms the value
fab := Right[string](func(x int) string {
if x > 0 {
return "positive"
}
return "non-positive"
})
fa := Right[string](42)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("positive"), result)
}
// TestApV_BothRight tests ApV when both function and value are Right
func TestApV_BothRight(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Right - should apply function
fa := Right[string](21)
fab := Right[string](N.Mul(2))
result := applyV(fa)(fab)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string](42), result)
}
// TestApV_BothLeft tests ApV when both function and value are Left
func TestApV_BothLeft(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Left - should combine errors
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
assert.Equal(t, Left[int]("error1; error2"), result)
}
// TestApV_LeftFunction tests ApV when function is Left and value is Right
func TestApV_LeftFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Function is Left, value is Right - should return function's error
fa := Right[string](21)
fab := Left[func(int) int]("function error")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("function error"), result)
}
// TestApV_LeftValue tests ApV when function is Right and value is Left
func TestApV_LeftValue(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup("; ")
// Create the validation applicative
applyV := ApV[int, int](sg)
// Function is Right, value is Left - should return value's error
fa := Left[int]("value error")
fab := Right[string](N.Mul(2))
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("value error"), result)
}
// TestApV_Composition tests ApV with function composition
func TestApV_Composition(t *testing.T) {
// Create a semigroup for string concatenation
sg := SG.MakeSemigroup(func(a, b string) string {
return a + " & " + b
})
// Create the validation applicative
applyV := ApV[string, int](sg)
// Test composition with pipe
fa := Right[string](10)
fab := Right[string](func(x int) string {
return F.Pipe1(x, func(n int) string {
if n >= 10 {
return "large"
}
return "small"
})
})
result := F.Pipe1(fa, applyV)(fab)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("large"), result)
}
// TestApV_WithStructSemigroup tests ApV with a custom struct semigroup
func TestApV_WithStructSemigroup(t *testing.T) {
type ValidationErrors struct {
Errors []string
}
// Create a semigroup that combines validation errors
sg := SG.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
return ValidationErrors{
Errors: append(append([]string{}, a.Errors...), b.Errors...),
}
})
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Left with validation errors
fa := Left[int](ValidationErrors{Errors: []string{"field2: invalid"}})
fab := Left[func(int) int](ValidationErrors{Errors: []string{"field1: required"}})
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa errors + fab errors
expected := Left[int](ValidationErrors{
Errors: []string{"field1: required", "field2: invalid"},
})
assert.Equal(t, expected, result)
}
// TestApV_MultipleValidations tests ApV with multiple validation steps
func TestApV_MultipleValidations(t *testing.T) {
// Create a semigroup for string concatenation
sg := SG.MakeSemigroup(func(a, b string) string {
return a + ", " + b
})
// Create the validation applicative
applyV := ApV[int, int](sg)
// Simulate multiple validation failures
validation1 := Left[int]("age must be positive")
validation2 := Left[func(int) int]("name is required")
result := applyV(validation1)(validation2)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: validation1 error + validation2 error
assert.Equal(t, Left[int]("name is required, age must be positive"), result)
}
// TestMonadApV_DifferentTypes tests MonadApV with different input and output types
func TestMonadApV_DifferentTypes(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.IntersperseSemigroup(" + ")
// Create the validation applicative
applyV := MonadApV[string, int](sg)
// Function converts int to string
fab := Right[string](func(x int) string {
return F.Pipe1(x, func(n int) string {
if n == 0 {
return "zero"
} else if n > 0 {
return "positive"
}
return "negative"
})
})
fa := Right[string](-5)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("negative"), result)
}
// TestApV_FirstSemigroup tests ApV with First semigroup (always returns first error)
func TestApV_FirstSemigroup(t *testing.T) {
// Use First semigroup which always returns the first value
sg := SG.First[string]()
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Left - should return first error
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// First semigroup returns the first value, which is fab's error
assert.Equal(t, Left[int]("error1"), result)
}
// TestApV_LastSemigroup tests ApV with Last semigroup (always returns last error)
func TestApV_LastSemigroup(t *testing.T) {
// Use Last semigroup which always returns the last value
sg := SG.Last[string]()
// Create the validation applicative
applyV := ApV[int, int](sg)
// Both are Left - should return last error
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// Last semigroup returns the last value, which is fa's error
assert.Equal(t, Left[int]("error2"), result)
}

View File

@@ -36,16 +36,21 @@
// ) // )
// //
// // Define some endomorphisms // // Define some endomorphisms
// double := func(x int) int { return x * 2 } // double := N.Mul(2)
// increment := func(x int) int { return x + 1 } // increment := func(x int) int { return x + 1 }
// //
// // Compose them // // Compose them (RIGHT-TO-LEFT execution)
// doubleAndIncrement := endomorphism.Compose(double, increment) // composed := endomorphism.Compose(double, increment)
// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11 // result := composed(5) // increment(5) then double: (5 + 1) * 2 = 12
//
// // Chain them (LEFT-TO-RIGHT execution)
// chained := endomorphism.MonadChain(double, increment)
// result2 := chained(5) // double(5) then increment: (5 * 2) + 1 = 11
// //
// # Monoid Operations // # Monoid Operations
// //
// Endomorphisms form a monoid, which means you can combine multiple endomorphisms: // Endomorphisms form a monoid, which means you can combine multiple endomorphisms.
// The monoid uses Compose, which executes RIGHT-TO-LEFT:
// //
// import ( // import (
// "github.com/IBM/fp-go/v2/endomorphism" // "github.com/IBM/fp-go/v2/endomorphism"
@@ -55,22 +60,39 @@
// // Get the monoid for int endomorphisms // // Get the monoid for int endomorphisms
// monoid := endomorphism.Monoid[int]() // monoid := endomorphism.Monoid[int]()
// //
// // Combine multiple endomorphisms // // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)( // combined := M.ConcatAll(monoid)(
// func(x int) int { return x * 2 }, // N.Mul(2), // applied third
// func(x int) int { return x + 1 }, // func(x int) int { return x + 1 }, // applied second
// func(x int) int { return x * 3 }, // func(x int) int { return x * 3 }, // applied first
// ) // )
// result := combined(5) // ((5 * 2) + 1) * 3 = 33 // result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
// //
// # Monad Operations // # Monad Operations
// //
// The package also provides monadic operations for endomorphisms: // The package also provides monadic operations for endomorphisms.
// MonadChain executes LEFT-TO-RIGHT, unlike Compose:
// //
// // Chain allows sequencing of endomorphisms // // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
// f := func(x int) int { return x * 2 } // f := N.Mul(2)
// g := func(x int) int { return x + 1 } // g := func(x int) int { return x + 1 }
// chained := endomorphism.MonadChain(f, g) // chained := endomorphism.MonadChain(f, g) // f first, then g
// result := chained(5) // (5 * 2) + 1 = 11
//
// # Compose vs Chain
//
// The key difference between Compose and Chain/MonadChain is execution order:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // Compose: RIGHT-TO-LEFT (mathematical composition)
// composed := endomorphism.Compose(double, increment)
// result1 := composed(5) // increment(5) * 2 = (5 + 1) * 2 = 12
//
// // MonadChain: LEFT-TO-RIGHT (sequential application)
// chained := endomorphism.MonadChain(double, increment)
// result2 := chained(5) // double(5) + 1 = (5 * 2) + 1 = 11
// //
// # Type Safety // # Type Safety
// //

View File

@@ -17,115 +17,372 @@ package endomorphism
import ( import (
"github.com/IBM/fp-go/v2/function" "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
) )
// MonadAp applies an endomorphism to a value in a monadic context. // MonadAp applies an endomorphism in a function to an endomorphism value.
// //
// This function applies the endomorphism fab to the value fa, returning the result. // For endomorphisms, Ap composes two endomorphisms using RIGHT-TO-LEFT composition.
// It's the monadic application operation for endomorphisms. // This is the applicative functor operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as MonadCompose):
// - fa is applied first to the input
// - fab is applied to the result
// //
// Parameters: // Parameters:
// - fab: An endomorphism to apply // - fab: An endomorphism to apply (outer function)
// - fa: The value to apply the endomorphism to // - fa: An endomorphism to apply first (inner function)
// //
// Returns: // Returns:
// - The result of applying fab to fa // - A new endomorphism that applies fa, then fab
// //
// Example: // Example:
// //
// double := func(x int) int { return x * 2 } // double := N.Mul(2)
// result := endomorphism.MonadAp(double, 5) // Returns: 10
func MonadAp[A any](fab Endomorphism[A], fa A) A {
return identity.MonadAp(fab, fa)
}
// Ap returns a function that applies a value to an endomorphism.
//
// This is the curried version of MonadAp. It takes a value and returns a function
// that applies that value to any endomorphism.
//
// Parameters:
// - fa: The value to be applied
//
// Returns:
// - A function that takes an endomorphism and applies fa to it
//
// Example:
//
// applyFive := endomorphism.Ap(5)
// double := func(x int) int { return x * 2 }
// result := applyFive(double) // Returns: 10
func Ap[A any](fa A) func(Endomorphism[A]) A {
return identity.Ap[A](fa)
}
// Compose composes two endomorphisms into a single endomorphism.
//
// Given two endomorphisms f1 and f2, Compose returns a new endomorphism that
// applies f1 first, then applies f2 to the result. This is function composition:
// Compose(f1, f2)(x) = f2(f1(x))
//
// Composition is associative: Compose(Compose(f, g), h) = Compose(f, Compose(g, h))
//
// Parameters:
// - f1: The first endomorphism to apply
// - f2: The second endomorphism to apply
//
// Returns:
// - A new endomorphism that is the composition of f1 and f2
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 } // increment := func(x int) int { return x + 1 }
// doubleAndIncrement := endomorphism.Compose(double, increment) // result := endomorphism.MonadAp(double, increment) // Composes: double increment
// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11 // // result(5) = double(increment(5)) = double(6) = 12
func Compose[A any](f1, f2 Endomorphism[A]) Endomorphism[A] { func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
return function.Flow2(f1, f2) return MonadCompose(fab, fa)
} }
// MonadChain chains two endomorphisms together. // Ap returns a function that applies an endomorphism to another endomorphism.
// //
// This is the monadic bind operation for endomorphisms. It composes two endomorphisms // This is the curried version of MonadAp. It takes an endomorphism fa and returns
// ma and f, returning a new endomorphism that applies ma first, then f. // a function that composes any endomorphism with fa using RIGHT-TO-LEFT composition.
// MonadChain is equivalent to Compose. //
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - fa is applied first to the input
// - The endomorphism passed to the returned function is applied to the result
// //
// Parameters: // Parameters:
// - ma: The first endomorphism in the chain // - fa: The first endomorphism to apply (inner function)
// - f: The second endomorphism in the chain
// //
// Returns: // Returns:
// - A new endomorphism that chains ma and f // - A function that takes an endomorphism and composes it with fa (right-to-left)
// //
// Example: // Example:
// //
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 } // increment := func(x int) int { return x + 1 }
// applyIncrement := endomorphism.Ap(increment)
// double := N.Mul(2)
// composed := applyIncrement(double) // double ∘ increment
// // composed(5) = double(increment(5)) = double(6) = 12
func Ap[A any](fa Endomorphism[A]) Operator[A] {
return Compose(fa)
}
// MonadCompose composes two endomorphisms, executing them from right to left.
//
// MonadCompose creates a new endomorphism that applies f2 first, then f1.
// This follows the mathematical notation of function composition: (f1 ∘ f2)(x) = f1(f2(x))
//
// IMPORTANT: The execution order is RIGHT-TO-LEFT:
// - f2 is applied first to the input
// - f1 is applied to the result of f2
//
// This is different from Chain/MonadChain which executes LEFT-TO-RIGHT.
//
// Parameters:
// - f1: The second function to apply (outer function)
// - f2: The first function to apply (inner function)
//
// Returns:
// - A new endomorphism that applies f2, then f1
//
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
// composed := endomorphism.MonadCompose(double, increment)
// result := composed(5) // (5 + 1) * 2 = 12
//
// // Compare with Chain which executes LEFT-TO-RIGHT:
// chained := endomorphism.MonadChain(double, increment)
// result2 := chained(5) // (5 * 2) + 1 = 11
func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
return function.Flow2(g, f)
}
// MonadMap maps an endomorphism over another endomorphism using function composition.
//
// For endomorphisms, Map is equivalent to Compose (RIGHT-TO-LEFT composition).
// This is the functor map operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - g is applied first to the input
// - f is applied to the result
//
// Parameters:
// - f: The function to map (outer function)
// - g: The endomorphism to map over (inner function)
//
// Returns:
// - A new endomorphism that applies g, then f
//
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// mapped := endomorphism.MonadMap(double, increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
return MonadCompose(f, g)
}
// Compose returns a function that composes an endomorphism with another, executing right to left.
//
// This is the curried version of MonadCompose. It takes an endomorphism g and returns
// a function that composes any endomorphism with g, applying g first (inner function),
// then the input endomorphism (outer function).
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (mathematical composition):
// - g is applied first to the input
// - The endomorphism passed to the returned function is applied to the result of g
//
// This follows the mathematical composition notation where Compose(g)(f) = f ∘ g
//
// Parameters:
// - g: The first endomorphism to apply (inner function)
//
// Returns:
// - A function that takes an endomorphism f and composes it with g (right-to-left)
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// composeWithIncrement := endomorphism.Compose(increment)
// double := N.Mul(2)
//
// // Composes double with increment (RIGHT-TO-LEFT: increment first, then double)
// composed := composeWithIncrement(double)
// result := composed(5) // (5 + 1) * 2 = 12
//
// // Compare with Chain which executes LEFT-TO-RIGHT:
// chainWithIncrement := endomorphism.Chain(increment)
// chained := chainWithIncrement(double)
// result2 := chained(5) // (5 * 2) + 1 = 11
func Compose[A any](g Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadCompose, g)
}
// Map returns a function that maps an endomorphism over another endomorphism.
//
// This is the curried version of MonadMap. It takes an endomorphism f and returns
// a function that maps f over any endomorphism using RIGHT-TO-LEFT composition.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as Compose):
// - The endomorphism passed to the returned function is applied first
// - f is applied to the result
//
// For endomorphisms, Map is equivalent to Compose.
//
// Parameters:
// - f: The function to map (outer function)
//
// Returns:
// - A function that takes an endomorphism and maps f over it (right-to-left)
//
// Example:
//
// double := N.Mul(2)
// mapDouble := endomorphism.Map(double)
// increment := func(x int) int { return x + 1 }
// mapped := mapDouble(increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func Map[A any](f Endomorphism[A]) Operator[A] {
return Compose(f)
}
// MonadChain chains two endomorphisms together, executing them from left to right.
//
// This is the monadic bind operation for endomorphisms. For endomorphisms, bind is
// simply left-to-right function composition: ma is applied first, then f.
//
// IMPORTANT: The execution order is LEFT-TO-RIGHT:
// - ma is applied first to the input
// - f is applied to the result of ma
//
// This is different from MonadCompose which executes RIGHT-TO-LEFT.
//
// Parameters:
// - ma: The first endomorphism to apply
// - f: The second endomorphism to apply
//
// Returns:
// - A new endomorphism that applies ma, then f
//
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
// chained := endomorphism.MonadChain(double, increment) // chained := endomorphism.MonadChain(double, increment)
// result := chained(5) // (5 * 2) + 1 = 11 // result := chained(5) // (5 * 2) + 1 = 11
//
// // Compare with MonadCompose which executes RIGHT-TO-LEFT:
// composed := endomorphism.MonadCompose(increment, double)
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] { func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
return Compose(ma, f) return function.Flow2(ma, f)
} }
// Chain returns a function that chains an endomorphism with another. // MonadChainFirst chains two endomorphisms but returns the result of the first.
// //
// This is the curried version of MonadChain. It takes an endomorphism f and returns // This applies ma first, then f, but discards the result of f and returns the result of ma.
// a function that chains any endomorphism with f. // Useful for performing side-effects while preserving the original value.
// //
// Parameters: // Parameters:
// - f: The endomorphism to chain with // - ma: The endomorphism whose result to keep
// - f: The endomorphism to apply for its effect
// //
// Returns: // Returns:
// - A function that takes an endomorphism and chains it with f // - A new endomorphism that applies both but returns ma's result
//
// Example:
//
// double := N.Mul(2)
// log := func(x int) int { fmt.Println(x); return x }
// chained := endomorphism.MonadChainFirst(double, log)
// result := chained(5) // Prints 10, returns 10
func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
return func(a A) A {
result := ma(a)
f(result) // Apply f for its effect
return result // But return ma's result
}
}
// ChainFirst returns a function that chains for effect but preserves the original result.
//
// This is the curried version of MonadChainFirst.
//
// Parameters:
// - f: The endomorphism to apply for its effect
//
// Returns:
// - A function that takes an endomorphism and chains it with f, keeping the first result
//
// Example:
//
// log := func(x int) int { fmt.Println(x); return x }
// chainLog := endomorphism.ChainFirst(log)
// double := N.Mul(2)
// chained := chainLog(double)
// result := chained(5) // Prints 10, returns 10
func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChainFirst, f)
}
// Chain returns a function that chains an endomorphism with another, executing left to right.
//
// This is the curried version of MonadChain. It takes an endomorphism f and returns
// a function that chains any endomorphism with f, applying the input endomorphism first,
// then f.
//
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
// - The endomorphism passed to the returned function is applied first
// - f is applied to the result
//
// Parameters:
// - f: The second endomorphism to apply
//
// Returns:
// - A function that takes an endomorphism and chains it with f (left-to-right)
// //
// Example: // Example:
// //
// increment := func(x int) int { return x + 1 } // increment := func(x int) int { return x + 1 }
// chainWithIncrement := endomorphism.Chain(increment) // chainWithIncrement := endomorphism.Chain(increment)
// double := func(x int) int { return x * 2 } // double := N.Mul(2)
//
// // Chains double (first) with increment (second)
// chained := chainWithIncrement(double) // chained := chainWithIncrement(double)
// result := chained(5) // (5 * 2) + 1 = 11 // result := chained(5) // (5 * 2) + 1 = 11
func Chain[A any](f Endomorphism[A]) Endomorphism[Endomorphism[A]] { func Chain[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChain, f) return function.Bind2nd(MonadChain, f)
} }
// Flatten collapses a nested endomorphism into a single endomorphism.
//
// Given an endomorphism that transforms endomorphisms (Endomorphism[Endomorphism[A]]),
// Flatten produces a simple endomorphism by applying the outer transformation to the
// identity function. This is the monadic join operation for the Endomorphism monad.
//
// The function applies the nested endomorphism to Identity[A] to extract the inner
// endomorphism, effectively "flattening" the two layers into one.
//
// Type Parameters:
// - A: The type being transformed by the endomorphisms
//
// Parameters:
// - mma: A nested endomorphism that transforms endomorphisms
//
// Returns:
// - An endomorphism that applies the transformation directly to values of type A
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// // An endomorphism that wraps another endomorphism
// addThenDouble := func(endo Endomorphism[Counter]) Endomorphism[Counter] {
// return func(c Counter) Counter {
// c = endo(c) // Apply the input endomorphism
// c.Value = c.Value * 2 // Then double
// return c
// }
// }
//
// flattened := Flatten(addThenDouble)
// result := flattened(Counter{Value: 5}) // Counter{Value: 10}
func Flatten[A any](mma Endomorphism[Endomorphism[A]]) Endomorphism[A] {
return mma(function.Identity[A])
}
// Join performs self-application of a function that produces endomorphisms.
//
// Given a function that takes a value and returns an endomorphism of that same type,
// Join creates an endomorphism that applies the value to itself through the function.
// This operation is also known as the W combinator (warbler) in combinatory logic,
// or diagonal application.
//
// The resulting endomorphism evaluates f(a)(a), applying the same value a to both
// the function f and the resulting endomorphism.
//
// Type Parameters:
// - A: The type being transformed
//
// Parameters:
// - f: A function that takes a value and returns an endomorphism of that type
//
// Returns:
// - An endomorphism that performs self-application: f(a)(a)
//
// Example:
//
// type Point struct {
// X, Y int
// }
//
// // Create an endomorphism based on the input point
// scaleBy := func(p Point) Endomorphism[Point] {
// return func(p2 Point) Point {
// return Point{
// X: p2.X * p.X,
// Y: p2.Y * p.Y,
// }
// }
// }
//
// selfScale := Join(scaleBy)
// result := selfScale(Point{X: 3, Y: 4}) // Point{X: 9, Y: 16}
func Join[A any](f Kleisli[A]) Endomorphism[A] {
return func(a A) A {
return f(a)(a)
}
}

View File

@@ -19,6 +19,7 @@ import (
"testing" "testing"
M "github.com/IBM/fp-go/v2/monoid" M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/semigroup" S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -76,84 +77,152 @@ func TestCurry3(t *testing.T) {
// TestMonadAp tests the MonadAp function // TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) { func TestMonadAp(t *testing.T) {
result := MonadAp(double, 5) // MonadAp composes two endomorphisms (RIGHT-TO-LEFT)
assert.Equal(t, 10, result, "MonadAp should apply endomorphism to value") // MonadAp(double, increment) means: increment first, then double
composed := MonadAp(double, increment)
result := composed(5)
assert.Equal(t, 12, result, "MonadAp should compose right-to-left: (5 + 1) * 2 = 12")
result2 := MonadAp(increment, 10) // Test with different order
assert.Equal(t, 11, result2, "MonadAp should work with different endomorphisms") composed2 := MonadAp(increment, double)
result2 := composed2(5)
assert.Equal(t, 11, result2, "MonadAp should compose right-to-left: (5 * 2) + 1 = 11")
result3 := MonadAp(square, 4) // Test with square
assert.Equal(t, 16, result3, "MonadAp should work with square function") composed3 := MonadAp(square, increment)
result3 := composed3(5)
assert.Equal(t, 36, result3, "MonadAp should compose right-to-left: (5 + 1) ^ 2 = 36")
} }
// TestAp tests the Ap function // TestAp tests the Ap function
func TestAp(t *testing.T) { func TestAp(t *testing.T) {
applyFive := Ap(5) // Ap is the curried version of MonadAp
// Ap(increment) returns a function that composes with increment (RIGHT-TO-LEFT)
applyIncrement := Ap(increment)
result := applyFive(double) composed := applyIncrement(double)
assert.Equal(t, 10, result, "Ap should apply value to endomorphism") result := composed(5)
assert.Equal(t, 12, result, "Ap should compose right-to-left: (5 + 1) * 2 = 12")
result2 := applyFive(increment) // Test with different endomorphism
assert.Equal(t, 6, result2, "Ap should work with different endomorphisms") composed2 := applyIncrement(square)
result2 := composed2(5)
assert.Equal(t, 36, result2, "Ap should compose right-to-left: (5 + 1) ^ 2 = 36")
applyTen := Ap(10) // Test with different base endomorphism
result3 := applyTen(square) applyDouble := Ap(double)
assert.Equal(t, 100, result3, "Ap should work with different values") composed3 := applyDouble(increment)
result3 := composed3(5)
assert.Equal(t, 11, result3, "Ap should compose right-to-left: (5 * 2) + 1 = 11")
} }
// TestCompose tests the Compose function // TestMonadCompose tests the MonadCompose function
func TestCompose(t *testing.T) { func TestMonadCompose(t *testing.T) {
// Test basic composition: (5 * 2) + 1 = 11 // Test basic composition: RIGHT-TO-LEFT execution
doubleAndIncrement := Compose(double, increment) // MonadCompose(double, increment) means: increment first, then double
result := doubleAndIncrement(5) composed := MonadCompose(double, increment)
assert.Equal(t, 11, result, "Compose should compose endomorphisms correctly") result := composed(5)
assert.Equal(t, 12, result, "MonadCompose should execute right-to-left: (5 + 1) * 2 = 12")
// Test composition order: (5 + 1) * 2 = 12 // Test composition order: RIGHT-TO-LEFT execution
incrementAndDouble := Compose(increment, double) // MonadCompose(increment, double) means: double first, then increment
result2 := incrementAndDouble(5) composed2 := MonadCompose(increment, double)
assert.Equal(t, 12, result2, "Compose should respect order of composition") result2 := composed2(5)
assert.Equal(t, 11, result2, "MonadCompose should execute right-to-left: (5 * 2) + 1 = 11")
// Test with three compositions: ((5 * 2) + 1) * ((5 * 2) + 1) = 121 // Test with three compositions: RIGHT-TO-LEFT execution
complex := Compose(Compose(double, increment), square) // MonadCompose(MonadCompose(double, increment), square) means: square, then increment, then double
complex := MonadCompose(MonadCompose(double, increment), square)
result3 := complex(5) result3 := complex(5)
assert.Equal(t, 121, result3, "Compose should work with nested compositions") // 5 -> square -> 25 -> increment -> 26 -> double -> 52
assert.Equal(t, 52, result3, "MonadCompose should work with nested compositions: square(5)=25, +1=26, *2=52")
} }
// TestMonadChain tests the MonadChain function // TestMonadChain tests the MonadChain function
func TestMonadChain(t *testing.T) { func TestMonadChain(t *testing.T) {
// MonadChain should behave like Compose // MonadChain executes LEFT-TO-RIGHT (first arg first, second arg second)
chained := MonadChain(double, increment) chained := MonadChain(double, increment)
result := chained(5) result := chained(5)
assert.Equal(t, 11, result, "MonadChain should chain endomorphisms correctly") assert.Equal(t, 11, result, "MonadChain should execute left-to-right: (5 * 2) + 1 = 11")
chained2 := MonadChain(increment, double) chained2 := MonadChain(increment, double)
result2 := chained2(5) result2 := chained2(5)
assert.Equal(t, 12, result2, "MonadChain should respect order") assert.Equal(t, 12, result2, "MonadChain should execute left-to-right: (5 + 1) * 2 = 12")
// Test with negative values // Test with negative values
chained3 := MonadChain(negate, increment) chained3 := MonadChain(negate, increment)
result3 := chained3(5) result3 := chained3(5)
assert.Equal(t, -4, result3, "MonadChain should work with negative values") assert.Equal(t, -4, result3, "MonadChain should execute left-to-right: -(5) + 1 = -4")
} }
// TestChain tests the Chain function // TestChain tests the Chain function
func TestChain(t *testing.T) { func TestChain(t *testing.T) {
// Chain(f) returns a function that applies its argument first, then f
chainWithIncrement := Chain(increment) chainWithIncrement := Chain(increment)
// chainWithIncrement(double) means: double first, then increment
chained := chainWithIncrement(double) chained := chainWithIncrement(double)
result := chained(5) result := chained(5)
assert.Equal(t, 11, result, "Chain should create chaining function correctly") assert.Equal(t, 11, result, "Chain should execute left-to-right: (5 * 2) + 1 = 11")
chainWithDouble := Chain(double) chainWithDouble := Chain(double)
// chainWithDouble(increment) means: increment first, then double
chained2 := chainWithDouble(increment) chained2 := chainWithDouble(increment)
result2 := chained2(5) result2 := chained2(5)
assert.Equal(t, 12, result2, "Chain should work with different endomorphisms") assert.Equal(t, 12, result2, "Chain should execute left-to-right: (5 + 1) * 2 = 12")
// Test chaining with square // Test chaining with square
chainWithSquare := Chain(square) chainWithSquare := Chain(square)
// chainWithSquare(double) means: double first, then square
chained3 := chainWithSquare(double) chained3 := chainWithSquare(double)
result3 := chained3(3) result3 := chained3(3)
assert.Equal(t, 36, result3, "Chain should work with square function") assert.Equal(t, 36, result3, "Chain should execute left-to-right: (3 * 2) ^ 2 = 36")
}
// TestCompose tests the curried Compose function
func TestCompose(t *testing.T) {
// Compose(g) returns a function that applies g first, then its argument
composeWithIncrement := Compose(increment)
// composeWithIncrement(double) means: increment first, then double
composed := composeWithIncrement(double)
result := composed(5)
assert.Equal(t, 12, result, "Compose should execute right-to-left: (5 + 1) * 2 = 12")
composeWithDouble := Compose(double)
// composeWithDouble(increment) means: double first, then increment
composed2 := composeWithDouble(increment)
result2 := composed2(5)
assert.Equal(t, 11, result2, "Compose should execute right-to-left: (5 * 2) + 1 = 11")
// Test composing with square
composeWithSquare := Compose(square)
// composeWithSquare(double) means: square first, then double
composed3 := composeWithSquare(double)
result3 := composed3(3)
assert.Equal(t, 18, result3, "Compose should execute right-to-left: (3 ^ 2) * 2 = 18")
}
// TestMonadComposeVsCompose demonstrates the relationship between MonadCompose and Compose
func TestMonadComposeVsCompose(t *testing.T) {
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
// MonadCompose takes both functions at once
monadComposed := MonadCompose(double, increment)
result1 := monadComposed(5) // (5 + 1) * 2 = 12
// Compose is the curried version - takes one function, returns a function
curriedCompose := Compose(increment)
composed := curriedCompose(double)
result2 := composed(5) // (5 + 1) * 2 = 12
assert.Equal(t, result1, result2, "MonadCompose and Compose should produce the same result")
assert.Equal(t, 12, result1, "Both should execute right-to-left: (5 + 1) * 2 = 12")
// Demonstrate that Compose(g)(f) is equivalent to MonadCompose(f, g)
assert.Equal(t, MonadCompose(double, increment)(5), Compose(increment)(double)(5),
"Compose(g)(f) should equal MonadCompose(f, g)")
} }
// TestOf tests the Of function // TestOf tests the Of function
@@ -191,12 +260,14 @@ func TestIdentity(t *testing.T) {
assert.Equal(t, 0, id(0), "Identity should work with zero") assert.Equal(t, 0, id(0), "Identity should work with zero")
assert.Equal(t, -10, id(-10), "Identity should work with negative values") assert.Equal(t, -10, id(-10), "Identity should work with negative values")
// Identity should be neutral for composition // Identity should be neutral for composition (RIGHT-TO-LEFT)
composed1 := Compose(id, double) // Compose(id, double) means: double first, then id
assert.Equal(t, 10, composed1(5), "Identity should be right neutral for composition") composed1 := MonadCompose(id, double)
assert.Equal(t, 10, composed1(5), "Identity should be left neutral: double(5) = 10")
composed2 := Compose(double, id) // Compose(double, id) means: id first, then double
assert.Equal(t, 10, composed2(5), "Identity should be left neutral for composition") composed2 := MonadCompose(double, id)
assert.Equal(t, 10, composed2(5), "Identity should be right neutral: id(5) then double = 10")
// Test with strings // Test with strings
idStr := Identity[string]() idStr := Identity[string]()
@@ -207,10 +278,11 @@ func TestIdentity(t *testing.T) {
func TestSemigroup(t *testing.T) { func TestSemigroup(t *testing.T) {
sg := Semigroup[int]() sg := Semigroup[int]()
// Test basic concat // Test basic concat (RIGHT-TO-LEFT execution via Compose)
// Concat(double, increment) means: increment first, then double
combined := sg.Concat(double, increment) combined := sg.Concat(double, increment)
result := combined(5) result := combined(5)
assert.Equal(t, 11, result, "Semigroup concat should compose endomorphisms") assert.Equal(t, 12, result, "Semigroup concat should execute right-to-left: (5 + 1) * 2 = 12")
// Test associativity: (f . g) . h = f . (g . h) // Test associativity: (f . g) . h = f . (g . h)
f := double f := double
@@ -223,10 +295,12 @@ func TestSemigroup(t *testing.T) {
testValue := 3 testValue := 3
assert.Equal(t, left(testValue), right(testValue), "Semigroup should be associative") assert.Equal(t, left(testValue), right(testValue), "Semigroup should be associative")
// Test with ConcatAll from semigroup package // Test with ConcatAll from semigroup package (RIGHT-TO-LEFT)
// ConcatAll(double)(increment, square) means: square, then increment, then double
combined2 := S.ConcatAll(sg)(double)([]Endomorphism[int]{increment, square}) combined2 := S.ConcatAll(sg)(double)([]Endomorphism[int]{increment, square})
result2 := combined2(5) result2 := combined2(5)
assert.Equal(t, 121, result2, "Semigroup should work with ConcatAll") // 5 -> square -> 25 -> increment -> 26 -> double -> 52
assert.Equal(t, 52, result2, "Semigroup ConcatAll should execute right-to-left: square(5)=25, +1=26, *2=52")
} }
// TestMonoid tests the Monoid function // TestMonoid tests the Monoid function
@@ -237,19 +311,21 @@ func TestMonoid(t *testing.T) {
empty := monoid.Empty() empty := monoid.Empty()
assert.Equal(t, 42, empty(42), "Monoid empty should be identity") assert.Equal(t, 42, empty(42), "Monoid empty should be identity")
// Test right identity: x . empty = x // Test right identity: x . empty = x (RIGHT-TO-LEFT: empty first, then x)
// Concat(double, empty) means: empty first, then double
rightIdentity := monoid.Concat(double, empty) rightIdentity := monoid.Concat(double, empty)
assert.Equal(t, 10, rightIdentity(5), "Monoid should satisfy right identity") assert.Equal(t, 10, rightIdentity(5), "Monoid should satisfy right identity: empty(5) then double = 10")
// Test left identity: empty . x = x // Test left identity: empty . x = x (RIGHT-TO-LEFT: x first, then empty)
// Concat(empty, double) means: double first, then empty
leftIdentity := monoid.Concat(empty, double) leftIdentity := monoid.Concat(empty, double)
assert.Equal(t, 10, leftIdentity(5), "Monoid should satisfy left identity") assert.Equal(t, 10, leftIdentity(5), "Monoid should satisfy left identity: double(5) then empty = 10")
// Test ConcatAll with multiple endomorphisms // Test ConcatAll with multiple endomorphisms (RIGHT-TO-LEFT execution)
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square}) combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
result := combined(5) result := combined(5)
// (5 * 2) = 10, (10 + 1) = 11, (11 * 11) = 121 // RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 121, result, "Monoid should work with ConcatAll") assert.Equal(t, 52, result, "Monoid ConcatAll should execute right-to-left: square(5)=25, +1=26, *2=52")
// Test ConcatAll with empty list should return identity // Test ConcatAll with empty list should return identity
emptyResult := M.ConcatAll(monoid)([]Endomorphism[int]{}) emptyResult := M.ConcatAll(monoid)([]Endomorphism[int]{})
@@ -294,19 +370,20 @@ func TestMonoidLaws(t *testing.T) {
// TestEndomorphismWithDifferentTypes tests endomorphisms with different types // TestEndomorphismWithDifferentTypes tests endomorphisms with different types
func TestEndomorphismWithDifferentTypes(t *testing.T) { func TestEndomorphismWithDifferentTypes(t *testing.T) {
// Test with strings // Test with strings (RIGHT-TO-LEFT execution)
toUpper := func(s string) string { addExclamation := func(s string) string {
return s + "!" return s + "!"
} }
addPrefix := func(s string) string { addPrefix := func(s string) string {
return "Hello, " + s return "Hello, " + s
} }
strComposed := Compose(toUpper, addPrefix) // Compose(addExclamation, addPrefix) means: addPrefix first, then addExclamation
strComposed := MonadCompose(addExclamation, addPrefix)
result := strComposed("World") result := strComposed("World")
assert.Equal(t, "Hello, World!", result, "Endomorphism should work with strings") assert.Equal(t, "Hello, World!", result, "Compose should execute right-to-left with strings")
// Test with float64 // Test with float64 (RIGHT-TO-LEFT execution)
doubleFloat := func(x float64) float64 { doubleFloat := func(x float64) float64 {
return x * 2.0 return x * 2.0
} }
@@ -314,76 +391,116 @@ func TestEndomorphismWithDifferentTypes(t *testing.T) {
return x + 1.0 return x + 1.0
} }
floatComposed := Compose(doubleFloat, addOne) // Compose(doubleFloat, addOne) means: addOne first, then doubleFloat
floatComposed := MonadCompose(doubleFloat, addOne)
resultFloat := floatComposed(5.5) resultFloat := floatComposed(5.5)
assert.Equal(t, 12.0, resultFloat, "Endomorphism should work with float64") // 5.5 + 1.0 = 6.5, 6.5 * 2.0 = 13.0
assert.Equal(t, 13.0, resultFloat, "Compose should execute right-to-left: (5.5 + 1.0) * 2.0 = 13.0")
} }
// TestComplexCompositions tests more complex composition scenarios // TestComplexCompositions tests more complex composition scenarios
func TestComplexCompositions(t *testing.T) { func TestComplexCompositions(t *testing.T) {
// Create a pipeline of transformations // Create a pipeline of transformations (RIGHT-TO-LEFT execution)
pipeline := Compose( // Innermost Compose is evaluated first in the composition chain
Compose( pipeline := MonadCompose(
Compose(double, increment), MonadCompose(
MonadCompose(double, increment),
square, square,
), ),
negate, negate,
) )
// (5 * 2) = 10, (10 + 1) = 11, (11 * 11) = 121, -(121) = -121 // RIGHT-TO-LEFT: negate(5) = -5, square(-5) = 25, increment(25) = 26, double(26) = 52
result := pipeline(5) result := pipeline(5)
assert.Equal(t, -121, result, "Complex composition should work correctly") assert.Equal(t, 52, result, "Complex composition should execute right-to-left")
// Test using monoid to build the same pipeline // Test using monoid to build the same pipeline (RIGHT-TO-LEFT)
monoid := Monoid[int]() monoid := Monoid[int]()
pipelineMonoid := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square, negate}) pipelineMonoid := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square, negate})
resultMonoid := pipelineMonoid(5) resultMonoid := pipelineMonoid(5)
assert.Equal(t, -121, resultMonoid, "Monoid-based pipeline should match composition") // RIGHT-TO-LEFT: negate(5) = -5, square(-5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 52, resultMonoid, "Monoid-based pipeline should match composition (right-to-left)")
} }
// TestOperatorType tests the Operator type // TestOperatorType tests the Operator type
func TestOperatorType(t *testing.T) { func TestOperatorType(t *testing.T) {
// Create an operator that lifts an int endomorphism to work on the length of strings // Create an operator that transforms int endomorphisms
lengthOperator := func(f Endomorphism[int]) Endomorphism[string] { // This operator takes an endomorphism and returns a new one that applies it twice
return func(s string) string { applyTwice := func(f Endomorphism[int]) Endomorphism[int] {
newLen := f(len(s)) return func(x int) int {
if newLen > len(s) { return f(f(x))
// Pad with spaces
for i := len(s); i < newLen; i++ {
s += " "
}
} else if newLen < len(s) {
// Truncate
s = s[:newLen]
}
return s
} }
} }
// Use the operator // Use the operator
var op Operator[int, string] = lengthOperator var op Operator[int] = applyTwice
doubleLength := op(double) doubleDouble := op(double)
result := doubleLength("hello") // len("hello") = 5, 5 * 2 = 10 result := doubleDouble(5) // double(double(5)) = double(10) = 20
assert.Equal(t, 10, len(result), "Operator should transform endomorphisms correctly") assert.Equal(t, 20, result, "Operator should transform endomorphisms correctly")
assert.Equal(t, "hello ", result, "Operator should pad string correctly")
// Test with increment
incrementTwice := op(increment)
result2 := incrementTwice(5) // increment(increment(5)) = increment(6) = 7
assert.Equal(t, 7, result2, "Operator should work with different endomorphisms")
} }
// BenchmarkCompose benchmarks the Compose function // BenchmarkCompose benchmarks the Compose function
func BenchmarkCompose(b *testing.B) { func BenchmarkCompose(b *testing.B) {
composed := Compose(double, increment) composed := MonadCompose(double, increment)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for b.Loop() {
_ = composed(5) _ = composed(5)
} }
} }
// BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid // BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid
// TestComposeVsChain demonstrates the key difference between Compose and Chain
func TestComposeVsChain(t *testing.T) {
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
// Compose executes RIGHT-TO-LEFT
// Compose(double, increment) means: increment first, then double
composed := MonadCompose(double, increment)
composedResult := composed(5) // (5 + 1) * 2 = 12
// MonadChain executes LEFT-TO-RIGHT
// MonadChain(double, increment) means: double first, then increment
chained := MonadChain(double, increment)
chainedResult := chained(5) // (5 * 2) + 1 = 11
assert.Equal(t, 12, composedResult, "Compose should execute right-to-left")
assert.Equal(t, 11, chainedResult, "MonadChain should execute left-to-right")
assert.NotEqual(t, composedResult, chainedResult, "Compose and Chain should produce different results with non-commutative operations")
// To get the same result with Compose, we need to reverse the order
composedReversed := MonadCompose(increment, double)
assert.Equal(t, chainedResult, composedReversed(5), "Compose with reversed args should match Chain")
// Demonstrate with a more complex example
square := func(x int) int { return x * x }
// Compose: RIGHT-TO-LEFT
composed3 := MonadCompose(MonadCompose(square, increment), double)
// double(5) = 10, increment(10) = 11, square(11) = 121
result1 := composed3(5)
// MonadChain: LEFT-TO-RIGHT
chained3 := MonadChain(MonadChain(double, increment), square)
// double(5) = 10, increment(10) = 11, square(11) = 121
result2 := chained3(5)
assert.Equal(t, 121, result1, "Compose should execute right-to-left")
assert.Equal(t, 121, result2, "MonadChain should execute left-to-right")
assert.Equal(t, result1, result2, "Both should produce same result when operations are in correct order")
}
func BenchmarkMonoidConcatAll(b *testing.B) { func BenchmarkMonoidConcatAll(b *testing.B) {
monoid := Monoid[int]() monoid := Monoid[int]()
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square}) combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for b.Loop() {
_ = combined(5) _ = combined(5)
} }
} }
@@ -393,7 +510,215 @@ func BenchmarkChain(b *testing.B) {
chainWithIncrement := Chain(increment) chainWithIncrement := Chain(increment)
chained := chainWithIncrement(double) chained := chainWithIncrement(double)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for b.Loop() {
_ = chained(5) _ = chained(5)
} }
} }
// TestFunctorLaws tests that endomorphisms satisfy the functor laws
func TestFunctorLaws(t *testing.T) {
// Functor Law 1: Identity
// map(id) = id
t.Run("Identity", func(t *testing.T) {
id := Identity[int]()
endo := double
// map(id)(endo) should equal endo
mapped := MonadMap(id, endo)
testValue := 5
assert.Equal(t, endo(testValue), mapped(testValue), "map(id) should equal id")
})
// Functor Law 2: Composition
// map(f . g) = map(f) . map(g)
t.Run("Composition", func(t *testing.T) {
f := double
g := increment
endo := square
// Left side: map(f . g)(endo)
composed := MonadCompose(f, g)
left := MonadMap(composed, endo)
// Right side: map(f)(map(g)(endo))
mappedG := MonadMap(g, endo)
right := MonadMap(f, mappedG)
testValue := 3
assert.Equal(t, left(testValue), right(testValue), "map(f . g) should equal map(f) . map(g)")
})
}
// TestApplicativeLaws tests that endomorphisms satisfy the applicative functor laws
func TestApplicativeLaws(t *testing.T) {
// Applicative Law 1: Identity
// ap(id, v) = v
t.Run("Identity", func(t *testing.T) {
id := Identity[int]()
v := double
applied := MonadAp(id, v)
testValue := 5
assert.Equal(t, v(testValue), applied(testValue), "ap(id, v) should equal v")
})
// Applicative Law 2: Composition
// ap(ap(ap(compose, u), v), w) = ap(u, ap(v, w))
t.Run("Composition", func(t *testing.T) {
u := double
v := increment
w := square
// For endomorphisms, ap is just composition
// Left side: ap(ap(ap(compose, u), v), w) = compose(compose(u, v), w)
left := MonadCompose(MonadCompose(u, v), w)
// Right side: ap(u, ap(v, w)) = compose(u, compose(v, w))
right := MonadCompose(u, MonadCompose(v, w))
testValue := 3
assert.Equal(t, left(testValue), right(testValue), "Applicative composition law")
})
// Applicative Law 3: Homomorphism
// ap(pure(f), pure(x)) = pure(f(x))
t.Run("Homomorphism", func(t *testing.T) {
// For endomorphisms, "pure" is just the identity function that returns a constant
// This law is trivially satisfied for endomorphisms
f := double
x := 5
// ap(f, id) applied to x should equal f(x)
id := Identity[int]()
applied := MonadAp(f, id)
assert.Equal(t, f(x), applied(x), "Homomorphism law")
})
}
// TestMonadLaws tests that endomorphisms satisfy the monad laws
func TestMonadLaws(t *testing.T) {
// Monad Law 1: Left Identity
// chain(pure(a), f) = f(a)
t.Run("LeftIdentity", func(t *testing.T) {
// For endomorphisms, "pure" is the identity function
// chain(id, f) = f
id := Identity[int]()
f := double
chained := MonadChain(id, f)
testValue := 5
assert.Equal(t, f(testValue), chained(testValue), "chain(id, f) should equal f")
})
// Monad Law 2: Right Identity
// chain(m, pure) = m
t.Run("RightIdentity", func(t *testing.T) {
m := double
id := Identity[int]()
chained := MonadChain(m, id)
testValue := 5
assert.Equal(t, m(testValue), chained(testValue), "chain(m, id) should equal m")
})
// Monad Law 3: Associativity
// chain(chain(m, f), g) = chain(m, x => chain(f(x), g))
t.Run("Associativity", func(t *testing.T) {
m := square
f := double
g := increment
// Left side: chain(chain(m, f), g)
left := MonadChain(MonadChain(m, f), g)
// Right side: chain(m, chain(f, g))
// For simple endomorphisms (not Kleisli arrows), this simplifies to:
right := MonadChain(m, MonadChain(f, g))
testValue := 3
assert.Equal(t, left(testValue), right(testValue), "Monad associativity law")
})
}
// TestMonadComposeVsMonadChain verifies the relationship between Compose and Chain
func TestMonadComposeVsMonadChain(t *testing.T) {
f := double
g := increment
// MonadCompose(f, g) should equal MonadChain(g, f)
// Because Compose is right-to-left and Chain is left-to-right
composed := MonadCompose(f, g)
chained := MonadChain(g, f)
testValue := 5
assert.Equal(t, composed(testValue), chained(testValue),
"MonadCompose(f, g) should equal MonadChain(g, f)")
}
// TestMapEqualsCompose verifies that Map is equivalent to Compose for endomorphisms
func TestMapEqualsCompose(t *testing.T) {
f := double
g := increment
// MonadMap(f, g) should equal MonadCompose(f, g)
mapped := MonadMap(f, g)
composed := MonadCompose(f, g)
testValue := 5
assert.Equal(t, composed(testValue), mapped(testValue),
"MonadMap should equal MonadCompose for endomorphisms")
// Curried versions
mapF := Map(f)
composeF := Compose(f)
mappedG := mapF(g)
composedG := composeF(g)
assert.Equal(t, composedG(testValue), mappedG(testValue),
"Map should equal Compose for endomorphisms (curried)")
}
// TestApEqualsCompose verifies that Ap is equivalent to Compose for endomorphisms
func TestApEqualsCompose(t *testing.T) {
f := double
g := increment
// MonadAp(f, g) should equal MonadCompose(f, g)
applied := MonadAp(f, g)
composed := MonadCompose(f, g)
testValue := 5
assert.Equal(t, composed(testValue), applied(testValue),
"MonadAp should equal MonadCompose for endomorphisms")
// Curried versions
apG := Ap(g)
composeG := Compose(g)
appliedF := apG(f)
composedF := composeG(f)
assert.Equal(t, composedF(testValue), appliedF(testValue),
"Ap should equal Compose for endomorphisms (curried)")
}
// TestChainFirst tests the ChainFirst operation
func TestChainFirst(t *testing.T) {
double := N.Mul(2)
// Track side effect
var sideEffect int
logEffect := func(x int) int {
sideEffect = x
return x + 100 // This result should be discarded
}
chained := MonadChainFirst(double, logEffect)
result := chained(5)
// Should return double's result (10), not logEffect's result
assert.Equal(t, 10, result, "ChainFirst should return first result")
// But side effect should have been executed with double's result
assert.Equal(t, 10, sideEffect, "ChainFirst should execute second function for effect")
}

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

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

View File

@@ -35,7 +35,7 @@ import (
// //
// Example: // Example:
// //
// myFunc := func(x int) int { return x * 2 } // myFunc := N.Mul(2)
// endo := endomorphism.Of(myFunc) // endo := endomorphism.Of(myFunc)
func Of[F ~func(A) A, A any](f F) Endomorphism[A] { func Of[F ~func(A) A, A any](f F) Endomorphism[A] {
return f return f
@@ -75,7 +75,7 @@ func Unwrap[F ~func(A) A, A any](f Endomorphism[A]) F {
// result := id(42) // Returns: 42 // result := id(42) // Returns: 42
// //
// // Identity is neutral for composition // // Identity is neutral for composition
// double := func(x int) int { return x * 2 } // double := N.Mul(2)
// composed := endomorphism.Compose(id, double) // composed := endomorphism.Compose(id, double)
// // composed behaves exactly like double // // composed behaves exactly like double
func Identity[A any]() Endomorphism[A] { func Identity[A any]() Endomorphism[A] {
@@ -88,25 +88,29 @@ func Identity[A any]() Endomorphism[A] {
// For endomorphisms, this operation is composition (Compose). This means: // For endomorphisms, this operation is composition (Compose). This means:
// - Concat(f, Concat(g, h)) = Concat(Concat(f, g), h) // - Concat(f, Concat(g, h)) = Concat(Concat(f, g), h)
// //
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
// - Concat(f, g) applies g first, then f
// - This is equivalent to Compose(f, g)
//
// The returned semigroup can be used with semigroup operations to combine // The returned semigroup can be used with semigroup operations to combine
// multiple endomorphisms. // multiple endomorphisms.
// //
// Returns: // Returns:
// - A Semigroup[Endomorphism[A]] where concat is composition // - A Semigroup[Endomorphism[A]] where concat is composition (right-to-left)
// //
// Example: // Example:
// //
// import S "github.com/IBM/fp-go/v2/semigroup" // import S "github.com/IBM/fp-go/v2/semigroup"
// //
// sg := endomorphism.Semigroup[int]() // sg := endomorphism.Semigroup[int]()
// double := func(x int) int { return x * 2 } // double := N.Mul(2)
// increment := func(x int) int { return x + 1 } // increment := func(x int) int { return x + 1 }
// //
// // Combine using the semigroup // // Combine using the semigroup (RIGHT-TO-LEFT execution)
// combined := sg.Concat(double, increment) // combined := sg.Concat(double, increment)
// result := combined(5) // (5 * 2) + 1 = 11 // result := combined(5) // (5 + 1) * 2 = 12 (increment first, then double)
func Semigroup[A any]() S.Semigroup[Endomorphism[A]] { func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
return S.MakeSemigroup(Compose[A]) return S.MakeSemigroup(MonadCompose[A])
} }
// Monoid returns a Monoid for endomorphisms where concat is composition and empty is identity. // Monoid returns a Monoid for endomorphisms where concat is composition and empty is identity.
@@ -115,6 +119,10 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// - The binary operation is composition (Compose) // - The binary operation is composition (Compose)
// - The identity element is the identity function (Identity) // - The identity element is the identity function (Identity)
// //
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
// - Concat(f, g) applies g first, then f
// - ConcatAll applies functions from right to left
//
// This satisfies the monoid laws: // This satisfies the monoid laws:
// - Right identity: Concat(x, Empty) = x // - Right identity: Concat(x, Empty) = x
// - Left identity: Concat(Empty, x) = x // - Left identity: Concat(Empty, x) = x
@@ -124,20 +132,20 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// combine multiple endomorphisms. // combine multiple endomorphisms.
// //
// Returns: // Returns:
// - A Monoid[Endomorphism[A]] with composition and identity // - A Monoid[Endomorphism[A]] with composition (right-to-left) and identity
// //
// Example: // Example:
// //
// import M "github.com/IBM/fp-go/v2/monoid" // import M "github.com/IBM/fp-go/v2/monoid"
// //
// monoid := endomorphism.Monoid[int]() // monoid := endomorphism.Monoid[int]()
// double := func(x int) int { return x * 2 } // double := N.Mul(2)
// increment := func(x int) int { return x + 1 } // increment := func(x int) int { return x + 1 }
// square := func(x int) int { return x * x } // square := func(x int) int { return x * x }
// //
// // Combine multiple endomorphisms // // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(double, increment, square) // combined := M.ConcatAll(monoid)(double, increment, square)
// result := combined(5) // ((5 * 2) + 1) * ((5 * 2) + 1) = 121 // result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
func Monoid[A any]() M.Monoid[Endomorphism[A]] { func Monoid[A any]() M.Monoid[Endomorphism[A]] {
return M.MakeMonoid(Compose[A], Identity[A]()) return M.MakeMonoid(MonadCompose[A], Identity[A]())
} }

View File

@@ -29,7 +29,7 @@ type (
// Example: // Example:
// //
// // Simple endomorphisms on integers // // Simple endomorphisms on integers
// double := func(x int) int { return x * 2 } // double := N.Mul(2)
// increment := func(x int) int { return x + 1 } // increment := func(x int) int { return x + 1 }
// //
// // Both are endomorphisms of type Endomorphism[int] // // Both are endomorphisms of type Endomorphism[int]
@@ -37,6 +37,8 @@ type (
// var g endomorphism.Endomorphism[int] = increment // var g endomorphism.Endomorphism[int] = increment
Endomorphism[A any] = func(A) A Endomorphism[A any] = func(A) A
Kleisli[A any] = func(A) Endomorphism[A]
// Operator represents a transformation from one endomorphism to another. // Operator represents a transformation from one endomorphism to another.
// //
// An Operator takes an endomorphism on type A and produces an endomorphism on type B. // An Operator takes an endomorphism on type A and produces an endomorphism on type B.
@@ -52,5 +54,5 @@ type (
// return strconv.Itoa(result) // return strconv.Itoa(result)
// } // }
// } // }
Operator[A, B any] = func(Endomorphism[A]) Endomorphism[B] Operator[A any] = Endomorphism[Endomorphism[A]]
) )

View File

@@ -15,7 +15,84 @@
package eq package eq
// Contramap implements an Equals predicate based on a mapping // Contramap creates an Eq[B] from an Eq[A] by providing a function that maps B to A.
// This is a contravariant functor operation that allows you to transform equality predicates
// by mapping the input type. It's particularly useful for comparing complex types by
// extracting comparable fields.
//
// The name "contramap" comes from category theory, where it represents a contravariant
// functor. Unlike regular map (covariant), which transforms the output, contramap
// transforms the input in the opposite direction.
//
// Type Parameters:
// - A: The type that has an existing Eq instance
// - B: The type for which we want to create an Eq instance
//
// Parameters:
// - f: A function that extracts or converts a value of type B to type A
//
// Returns:
// - A function that takes an Eq[A] and returns an Eq[B]
//
// The resulting Eq[B] compares two B values by:
// 1. Applying f to both values to get A values
// 2. Using the original Eq[A] to compare those A values
//
// Example - Compare structs by a single field:
//
// type Person struct {
// ID int
// Name string
// Age int
// }
//
// // Compare persons by ID only
// personEqByID := eq.Contramap(func(p Person) int {
// return p.ID
// })(eq.FromStrictEquals[int]())
//
// p1 := Person{ID: 1, Name: "Alice", Age: 30}
// p2 := Person{ID: 1, Name: "Bob", Age: 25}
// assert.True(t, personEqByID.Equals(p1, p2)) // Same ID, different names
//
// Example - Case-insensitive string comparison:
//
// type User struct {
// Username string
// Email string
// }
//
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
//
// userEqByUsername := eq.Contramap(func(u User) string {
// return u.Username
// })(caseInsensitiveEq)
//
// u1 := User{Username: "Alice", Email: "alice@example.com"}
// u2 := User{Username: "ALICE", Email: "different@example.com"}
// assert.True(t, userEqByUsername.Equals(u1, u2)) // Case-insensitive match
//
// Example - Nested field access:
//
// type Address struct {
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Compare persons by city
// personEqByCity := eq.Contramap(func(p Person) string {
// return p.Address.City
// })(eq.FromStrictEquals[string]())
//
// Contramap Law:
// Contramap must satisfy: Contramap(f)(Contramap(g)(eq)) = Contramap(g ∘ f)(eq)
// This means contramapping twice is the same as contramapping with the composed function.
func Contramap[A, B any](f func(b B) A) func(Eq[A]) Eq[B] { func Contramap[A, B any](f func(b B) A) func(Eq[A]) Eq[B] {
return func(fa Eq[A]) Eq[B] { return func(fa Eq[A]) Eq[B] {
equals := fa.Equals equals := fa.Equals

View File

@@ -19,38 +19,188 @@ import (
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
) )
// Eq represents an equality type class for type T.
// It provides a way to define custom equality semantics for any type,
// not just those that are comparable with Go's == operator.
//
// Type Parameters:
// - T: The type for which equality is defined
//
// Methods:
// - Equals(x, y T) bool: Returns true if x and y are considered equal
//
// Laws:
// An Eq instance must satisfy the equivalence relation laws:
// 1. Reflexivity: Equals(x, x) = true for all x
// 2. Symmetry: Equals(x, y) = Equals(y, x) for all x, y
// 3. Transitivity: If Equals(x, y) and Equals(y, z), then Equals(x, z)
//
// Example:
//
// // Create an equality predicate for integers
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// // Create a custom equality predicate
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
type Eq[T any] interface { type Eq[T any] interface {
// Equals returns true if x and y are considered equal according to this equality predicate.
//
// Parameters:
// - x: The first value to compare
// - y: The second value to compare
//
// Returns:
// - true if x and y are equal, false otherwise
Equals(x, y T) bool Equals(x, y T) bool
} }
// eq is the internal implementation of the Eq interface.
// It wraps a comparison function to provide the Eq interface.
type eq[T any] struct { type eq[T any] struct {
c func(x, y T) bool c func(x, y T) bool
} }
// Equals implements the Eq interface by delegating to the wrapped comparison function.
func (e eq[T]) Equals(x, y T) bool { func (e eq[T]) Equals(x, y T) bool {
return e.c(x, y) return e.c(x, y)
} }
// strictEq is a helper function that uses Go's built-in == operator for comparison.
// It can only be used with comparable types.
func strictEq[A comparable](a, b A) bool { func strictEq[A comparable](a, b A) bool {
return a == b return a == b
} }
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function // FromStrictEquals constructs an Eq instance using Go's built-in == operator.
// This is the most common way to create an Eq for types that support ==.
//
// Type Parameters:
// - T: Must be a comparable type (supports ==)
//
// Returns:
// - An Eq[T] that uses == for equality comparison
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// stringEq := eq.FromStrictEquals[string]()
// assert.True(t, stringEq.Equals("hello", "hello"))
// assert.False(t, stringEq.Equals("hello", "world"))
//
// Note: For types that are not comparable or require custom equality logic,
// use FromEquals instead.
func FromStrictEquals[T comparable]() Eq[T] { func FromStrictEquals[T comparable]() Eq[T] {
return FromEquals(strictEq[T]) return FromEquals(strictEq[T])
} }
// FromEquals constructs an [EQ.Eq] from the comparison function // FromEquals constructs an Eq instance from a custom comparison function.
// This allows defining equality for any type, including non-comparable types
// or types that need custom equality semantics.
//
// Type Parameters:
// - T: The type for which equality is being defined (can be any type)
//
// Parameters:
// - c: A function that takes two values of type T and returns true if they are equal
//
// Returns:
// - An Eq[T] that uses the provided function for equality comparison
//
// Example:
//
// // Case-insensitive string equality
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
//
// // Approximate float equality
// approxEq := eq.FromEquals(func(a, b float64) bool {
// return math.Abs(a-b) < 0.0001
// })
// assert.True(t, approxEq.Equals(1.0, 1.00009))
//
// // Custom struct equality (compare by specific fields)
// type Person struct { ID int; Name string }
// personEq := eq.FromEquals(func(a, b Person) bool {
// return a.ID == b.ID // Compare only by ID
// })
//
// Note: The provided function should satisfy the equivalence relation laws
// (reflexivity, symmetry, transitivity) for correct behavior.
func FromEquals[T any](c func(x, y T) bool) Eq[T] { func FromEquals[T any](c func(x, y T) bool) Eq[T] {
return eq[T]{c: c} return eq[T]{c: c}
} }
// Empty returns the equals predicate that is always true // Empty returns an Eq instance that always returns true for any comparison.
// This is the identity element for the Eq Monoid and is useful when you need
// an equality predicate that accepts everything.
//
// Type Parameters:
// - T: The type for which the always-true equality is defined
//
// Returns:
// - An Eq[T] where Equals(x, y) always returns true
//
// Example:
//
// alwaysTrue := eq.Empty[int]()
// assert.True(t, alwaysTrue.Equals(1, 2))
// assert.True(t, alwaysTrue.Equals(42, 100))
//
// // Useful as identity in monoid operations
// monoid := eq.Monoid[string]()
// combined := monoid.Concat(eq.FromStrictEquals[string](), monoid.Empty())
// // combined behaves the same as FromStrictEquals
//
// Use cases:
// - As the identity element in Monoid operations
// - When you need a placeholder equality that accepts everything
// - In generic code that requires an Eq but doesn't need actual comparison
func Empty[T any]() Eq[T] { func Empty[T any]() Eq[T] {
return FromEquals(F.Constant2[T, T](true)) return FromEquals(F.Constant2[T, T](true))
} }
// Equals returns a predicate to test if one value equals the other under an equals predicate // Equals returns a curried equality checking function.
// This is useful for partial application and functional composition.
//
// Type Parameters:
// - T: The type being compared
//
// Parameters:
// - eq: The Eq instance to use for comparison
//
// Returns:
// - A function that takes a value and returns another function that checks equality with that value
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// equals42 := eq.Equals(intEq)(42)
//
// assert.True(t, equals42(42))
// assert.False(t, equals42(43))
//
// // Use in higher-order functions
// numbers := []int{40, 41, 42, 43, 44}
// filtered := array.Filter(equals42)(numbers)
// // filtered = [42]
//
// // Partial application
// equalsFunc := eq.Equals(intEq)
// equals10 := equalsFunc(10)
// equals20 := equalsFunc(20)
//
// This is particularly useful when working with functional programming patterns
// like map, filter, and other higher-order functions.
func Equals[T any](eq Eq[T]) func(T) func(T) bool { func Equals[T any](eq Eq[T]) func(T) func(T) bool {
return func(other T) func(T) bool { return func(other T) func(T) bool {
return F.Bind2nd(eq.Equals, other) return F.Bind2nd(eq.Equals, other)

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