1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-26 13:06:09 +02:00

Compare commits

...

32 Commits

Author SHA1 Message Date
Dr. Carsten Leue
c4cac1cb3e fix: add FromReaderResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-21 16:44:51 +01:00
Dr. Carsten Leue
a3fdb03df4 fix: better assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-13 09:27:49 +01:00
Dr. Carsten Leue
47727fd514 fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:51:34 +01:00
Dr. Carsten Leue
ece7d088ea fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:50:30 +01:00
Dr. Carsten Leue
13d25eca32 fix: add composition logic to Iso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:46:41 +01:00
Dr. Carsten Leue
a68e32308d fix: add filterable to Either and Result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 09:28:42 +01:00
Dr. Carsten Leue
61b948425b fix: cleaner use of Kleisli
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-11 16:24:11 +01:00
Dr. Carsten Leue
a276f3acff fix: add llms.txt
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 09:48:19 +01:00
Dr. Carsten Leue
8c656a4297 fix: more Alt tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 08:52:39 +01:00
Dr. Carsten Leue
bd9a642e93 fix: implement Alt for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 18:31:00 +01:00
Dr. Carsten Leue
3b55cae265 fix: implement alternative monoid for codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 09:59:17 +01:00
Dr. Carsten Leue
1472fa5a50 fix: add some more validation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-04 17:58:08 +01:00
Dr. Carsten Leue
49deb57d24 fix: OrElse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 17:54:43 +01:00
Dr. Carsten Leue
abb55ddbd0 fix: validation logic and ChainLeft
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 14:00:44 +01:00
Dr. Carsten Leue
f6b01dffdc fix: add ModifiyReaderIOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-02 09:09:04 +01:00
Dr. Carsten Leue
43b666edbb fix: add bind to codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-31 11:47:50 +01:00
Dr. Carsten Leue
e42d765852 fix: readeriooption
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 16:59:32 +01:00
Dr. Carsten Leue
d2da8a32b4 fix: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 11:45:45 +01:00
Dr. Carsten Leue
7484af664b fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:12:27 +01:00
Dr. Carsten Leue
ae38e3f8f4 fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:07:12 +01:00
Dr. Carsten Leue
e0f854bda3 fix: executes_all_IO_operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:25:39 +01:00
Dr. Carsten Leue
34786c3cd8 fix: more tests and lens generation fix for prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:11:46 +01:00
Dr. Carsten Leue
a7aa7e3560 fix: better DI example
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 22:45:17 +01:00
Dr. Carsten Leue
ff2a4299b2 fix: add some useful lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 17:39:34 +01:00
Dr. Carsten Leue
edd66d63e6 fix: more codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 14:51:35 +01:00
Dr. Carsten Leue
909aec8eba fix: better sequence iter
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-26 10:41:25 +01:00
Obed Tetteh
da0344f9bd feat(iterator): add Last function with Option return type (#155)
- Add Last function to retrieve the final element from an iterator,
  returning Some(element) for non-empty sequences and None for empty ones.
- Includes tests covering simple types and  complex types
- Add documentation including example code
2026-01-26 09:04:51 +01:00
Dr. Carsten Leue
cd79dd56b9 fix: simplify tests a bit
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 17:56:28 +01:00
Dr. Carsten Leue
df07599a9e fix: some docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:40:45 +01:00
Dr. Carsten Leue
30ad0e4dd8 doc: add validation docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:26:53 +01:00
Dr. Carsten Leue
2374d7f1e4 fix: support unexported fields for lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:18:44 +01:00
Dr. Carsten Leue
eafc008798 fix: doc for lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:00:11 +01:00
213 changed files with 45392 additions and 1764 deletions

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
@@ -64,7 +64,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24.x', '1.25.x']
go-version: ['1.24.x', '1.25.x', '1.26.x']
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:

226
v2/AGENTS.md Normal file
View File

@@ -0,0 +1,226 @@
# Agent Guidelines for fp-go/v2
This document provides guidelines for AI agents working on the fp-go/v2 project.
## Documentation Standards
### Go Doc Comments
1. **Use Standard Go Doc Format**
- Do NOT use markdown-style links like `[text](url)`
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
- Go's documentation system will automatically create links
2. **Structure**
```go
// FunctionName does something useful.
//
// Longer description explaining the purpose and behavior.
//
// # Type Parameters
//
// - T: Description of type parameter
//
// # Parameters
//
// - param: Description of parameter
//
// # Returns
//
// - ReturnType: Description of return value
//
// # Example Usage
//
// code example here
//
// # See Also
//
// - RelatedFunction: Brief description
func FunctionName[T any](param T) ReturnType {
```
3. **Code Examples**
- Use idiomatic Go patterns
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
- Show realistic, runnable examples
### File Headers
Always include the Apache 2.0 license header:
```go
// 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.
```
## Testing Standards
### Test Structure
1. **Organize Tests by Category**
```go
func TestFunctionName_Success(t *testing.T) {
t.Run("specific success case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_Failure(t *testing.T) {
t.Run("specific failure case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_EdgeCases(t *testing.T) {
// edge case tests
}
func TestFunctionName_Integration(t *testing.T) {
// integration tests
}
```
2. **Use Direct Assertions**
- Prefer: `assert.Equal(t, validation.Success(expected), actual)`
- Avoid: Verbose `either.MonadFold` patterns unless necessary
- Exception: When you need to verify pointer is not nil or extract specific fields
3. **Use Idiomatic Patterns**
- Use `result.Eitherize1` for converting `(T, error)` functions
- Use `result.Of` for success values
- Use `result.Left` for error values
### Test Coverage
Include tests for:
- **Success cases**: Normal operation with various input types
- **Failure cases**: Error handling and error preservation
- **Edge cases**: Nil, empty, zero values, boundary conditions
- **Integration**: Composition with other functions
- **Type safety**: Verify type parameters work correctly
- **Benchmarks**: Performance-critical paths
### Example Test Pattern
```go
func TestFromReaderResult_Success(t *testing.T) {
t.Run("converts successful ReaderResult", func(t *testing.T) {
// Arrange
parseIntRR := result.Eitherize1(strconv.Atoi)
validator := FromReaderResult[string, int](parseIntRR)
// Act
result := validator("42")(nil)
// Assert
assert.Equal(t, validation.Success(42), result)
})
}
```
## Code Style
### Functional Patterns
1. **Prefer Composition**
```go
validator := F.Pipe1(
FromReaderResult[string, int](parseIntRR),
Chain(validatePositive),
)
```
2. **Use Type-Safe Helpers**
- `result.Eitherize1` for `func(T) (R, error)`
- `result.Of` for wrapping success values
- `result.Left` for wrapping errors
3. **Avoid Verbose Patterns**
- Don't manually handle `(value, error)` tuples when helpers exist
- Don't use `either.MonadFold` in tests unless necessary
### Error Handling
1. **In Production Code**
- Use `validation.Success` for successful validations
- Use `validation.FailureWithMessage` for simple failures
- Use `validation.FailureWithError` to preserve error causes
2. **In Tests**
- Verify error messages and causes
- Check error context is preserved
- Test error accumulation when applicable
## Common Patterns
### Converting Error-Based Functions
```go
// Good: Use Eitherize1
parseIntRR := result.Eitherize1(strconv.Atoi)
// Avoid: Manual error handling
parseIntRR := func(input string) result.Result[int] {
val, err := strconv.Atoi(input)
if err != nil {
return result.Left[int](err)
}
return result.Of(val)
}
```
### Testing Validation Results
```go
// Good: Direct comparison
assert.Equal(t, validation.Success(42), result)
// Avoid: Verbose extraction (unless you need to verify specific fields)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
```
### Documentation Examples
```go
// Good: Concise and idiomatic
// parseIntRR := result.Eitherize1(strconv.Atoi)
// validator := FromReaderResult[string, int](parseIntRR)
// Avoid: Verbose manual patterns
// parseIntRR := func(input string) result.Result[int] {
// val, err := strconv.Atoi(input)
// if err != nil {
// return result.Left[int](err)
// }
// return result.Of(val)
// }
```
## Checklist for New Code
- [ ] Apache 2.0 license header included
- [ ] Go doc comments use standard format (no markdown links)
- [ ] Code examples are idiomatic and concise
- [ ] Tests cover success, failure, edge cases, and integration
- [ ] Tests use direct assertions where possible
- [ ] Benchmarks included for performance-critical code
- [ ] All tests pass
- [ ] Code uses functional composition patterns
- [ ] Error handling preserves context and causes

View File

@@ -460,12 +460,15 @@ func process() IOResult[string] {
- **Either** - Type-safe error handling with left/right values
- **Result** - Simplified Either with error as left type (recommended for error handling)
- **IO** - Lazy evaluation and side effect management
- **IOOption** - Combine IO with Option for optional values with side effects
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
- **Reader** - Dependency injection pattern
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
#### Idiomatic Packages (Tuple-based, High Performance)
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples

View File

@@ -21,7 +21,7 @@ import (
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// From constructs an array from a set of variadic arguments
@@ -163,11 +163,11 @@ func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return G.FilterMapWithIndex[[]A, []B](f)
}
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
//
//go:inline
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.FilterChain[[]A](f)
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.ChainOptionK[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
@@ -239,6 +239,16 @@ func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B {
}
// Append adds an element to the end of an array, returning a new array.
// This is a non-curried version that takes both the array and element as parameters.
//
// Example:
//
// arr := []int{1, 2, 3}
// result := array.Append(arr, 4)
// // result: []int{1, 2, 3, 4}
// // arr: []int{1, 2, 3} (unchanged)
//
// For a curried version, see Push.
//
//go:inline
func Append[A any](as []A, a A) []A {
@@ -443,7 +453,7 @@ func Size[A any](as []A) int {
// the second contains elements for which it returns true.
//
//go:inline
func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
return G.MonadPartition(as, pred)
}
@@ -451,7 +461,7 @@ func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
// for which the predicate returns false, the right one those for which the predicate returns true
//
//go:inline
func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] {
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
return G.Partition[[]A](pred)
}

View File

@@ -16,308 +16,88 @@
package array
import (
"fmt"
"testing"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestReplicate(t *testing.T) {
result := Replicate(3, "a")
assert.Equal(t, []string{"a", "a", "a"}, result)
empty := Replicate(0, 42)
assert.Equal(t, []int{}, empty)
}
func TestMonadMap(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMap(src, N.Mul(2))
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMonadMapRef(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMapRef(src, func(x *int) int { return *x * 2 })
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
mapper := MapWithIndex(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := mapper(src)
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result)
}
func TestMapRef(t *testing.T) {
src := []int{1, 2, 3}
mapper := MapRef(func(x *int) int { return *x * 2 })
result := mapper(src)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestFilterWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterWithIndex(func(i, x int) bool {
return i%2 == 0 && x > 2
})
result := filter(src)
assert.Equal(t, []int{3, 5}, result)
}
func TestFilterRef(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterRef(func(x *int) bool { return *x > 2 })
result := filter(src)
assert.Equal(t, []int{3, 4, 5}, result)
}
func TestMonadFilterMap(t *testing.T) {
src := []int{1, 2, 3, 4}
result := MonadFilterMap(src, func(x int) O.Option[string] {
if x%2 == 0 {
return O.Some(fmt.Sprintf("even:%d", x))
}
return O.None[string]()
})
assert.Equal(t, []string{"even:2", "even:4"}, result)
}
func TestMonadFilterMapWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4}
result := MonadFilterMapWithIndex(src, func(i, x int) O.Option[string] {
if i%2 == 0 {
return O.Some(fmt.Sprintf("%d:%d", i, x))
}
return O.None[string]()
})
assert.Equal(t, []string{"0:1", "2:3"}, result)
}
func TestFilterMapWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4}
filter := FilterMapWithIndex(func(i, x int) O.Option[string] {
if i%2 == 0 {
return O.Some(fmt.Sprintf("%d:%d", i, x))
}
return O.None[string]()
})
result := filter(src)
assert.Equal(t, []string{"0:1", "2:3"}, result)
}
func TestFilterMapRef(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterMapRef(
func(x *int) bool { return *x > 2 },
func(x *int) string { return fmt.Sprintf("val:%d", *x) },
)
result := filter(src)
assert.Equal(t, []string{"val:3", "val:4", "val:5"}, result)
}
func TestReduceWithIndex(t *testing.T) {
src := []int{1, 2, 3}
reducer := ReduceWithIndex(func(i, acc, x int) int {
return acc + i + x
// TestMonadReduceWithIndex tests the MonadReduceWithIndex function
func TestMonadReduceWithIndex(t *testing.T) {
// Test with integers - sum with index multiplication
numbers := []int{1, 2, 3, 4, 5}
result := MonadReduceWithIndex(numbers, func(idx, acc, val int) int {
return acc + (val * idx)
}, 0)
result := reducer(src)
assert.Equal(t, 9, result) // 0 + (0+1) + (1+2) + (2+3) = 9
}
// Expected: 0*1 + 1*2 + 2*3 + 3*4 + 4*5 = 0 + 2 + 6 + 12 + 20 = 40
assert.Equal(t, 40, result)
func TestReduceRightWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
reducer := ReduceRightWithIndex(func(i int, x, acc string) string {
return fmt.Sprintf("%s%d:%s", acc, i, x)
// Test with empty array
empty := []int{}
result2 := MonadReduceWithIndex(empty, func(idx, acc, val int) int {
return acc + val
}, 10)
assert.Equal(t, 10, result2)
// Test with strings - concatenate with index
words := []string{"a", "b", "c"}
result3 := MonadReduceWithIndex(words, func(idx int, acc, val string) string {
return acc + val + string(rune('0'+idx))
}, "")
result := reducer(src)
assert.Equal(t, "2:c1:b0:a", result)
assert.Equal(t, "a0b1c2", result3)
}
func TestReduceRef(t *testing.T) {
src := []int{1, 2, 3}
reducer := ReduceRef(func(acc int, x *int) int {
return acc + *x
}, 0)
result := reducer(src)
assert.Equal(t, 6, result)
}
func TestZero(t *testing.T) {
result := Zero[int]()
assert.Equal(t, []int{}, result)
assert.True(t, IsEmpty(result))
}
func TestMonadChain(t *testing.T) {
src := []int{1, 2, 3}
result := MonadChain(src, func(x int) []int {
return []int{x, x * 10}
})
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
}
func TestChain(t *testing.T) {
src := []int{1, 2, 3}
chain := Chain(func(x int) []int {
return []int{x, x * 10}
})
result := chain(src)
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
}
func TestMonadAp(t *testing.T) {
fns := []func(int) int{
N.Mul(2),
N.Add(10),
}
values := []int{1, 2}
result := MonadAp(fns, values)
assert.Equal(t, []int{2, 4, 11, 12}, result)
}
func TestMatchLeft(t *testing.T) {
matcher := MatchLeft(
func() string { return "empty" },
func(head int, tail []int) string {
return fmt.Sprintf("head:%d,tail:%v", head, tail)
},
)
assert.Equal(t, "empty", matcher([]int{}))
assert.Equal(t, "head:1,tail:[2 3]", matcher([]int{1, 2, 3}))
}
func TestTail(t *testing.T) {
assert.Equal(t, O.None[[]int](), Tail([]int{}))
assert.Equal(t, O.Some([]int{2, 3}), Tail([]int{1, 2, 3}))
assert.Equal(t, O.Some([]int{}), Tail([]int{1}))
}
func TestFirst(t *testing.T) {
assert.Equal(t, O.None[int](), First([]int{}))
assert.Equal(t, O.Some(1), First([]int{1, 2, 3}))
}
func TestLast(t *testing.T) {
assert.Equal(t, O.None[int](), Last([]int{}))
assert.Equal(t, O.Some(3), Last([]int{1, 2, 3}))
assert.Equal(t, O.Some(1), Last([]int{1}))
}
func TestUpsertAt(t *testing.T) {
src := []int{1, 2, 3}
upsert := UpsertAt(99)
result1 := upsert(src)
assert.Equal(t, []int{1, 2, 3, 99}, result1)
}
func TestSize(t *testing.T) {
assert.Equal(t, 0, Size([]int{}))
assert.Equal(t, 3, Size([]int{1, 2, 3}))
}
func TestMonadPartition(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
result := MonadPartition(src, func(x int) bool { return x > 2 })
assert.Equal(t, []int{1, 2}, result.F1)
assert.Equal(t, []int{3, 4, 5}, result.F2)
}
func TestIsNil(t *testing.T) {
var nilSlice []int
assert.True(t, IsNil(nilSlice))
assert.False(t, IsNil([]int{}))
assert.False(t, IsNil([]int{1}))
}
func TestIsNonNil(t *testing.T) {
var nilSlice []int
assert.False(t, IsNonNil(nilSlice))
assert.True(t, IsNonNil([]int{}))
assert.True(t, IsNonNil([]int{1}))
}
func TestConstNil(t *testing.T) {
result := ConstNil[int]()
assert.True(t, IsNil(result))
}
func TestSliceRight(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
slicer := SliceRight[int](2)
result := slicer(src)
assert.Equal(t, []int{3, 4, 5}, result)
}
func TestCopy(t *testing.T) {
src := []int{1, 2, 3}
copied := Copy(src)
assert.Equal(t, src, copied)
// Verify it's a different slice
copied[0] = 99
assert.Equal(t, 1, src[0])
assert.Equal(t, 99, copied[0])
}
func TestClone(t *testing.T) {
src := []int{1, 2, 3}
cloner := Clone(N.Mul(2))
result := cloner(src)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestFoldMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
folder := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := folder(src)
assert.Equal(t, "0:a1:b2:c", result)
}
func TestFold(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
folder := Fold(N.MonoidSum[int]())
result := folder(src)
assert.Equal(t, 15, result)
}
func TestPush(t *testing.T) {
src := []int{1, 2, 3}
pusher := Push(4)
result := pusher(src)
// TestAppend tests the Append function
func TestAppend(t *testing.T) {
// Test appending to non-empty array
arr := []int{1, 2, 3}
result := Append(arr, 4)
assert.Equal(t, []int{1, 2, 3, 4}, result)
// Verify original array is unchanged
assert.Equal(t, []int{1, 2, 3}, arr)
// Test appending to empty array
empty := []int{}
result2 := Append(empty, 1)
assert.Equal(t, []int{1}, result2)
// Test appending strings
words := []string{"hello", "world"}
result3 := Append(words, "!")
assert.Equal(t, []string{"hello", "world", "!"}, result3)
// Test appending to nil array
var nilArr []int
result4 := Append(nilArr, 42)
assert.Equal(t, []int{42}, result4)
}
func TestMonadFlap(t *testing.T) {
fns := []func(int) string{
func(x int) string { return fmt.Sprintf("a%d", x) },
func(x int) string { return fmt.Sprintf("b%d", x) },
}
result := MonadFlap(fns, 5)
assert.Equal(t, []string{"a5", "b5"}, result)
}
// TestStrictEquals tests the StrictEquals function
func TestStrictEquals(t *testing.T) {
eq := StrictEquals[int]()
func TestFlap(t *testing.T) {
fns := []func(int) string{
func(x int) string { return fmt.Sprintf("a%d", x) },
func(x int) string { return fmt.Sprintf("b%d", x) },
}
flapper := Flap[string](5)
result := flapper(fns)
assert.Equal(t, []string{"a5", "b5"}, result)
}
// Test equal arrays
arr1 := []int{1, 2, 3}
arr2 := []int{1, 2, 3}
assert.True(t, eq.Equals(arr1, arr2))
func TestPrepend(t *testing.T) {
src := []int{2, 3, 4}
prepender := Prepend(1)
result := prepender(src)
assert.Equal(t, []int{1, 2, 3, 4}, result)
// Test different arrays
arr3 := []int{1, 2, 4}
assert.False(t, eq.Equals(arr1, arr3))
// Test different lengths
arr4 := []int{1, 2}
assert.False(t, eq.Equals(arr1, arr4))
// Test empty arrays
empty1 := []int{}
empty2 := []int{}
assert.True(t, eq.Equals(empty1, empty2))
// Test with strings
strEq := StrictEquals[string]()
words1 := []string{"hello", "world"}
words2 := []string{"hello", "world"}
words3 := []string{"hello", "there"}
assert.True(t, strEq.Equals(words1, words2))
assert.False(t, strEq.Equals(words1, words3))
}

View File

@@ -24,8 +24,8 @@ import (
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
return n > 2
}
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
}
func TestFilterChain(t *testing.T) {
func TestChainOptionK(t *testing.T) {
src := From(1, 2, 3)
f := func(i int) O.Option[[]string] {
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
return O.None[[]string]()
}
res := FilterChain(f)(src)
res := ChainOptionK(f)(src)
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
}

View File

@@ -63,17 +63,26 @@ func Bind[S1, S2, T any](
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Unlike Bind, the computation function returns a plain value T rather than []T.
// This is useful when you need to compute a derived value from the current context
// without introducing additional array elements.
//
// Example:
//
// result := array.Let(
// func(sum int) func(s struct{ X int }) struct{ X, Sum int } {
// return func(s struct{ X int }) struct{ X, Sum int } {
// return struct{ X, Sum int }{s.X, sum}
// }
// },
// func(s struct{ X int }) int { return s.X * 2 },
// type State1 struct{ X int }
// type State2 struct{ X, Double int }
//
// result := F.Pipe2(
// []State1{{X: 5}, {X: 10}},
// array.Let(
// func(double int) func(s State1) State2 {
// return func(s State1) State2 {
// return State2{X: s.X, Double: double}
// }
// },
// func(s State1) int { return s.X * 2 },
// ),
// )
// // result: []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
//
//go:inline
func Let[S1, S2, T any](
@@ -84,18 +93,25 @@ func Let[S1, S2, T any](
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
// This is useful for adding constant values to the context.
// This is useful for adding constant values to the context without computation.
//
// Example:
//
// result := array.LetTo(
// func(name string) func(s struct{ X int }) struct{ X int; Name string } {
// return func(s struct{ X int }) struct{ X int; Name string } {
// return struct{ X int; Name string }{s.X, name}
// }
// },
// "constant",
// type State1 struct{ X int }
// type State2 struct{ X int; Name string }
//
// result := F.Pipe2(
// []State1{{X: 1}, {X: 2}},
// array.LetTo(
// func(name string) func(s State1) State2 {
// return func(s State1) State2 {
// return State2{X: s.X, Name: name}
// }
// },
// "constant",
// ),
// )
// // result: []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
//
//go:inline
func LetTo[S1, S2, T any](
@@ -107,15 +123,19 @@ func LetTo[S1, S2, T any](
// BindTo initializes a new state S1 from a value T.
// This is typically the first operation after Do to start building the context.
// It transforms each element of type T into a state of type S1.
//
// Example:
//
// type State struct{ X int }
//
// result := F.Pipe2(
// []int{1, 2, 3},
// array.BindTo(func(x int) struct{ X int } {
// return struct{ X int }{x}
// array.BindTo(func(x int) State {
// return State{X: x}
// }),
// )
// // result: []State{{X: 1}, {X: 2}, {X: 3}}
//
//go:inline
func BindTo[S1, T any](

View File

@@ -22,57 +22,176 @@ import (
"github.com/stretchr/testify/assert"
)
type TestState1 struct {
X int
}
type TestState2 struct {
X int
Y int
}
// TestLet tests the Let function
func TestLet(t *testing.T) {
result := F.Pipe2(
Do(TestState1{}),
type State1 struct {
X int
}
type State2 struct {
X int
Double int
}
// Test Let with pure computation
result := F.Pipe1(
[]State1{{X: 5}, {X: 10}},
Let(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s TestState1) int { return s.X * 2 },
func(s State1) int { return s.X * 2 },
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{0}, result)
expected := []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
assert.Equal(t, expected, result)
// Test Let with empty array
empty := []State1{}
result2 := F.Pipe1(
empty,
Let(
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s State1) int { return s.X * 2 },
),
)
assert.Equal(t, []State2{}, result2)
}
// TestLetTo tests the LetTo function
func TestLetTo(t *testing.T) {
result := F.Pipe2(
Do(TestState1{X: 5}),
type State1 struct {
X int
}
type State2 struct {
X int
Name string
}
// Test LetTo with constant value
result := F.Pipe1(
[]State1{{X: 1}, {X: 2}},
LetTo(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
func(name string) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Name: name}
}
},
42,
"constant",
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{47}, result)
expected := []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
assert.Equal(t, expected, result)
// Test LetTo with different constant
result2 := F.Pipe1(
[]State1{{X: 10}},
LetTo(
func(name string) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Name: name}
}
},
"test",
),
)
expected2 := []State2{{X: 10, Name: "test"}}
assert.Equal(t, expected2, result2)
}
// TestBindTo tests the BindTo function
func TestBindTo(t *testing.T) {
type State struct {
X int
}
// Test BindTo with integers
result := F.Pipe1(
[]int{1, 2, 3},
BindTo(func(x int) TestState1 {
return TestState1{X: x}
BindTo(func(x int) State {
return State{X: x}
}),
)
expected := []TestState1{{X: 1}, {X: 2}, {X: 3}}
expected := []State{{X: 1}, {X: 2}, {X: 3}}
assert.Equal(t, expected, result)
// Test BindTo with strings
type StringState struct {
Value string
}
result2 := F.Pipe1(
[]string{"hello", "world"},
BindTo(func(s string) StringState {
return StringState{Value: s}
}),
)
expected2 := []StringState{{Value: "hello"}, {Value: "world"}}
assert.Equal(t, expected2, result2)
// Test BindTo with empty array
empty := []int{}
result3 := F.Pipe1(
empty,
BindTo(func(x int) State {
return State{X: x}
}),
)
assert.Equal(t, []State{}, result3)
}
// TestDoWithLetAndBindTo tests combining Do, Let, LetTo, and BindTo
func TestDoWithLetAndBindTo(t *testing.T) {
type State1 struct {
X int
}
type State2 struct {
X int
Double int
}
type State3 struct {
X int
Double int
Name string
}
// Test complex pipeline
result := F.Pipe3(
[]int{5, 10},
BindTo(func(x int) State1 {
return State1{X: x}
}),
Let(
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s State1) int { return s.X * 2 },
),
LetTo(
func(name string) func(s State2) State3 {
return func(s State2) State3 {
return State3{X: s.X, Double: s.Double, Name: name}
}
},
"result",
),
)
expected := []State3{
{X: 5, Double: 10, Name: "result"},
{X: 10, Double: 20, Name: "result"},
}
assert.Equal(t, expected, result)
}

137
v2/array/coverage.out Normal file
View File

@@ -0,0 +1,137 @@
mode: set
github.com/IBM/fp-go/v2/array/any.go:34.65,36.2 1 1
github.com/IBM/fp-go/v2/array/any.go:48.51,50.2 1 1
github.com/IBM/fp-go/v2/array/array.go:30.33,32.2 1 1
github.com/IBM/fp-go/v2/array/array.go:37.52,39.2 1 1
github.com/IBM/fp-go/v2/array/array.go:44.39,46.2 1 0
github.com/IBM/fp-go/v2/array/array.go:52.50,54.2 1 0
github.com/IBM/fp-go/v2/array/array.go:58.54,61.23 3 0
github.com/IBM/fp-go/v2/array/array.go:61.23,63.3 1 0
github.com/IBM/fp-go/v2/array/array.go:64.2,64.11 1 0
github.com/IBM/fp-go/v2/array/array.go:70.62,72.2 1 0
github.com/IBM/fp-go/v2/array/array.go:83.48,85.2 1 1
github.com/IBM/fp-go/v2/array/array.go:89.52,91.2 1 0
github.com/IBM/fp-go/v2/array/array.go:93.55,96.23 3 0
github.com/IBM/fp-go/v2/array/array.go:96.23,98.14 2 0
github.com/IBM/fp-go/v2/array/array.go:98.14,100.4 1 0
github.com/IBM/fp-go/v2/array/array.go:102.2,102.15 1 0
github.com/IBM/fp-go/v2/array/array.go:105.75,108.23 3 0
github.com/IBM/fp-go/v2/array/array.go:108.23,110.14 2 0
github.com/IBM/fp-go/v2/array/array.go:110.14,112.4 1 0
github.com/IBM/fp-go/v2/array/array.go:114.2,114.15 1 0
github.com/IBM/fp-go/v2/array/array.go:120.54,122.2 1 1
github.com/IBM/fp-go/v2/array/array.go:127.68,129.2 1 0
github.com/IBM/fp-go/v2/array/array.go:132.58,134.2 1 0
github.com/IBM/fp-go/v2/array/array.go:140.67,142.2 1 0
github.com/IBM/fp-go/v2/array/array.go:148.78,150.2 1 0
github.com/IBM/fp-go/v2/array/array.go:155.65,157.2 1 1
github.com/IBM/fp-go/v2/array/array.go:162.76,164.2 1 0
github.com/IBM/fp-go/v2/array/array.go:169.69,171.2 1 1
github.com/IBM/fp-go/v2/array/array.go:174.80,175.26 1 0
github.com/IBM/fp-go/v2/array/array.go:175.26,177.3 1 0
github.com/IBM/fp-go/v2/array/array.go:180.64,182.25 2 0
github.com/IBM/fp-go/v2/array/array.go:182.25,184.3 1 0
github.com/IBM/fp-go/v2/array/array.go:185.2,185.16 1 0
github.com/IBM/fp-go/v2/array/array.go:189.65,191.2 1 1
github.com/IBM/fp-go/v2/array/array.go:194.79,196.2 1 1
github.com/IBM/fp-go/v2/array/array.go:206.62,208.2 1 1
github.com/IBM/fp-go/v2/array/array.go:214.76,216.2 1 0
github.com/IBM/fp-go/v2/array/array.go:221.67,223.2 1 1
github.com/IBM/fp-go/v2/array/array.go:229.81,231.2 1 0
github.com/IBM/fp-go/v2/array/array.go:235.66,236.24 1 0
github.com/IBM/fp-go/v2/array/array.go:236.24,238.3 1 0
github.com/IBM/fp-go/v2/array/array.go:254.37,256.2 1 1
github.com/IBM/fp-go/v2/array/array.go:261.34,263.2 1 1
github.com/IBM/fp-go/v2/array/array.go:266.37,268.2 1 1
github.com/IBM/fp-go/v2/array/array.go:273.25,275.2 1 1
github.com/IBM/fp-go/v2/array/array.go:280.24,282.2 1 0
github.com/IBM/fp-go/v2/array/array.go:287.25,289.2 1 1
github.com/IBM/fp-go/v2/array/array.go:295.56,297.2 1 0
github.com/IBM/fp-go/v2/array/array.go:308.54,310.2 1 1
github.com/IBM/fp-go/v2/array/array.go:316.53,318.2 1 0
github.com/IBM/fp-go/v2/array/array.go:324.50,326.2 1 1
github.com/IBM/fp-go/v2/array/array.go:331.76,333.2 1 1
github.com/IBM/fp-go/v2/array/array.go:338.83,340.2 1 0
github.com/IBM/fp-go/v2/array/array.go:346.38,348.2 1 0
github.com/IBM/fp-go/v2/array/array.go:354.36,356.2 1 1
github.com/IBM/fp-go/v2/array/array.go:362.37,364.2 1 0
github.com/IBM/fp-go/v2/array/array.go:370.36,372.2 1 0
github.com/IBM/fp-go/v2/array/array.go:375.49,376.26 1 1
github.com/IBM/fp-go/v2/array/array.go:376.26,380.35 4 1
github.com/IBM/fp-go/v2/array/array.go:380.35,385.4 4 1
github.com/IBM/fp-go/v2/array/array.go:386.3,386.16 1 1
github.com/IBM/fp-go/v2/array/array.go:395.50,397.26 2 1
github.com/IBM/fp-go/v2/array/array.go:397.26,398.18 1 1
github.com/IBM/fp-go/v2/array/array.go:398.18,400.4 1 1
github.com/IBM/fp-go/v2/array/array.go:401.3,401.25 1 1
github.com/IBM/fp-go/v2/array/array.go:406.60,407.36 1 1
github.com/IBM/fp-go/v2/array/array.go:407.36,409.3 1 1
github.com/IBM/fp-go/v2/array/array.go:419.36,421.2 1 1
github.com/IBM/fp-go/v2/array/array.go:424.49,426.2 1 1
github.com/IBM/fp-go/v2/array/array.go:432.49,434.2 1 1
github.com/IBM/fp-go/v2/array/array.go:440.42,442.2 1 0
github.com/IBM/fp-go/v2/array/array.go:447.30,449.2 1 1
github.com/IBM/fp-go/v2/array/array.go:456.78,458.2 1 0
github.com/IBM/fp-go/v2/array/array.go:464.75,466.2 1 1
github.com/IBM/fp-go/v2/array/array.go:469.32,471.2 1 0
github.com/IBM/fp-go/v2/array/array.go:474.35,476.2 1 0
github.com/IBM/fp-go/v2/array/array.go:479.28,481.2 1 0
github.com/IBM/fp-go/v2/array/array.go:486.50,488.2 1 0
github.com/IBM/fp-go/v2/array/array.go:493.29,495.2 1 0
github.com/IBM/fp-go/v2/array/array.go:500.47,502.2 1 0
github.com/IBM/fp-go/v2/array/array.go:507.67,509.2 1 1
github.com/IBM/fp-go/v2/array/array.go:514.81,516.2 1 0
github.com/IBM/fp-go/v2/array/array.go:521.45,523.2 1 0
github.com/IBM/fp-go/v2/array/array.go:528.38,530.2 1 0
github.com/IBM/fp-go/v2/array/array.go:605.43,607.2 1 1
github.com/IBM/fp-go/v2/array/array.go:613.52,615.2 1 0
github.com/IBM/fp-go/v2/array/array.go:621.49,623.2 1 0
github.com/IBM/fp-go/v2/array/array.go:628.44,630.2 1 0
github.com/IBM/fp-go/v2/array/array.go:714.33,716.2 1 1
github.com/IBM/fp-go/v2/array/array.go:780.53,781.26 1 1
github.com/IBM/fp-go/v2/array/array.go:781.26,782.47 1 1
github.com/IBM/fp-go/v2/array/array.go:782.47,782.67 1 1
github.com/IBM/fp-go/v2/array/array.go:839.31,841.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:36.7,38.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:60.20,62.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:91.20,93.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:120.20,122.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:143.19,145.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:166.20,168.2 1 1
github.com/IBM/fp-go/v2/array/eq.go:35.37,37.49 2 1
github.com/IBM/fp-go/v2/array/eq.go:37.49,39.3 1 1
github.com/IBM/fp-go/v2/array/eq.go:43.45,45.2 1 1
github.com/IBM/fp-go/v2/array/find.go:33.65,35.2 1 1
github.com/IBM/fp-go/v2/array/find.go:48.79,50.2 1 1
github.com/IBM/fp-go/v2/array/find.go:68.78,70.2 1 1
github.com/IBM/fp-go/v2/array/find.go:76.89,78.2 1 1
github.com/IBM/fp-go/v2/array/find.go:89.64,91.2 1 1
github.com/IBM/fp-go/v2/array/find.go:97.78,99.2 1 1
github.com/IBM/fp-go/v2/array/find.go:105.77,107.2 1 1
github.com/IBM/fp-go/v2/array/find.go:113.88,115.2 1 1
github.com/IBM/fp-go/v2/array/magma.go:38.50,40.2 1 1
github.com/IBM/fp-go/v2/array/monad.go:39.65,41.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:35.36,37.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:48.42,50.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:52.45,54.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:68.45,73.48 3 1
github.com/IBM/fp-go/v2/array/monoid.go:73.48,75.3 1 1
github.com/IBM/fp-go/v2/array/monoid.go:77.2,77.12 1 1
github.com/IBM/fp-go/v2/array/sequence.go:27.19,29.2 1 1
github.com/IBM/fp-go/v2/array/sequence.go:69.22,71.2 1 1
github.com/IBM/fp-go/v2/array/sequence.go:92.53,98.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:35.47,37.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:65.68,67.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:96.51,98.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:66.34,68.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:83.24,86.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:94.39,96.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:105.29,108.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:110.142,117.46 1 1
github.com/IBM/fp-go/v2/array/traverse.go:117.46,118.54 1 1
github.com/IBM/fp-go/v2/array/traverse.go:118.54,125.4 1 1
github.com/IBM/fp-go/v2/array/uniq.go:20.43,22.2 1 1
github.com/IBM/fp-go/v2/array/uniq.go:49.60,51.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:38.73,40.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:58.55,60.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:81.62,83.2 1 1

View File

@@ -21,7 +21,7 @@ import (
FC "github.com/IBM/fp-go/v2/internal/functor"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// Of constructs a single element array
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
}
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
return F.Flow2(
FilterMap[GA, []GB](f),
Flatten[[]GB],
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
}
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
left := Empty[GA]()
right := Empty[GA]()
array.Reduce(as, func(c bool, a A) bool {
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
return c
}, true)
// returns the partition
return tuple.MakeTuple2(left, right)
return pair.MakePair(left, right)
}
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
return F.Bind2nd(MonadPartition[GA, A], pred)
}

View File

@@ -18,7 +18,7 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
// longer array are discarded
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
}
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2
as[i] = pair.Head(t)
bs[i] = pair.Tail(t)
}
return T.MakeTuple2(as, bs)
return pair.MakePair(as, bs)
}

View File

@@ -0,0 +1,78 @@
// 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 array
import (
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestSequenceWithOption tests the generic Sequence function with Option monad
func TestSequenceWithOption(t *testing.T) {
// Test with Option monad - all Some values
opts := From(
O.Some(1),
O.Some(2),
O.Some(3),
)
// Use the Sequence function with Option's applicative monoid
monoid := O.ApplicativeMonoid(Monoid[int]())
seq := Sequence(O.Map(Of[int]), monoid)
result := seq(opts)
assert.Equal(t, O.Of(From(1, 2, 3)), result)
// Test with Option monad - contains None
optsWithNone := From(
O.Some(1),
O.None[int](),
O.Some(3),
)
result2 := seq(optsWithNone)
assert.True(t, O.IsNone(result2))
// Test with empty array
empty := Empty[Option[int]]()
result3 := seq(empty)
assert.Equal(t, O.Some(Empty[int]()), result3)
}
// TestMonadSequence tests the MonadSequence function
func TestMonadSequence(t *testing.T) {
// Test with Option monad
opts := From(
O.Some("hello"),
O.Some("world"),
)
monoid := O.ApplicativeMonoid(Monoid[string]())
result := MonadSequence(O.Map(Of[string]), monoid, opts)
assert.Equal(t, O.Of(From("hello", "world")), result)
// Test with None in the array
optsWithNone := From(
O.Some("hello"),
O.None[string](),
)
result2 := MonadSequence(O.Map(Of[string]), monoid, optsWithNone)
assert.Equal(t, O.None[[]string](), result2)
}

View File

@@ -0,0 +1,164 @@
// 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 array
import (
"strconv"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestMonadTraverse tests the MonadTraverse function
func TestMonadTraverse(t *testing.T) {
// Test converting integers to strings via Option
numbers := []int{1, 2, 3}
result := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"1", "2", "3"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with a function that can return None
result2 := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(n int) O.Option[string] {
if n == 2 {
return O.None[string]()
}
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsNone(result2))
// Test with empty array
empty := []int{}
result3 := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
empty,
func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsSome(result3))
assert.Equal(t, []string{}, O.GetOrElse(func() []string { return nil })(result3))
}
// TestTraverseWithIndex tests the TraverseWithIndex function
func TestTraverseWithIndex(t *testing.T) {
// Test with index-aware transformation
words := []string{"a", "b", "c"}
traverser := TraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
func(idx int, s string) O.Option[string] {
return O.Some(s + strconv.Itoa(idx))
},
)
result := traverser(words)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"a0", "b1", "c2"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with conditional None based on index
traverser2 := TraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
func(idx int, s string) O.Option[string] {
if idx == 1 {
return O.None[string]()
}
return O.Some(s)
},
)
result2 := traverser2(words)
assert.True(t, O.IsNone(result2))
}
// TestMonadTraverseWithIndex tests the MonadTraverseWithIndex function
func TestMonadTraverseWithIndex(t *testing.T) {
// Test with index-aware transformation
numbers := []int{10, 20, 30}
result := MonadTraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(idx, n int) O.Option[string] {
return O.Some(strconv.Itoa(n * idx))
},
)
assert.True(t, O.IsSome(result))
// Expected: [10*0, 20*1, 30*2] = ["0", "20", "60"]
assert.Equal(t, []string{"0", "20", "60"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with None at specific index
result2 := MonadTraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(idx, n int) O.Option[string] {
if idx == 2 {
return O.None[string]()
}
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsNone(result2))
}
// TestMakeTraverseType tests the MakeTraverseType function
func TestMakeTraverseType(t *testing.T) {
// Create a traverse type for Option
traverseType := MakeTraverseType[int, string, O.Option[string], O.Option[[]string], O.Option[func(string) []string]]()
// Use it to traverse an array
numbers := []int{1, 2, 3}
result := traverseType(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
)(func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n * 2))
})(numbers)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"2", "4", "6"}, O.GetOrElse(func() []string { return []string{} })(result))
}

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays,
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
// // Result: [(a, 1), (b, 2)]
//
//go:inline
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
}
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
// ages := result.Tail // [30, 25, 35]
//
//go:inline
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
return G.Unzip[[]A, []B](cs)
}

View File

@@ -19,7 +19,7 @@ import (
"fmt"
"testing"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
res := Zip[string](left)(right)
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
}
func TestUnzip(t *testing.T) {
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
unzipped := Unzip(zipped)
assert.Equal(t, right, unzipped.F1)
assert.Equal(t, left, unzipped.F2)
assert.Equal(t, right, pair.Head(unzipped))
assert.Equal(t, left, pair.Tail(unzipped))
}

View File

@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
}
}
// ArrayEmpty checks if an array is empty.
//
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
//
// Example:
//
// func TestArrayEmpty(t *testing.T) {
// empty := []int{}
// assert.ArrayEmpty(empty)(t) // Passes
//
// numbers := []int{1, 2, 3}
// assert.ArrayEmpty(numbers)(t) // Fails
// }
func ArrayEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, arr)
}
}
// RecordNotEmpty checks if a map is not empty.
//
// Example:
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
}
}
// RecordEmpty checks if a map is empty.
//
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
//
// Example:
//
// func TestRecordEmpty(t *testing.T) {
// empty := map[string]int{}
// assert.RecordEmpty(empty)(t) // Passes
//
// config := map[string]int{"timeout": 30}
// assert.RecordEmpty(config)(t) // Fails
// }
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, mp)
}
}
// StringNotEmpty checks if a string is not empty.
//
// Example:
@@ -504,15 +542,7 @@ func AllOf(readers []Reader) Reader {
//
//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
}
return SequenceRecord(testcases)
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,

View File

@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
})
}
func TestArrayEmpty(t *testing.T) {
t.Run("should pass for empty array", func(t *testing.T) {
arr := []int{}
result := ArrayEmpty(arr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty array")
}
})
t.Run("should fail for non-empty array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayEmpty(arr)(mockT)
if result {
t.Error("Expected ArrayEmpty to fail for non-empty array")
}
})
t.Run("should work with different types", func(t *testing.T) {
strArr := []string{}
result := ArrayEmpty(strArr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty string 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}
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
})
}
func TestRecordEmpty(t *testing.T) {
t.Run("should pass for empty map", func(t *testing.T) {
mp := map[string]int{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map")
}
})
t.Run("should fail for non-empty map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := RecordEmpty(mp)(mockT)
if result {
t.Error("Expected RecordEmpty to fail for non-empty map")
}
})
t.Run("should work with different key-value types", func(t *testing.T) {
mp := map[int]string{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map with int keys")
}
})
}
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}
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
})
}
func TestStringNotEmpty(t *testing.T) {
t.Run("should pass for non-empty string", func(t *testing.T) {
str := "Hello, World!"
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for non-empty string")
}
})
t.Run("should fail for empty string", func(t *testing.T) {
mockT := &testing.T{}
str := ""
result := StringNotEmpty(str)(mockT)
if result {
t.Error("Expected StringNotEmpty to fail for empty string")
}
})
t.Run("should pass for string with whitespace", func(t *testing.T) {
str := " "
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for string with whitespace")
}
})
}
func TestStringLength(t *testing.T) {
t.Run("should pass when string length matches", func(t *testing.T) {
str := "hello"

122
v2/assert/from.go Normal file
View File

@@ -0,0 +1,122 @@
// 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
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations that may fail
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
// ReaderIOResult computation using the test's context, handles any potential errors by
// converting them to test failures via NoError, and returns the resulting Reader.
//
// The conversion process:
// 1. Executes the ReaderIOResult with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Extracts the Result, converting errors to test failures using NoError
// 4. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations (file access, network calls, etc.)
// - Handle potential errors gracefully in tests
//
// Parameters:
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithContext(t *testing.T) {
// // Create a ReaderIOResult that performs an IO operation
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Simulate database check
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
return func(t *testing.T) bool {
return F.Pipe1(
ri(t.Context())(),
result.GetOrElse(NoError),
)(t)
}
}
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
// and the simpler Reader type used for test assertions. It executes the ReaderIO
// computation using the test's context and returns the resulting Reader.
//
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
//
// The conversion process:
// 1. Executes the ReaderIO with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware testing utilities
//
// Parameters:
// - ri: A ReaderIO that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithIO(t *testing.T) {
// // Create a ReaderIO that performs an IO operation
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log something using context
// logger.InfoContext(ctx, "Running test")
// // Return an assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
func FromReaderIO(ri ReaderIO[Reader]) Reader {
return func(t *testing.T) bool {
return ri(t.Context())()(t)
}
}

383
v2/assert/from_test.go Normal file
View File

@@ -0,0 +1,383 @@
// 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
import (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/result"
)
func TestFromReaderIOResult(t *testing.T) {
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Reader
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Return a Reader that always passes
return result.Of[Reader](func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
}
})
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Equal assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(42))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns an error
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Left[Reader](errors.New("test error"))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
}
})
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns a failing assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(43))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIOResult that checks if context is provided
ri := func(ctx context.Context) func() result.Result[Reader] {
if ctx != nil {
contextUsed = true
}
return func() result.Result[Reader] {
return result.Of[Reader](func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIOResult to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns NoError assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](NoError(nil))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIOResult with multiple composed assertions
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
arr := []int{1, 2, 3}
assertions := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](3)(arr),
ArrayContains(2)(arr),
})
return result.Of[Reader](assertions)
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with complex assertions")
}
})
}
func TestFromReaderIO(t *testing.T) {
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
// Create a ReaderIO that returns a Reader that always passes
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
}
})
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
// Create a ReaderIO that returns an Equal assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(42)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIO that returns a failing assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(43)
}
}
reader := FromReaderIO(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIO to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIO that checks if context is provided
ri := func(ctx context.Context) func() Reader {
if ctx != nil {
contextUsed = true
}
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIO to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIO that returns NoError assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return NoError(nil)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with NoError assertion")
}
})
t.Run("should work with Error assertion", func(t *testing.T) {
// Create a ReaderIO that returns Error assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Error(errors.New("expected error"))
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Error assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIO with multiple composed assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
mp := map[string]int{"a": 1, "b": 2}
return AllOf([]Reader{
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
ContainsKey[int]("a")(mp),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with complex assertions")
}
})
t.Run("should work with string assertions", func(t *testing.T) {
// Create a ReaderIO with string assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
str := "hello world"
return AllOf([]Reader{
StringNotEmpty(str),
StringLength[any, any](11)(str),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with string assertions")
}
})
t.Run("should work with Result assertions", func(t *testing.T) {
// Create a ReaderIO with Result assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
successResult := result.Of[int](42)
return Success(successResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Success assertion")
}
})
t.Run("should work with Failure assertion", func(t *testing.T) {
// Create a ReaderIO with Failure assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
failureResult := result.Left[int](errors.New("test error"))
return Failure(failureResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Failure assertion")
}
})
}
// TestFromReaderIOResultIntegration tests integration scenarios
func TestFromReaderIOResultIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
// Create a ReaderIOResult that uses the context
ri := func(testCtx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Check if context is valid
if testCtx == nil {
return result.Left[Reader](errors.New("context is nil"))
}
// Return a successful assertion
return result.Of[Reader](Equal("test")("test"))
}
}
// Use the actual testing.T from the subtest
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
})
}
// TestFromReaderIOIntegration tests integration scenarios
func TestFromReaderIOIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
logCalled := false
// Create a ReaderIO that simulates logging
ri := func(ctx context.Context) func() Reader {
return func() Reader {
// Simulate logging with context
if ctx != nil {
logCalled = true
}
// Return an assertion
return Equal(100)(100)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
if !logCalled {
t.Error("Expected logging to be called")
}
})
}

207
v2/assert/logger.go Normal file
View File

@@ -0,0 +1,207 @@
// 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
import (
"testing"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/readerio"
)
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
//
// This function provides a functional programming approach to test logging, returning a
// [ReaderIO] that can be composed with other test operations. It's particularly useful
// for debugging tests, tracing execution flow, or documenting test behavior without
// affecting test outcomes.
//
// The function uses a curried design pattern:
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
// 2. This returns a function that takes a value of type T
// 3. That function returns a ReaderIO that performs the logging when executed
//
// # Parameters
//
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
// The format string should contain exactly one format verb that matches type T
//
// # Returns
//
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
// When executed, this ReaderIO logs the formatted message to the test output
//
// # Type Parameters
//
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
//
// # Use Cases
//
// - Debugging test execution by logging intermediate values
// - Tracing the flow of complex test scenarios
// - Documenting test behavior in the test output
// - Logging values in functional pipelines without breaking the chain
// - Creating reusable logging operations for specific types
//
// # Example - Basic Logging
//
// func TestBasicLogging(t *testing.T) {
// // Create a logger for integers
// logInt := assert.Logf[int]("Processing value: %d")
//
// // Use it to log a value
// value := 42
// logInt(value)(t)() // Outputs: "Processing value: 42"
// }
//
// # Example - Logging in Test Pipeline
//
// func TestPipelineWithLogging(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// user := User{Name: "Alice", Age: 30}
//
// // Create a logger for User
// logUser := assert.Logf[User]("Testing user: %+v")
//
// // Log the user being tested
// logUser(user)(t)()
//
// // Continue with assertions
// assert.StringNotEmpty(user.Name)(t)
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
// }
//
// # Example - Multiple Loggers for Different Types
//
// func TestMultipleLoggers(t *testing.T) {
// // Create type-specific loggers
// logString := assert.Logf[string]("String value: %s")
// logInt := assert.Logf[int]("Integer value: %d")
// logFloat := assert.Logf[float64]("Float value: %.2f")
//
// // Use them throughout the test
// logString("hello")(t)() // Outputs: "String value: hello"
// logInt(42)(t)() // Outputs: "Integer value: 42"
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
// }
//
// # Example - Logging Complex Structures
//
// func TestComplexStructureLogging(t *testing.T) {
// type Config struct {
// Host string
// Port int
// Timeout int
// }
//
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
//
// // Use %+v to include field names
// logConfig := assert.Logf[Config]("Configuration: %+v")
// logConfig(config)(t)()
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
//
// // Or use %#v for Go-syntax representation
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
// logConfigGo(config)(t)()
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
// }
//
// # Example - Debugging Test Failures
//
// func TestWithDebugLogging(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// logSlice := assert.Logf[[]int]("Testing slice: %v")
//
// // Log the input data
// logSlice(numbers)(t)()
//
// // Perform assertions
// assert.ArrayNotEmpty(numbers)(t)
// assert.ArrayLength[int](5)(numbers)(t)
//
// // Log intermediate results
// sum := 0
// for _, n := range numbers {
// sum += n
// }
// logInt := assert.Logf[int]("Sum: %d")
// logInt(sum)(t)()
//
// assert.Equal(15)(sum)(t)
// }
//
// # Example - Conditional Logging
//
// func TestConditionalLogging(t *testing.T) {
// logDebug := assert.Logf[string]("DEBUG: %s")
//
// values := []int{1, 2, 3, 4, 5}
// for _, v := range values {
// if v%2 == 0 {
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
// }
// }
// // Outputs:
// // DEBUG: Found even number: 2
// // DEBUG: Found even number: 4
// }
//
// # Format Verbs
//
// Common format verbs you can use in the prefix string:
// - %v: Default format
// - %+v: Default format with field names for structs
// - %#v: Go-syntax representation
// - %T: Type of the value
// - %d: Integer in base 10
// - %s: String
// - %f: Floating point number
// - %t: Boolean (true/false)
// - %p: Pointer address
//
// See the fmt package documentation for a complete list of format verbs.
//
// # Notes
//
// - Logging does not affect test pass/fail status
// - Log output appears in test results when running with -v flag or when tests fail
// - The function returns Void, indicating it's used for side effects only
// - The ReaderIO pattern allows logging to be composed with other operations
//
// # Related Functions
//
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
// - testing.T.Logf: The underlying Go testing log function
//
// # References
//
// - Go testing package: https://pkg.go.dev/testing
// - fmt package format verbs: https://pkg.go.dev/fmt
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
return func(a T) readerio.ReaderIO[*testing.T, Void] {
return func(t *testing.T) IO[Void] {
return io.FromImpure(func() {
t.Logf(prefix, a)
})
}
}
}

406
v2/assert/logger_test.go Normal file
View File

@@ -0,0 +1,406 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"fmt"
"testing"
)
// TestLogf_BasicInteger tests basic integer logging
func TestLogf_BasicInteger(t *testing.T) {
logInt := Logf[int]("Processing value: %d")
// This should not panic and should log the value
logInt(42)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicString tests basic string logging
func TestLogf_BasicString(t *testing.T) {
logString := Logf[string]("String value: %s")
logString("hello world")(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicFloat tests basic float logging
func TestLogf_BasicFloat(t *testing.T) {
logFloat := Logf[float64]("Float value: %.2f")
logFloat(3.14159)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicBoolean tests basic boolean logging
func TestLogf_BasicBoolean(t *testing.T) {
logBool := Logf[bool]("Boolean value: %t")
logBool(true)(t)()
logBool(false)(t)()
// Test passes if no panic occurs
}
// TestLogf_ComplexStruct tests logging of complex structures
func TestLogf_ComplexStruct(t *testing.T) {
type User struct {
Name string
Age int
}
logUser := Logf[User]("User: %+v")
user := User{Name: "Alice", Age: 30}
logUser(user)(t)()
// Test passes if no panic occurs
}
// TestLogf_Slice tests logging of slices
func TestLogf_Slice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
// Test passes if no panic occurs
}
// TestLogf_Map tests logging of maps
func TestLogf_Map(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
data := map[string]int{"a": 1, "b": 2, "c": 3}
logMap(data)(t)()
// Test passes if no panic occurs
}
// TestLogf_Pointer tests logging of pointers
func TestLogf_Pointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %p")
value := 42
logPtr(&value)(t)()
// Test passes if no panic occurs
}
// TestLogf_NilPointer tests logging of nil pointers
func TestLogf_NilPointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %v")
var nilPtr *int
logPtr(nilPtr)(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyString tests logging of empty strings
func TestLogf_EmptyString(t *testing.T) {
logString := Logf[string]("String: '%s'")
logString("")(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptySlice tests logging of empty slices
func TestLogf_EmptySlice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
logSlice([]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyMap tests logging of empty maps
func TestLogf_EmptyMap(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
logMap(map[string]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_MultipleTypes tests using multiple loggers for different types
func TestLogf_MultipleTypes(t *testing.T) {
logString := Logf[string]("String: %s")
logInt := Logf[int]("Integer: %d")
logFloat := Logf[float64]("Float: %.2f")
logString("test")(t)()
logInt(42)(t)()
logFloat(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_WithinTestPipeline tests logging within a test pipeline
func TestLogf_WithinTestPipeline(t *testing.T) {
type Config struct {
Host string
Port int
}
config := Config{Host: "localhost", Port: 8080}
logConfig := Logf[Config]("Testing config: %+v")
logConfig(config)(t)()
// Continue with assertions
StringNotEmpty(config.Host)(t)
That(func(port int) bool { return port > 0 })(config.Port)(t)
// Test passes if no panic occurs and assertions pass
}
// TestLogf_NestedStructures tests logging of nested structures
func TestLogf_NestedStructures(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
logPerson := Logf[Person]("Person: %+v")
person := Person{
Name: "Bob",
Address: Address{
Street: "123 Main St",
City: "Springfield",
},
}
logPerson(person)(t)()
// Test passes if no panic occurs
}
// TestLogf_Interface tests logging of interface values
func TestLogf_Interface(t *testing.T) {
logAny := Logf[any]("Value: %v")
logAny(42)(t)()
logAny("string")(t)()
logAny([]int{1, 2, 3})(t)()
// Test passes if no panic occurs
}
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
func TestLogf_GoSyntaxFormat(t *testing.T) {
type Point struct {
X int
Y int
}
logPoint := Logf[Point]("Point: %#v")
point := Point{X: 10, Y: 20}
logPoint(point)(t)()
// Test passes if no panic occurs
}
// TestLogf_TypeFormat tests logging with type format
func TestLogf_TypeFormat(t *testing.T) {
logType := Logf[any]("Type: %T, Value: %v")
logType(42)(t)()
logType("string")(t)()
logType(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_LargeNumbers tests logging of large numbers
func TestLogf_LargeNumbers(t *testing.T) {
logInt := Logf[int64]("Large number: %d")
logInt(9223372036854775807)(t)() // Max int64
// Test passes if no panic occurs
}
// TestLogf_NegativeNumbers tests logging of negative numbers
func TestLogf_NegativeNumbers(t *testing.T) {
logInt := Logf[int]("Number: %d")
logInt(-42)(t)()
logInt(-100)(t)()
// Test passes if no panic occurs
}
// TestLogf_SpecialFloats tests logging of special float values
func TestLogf_SpecialFloats(t *testing.T) {
logFloat := Logf[float64]("Float: %v")
logFloat(0.0)(t)()
logFloat(-0.0)(t)()
// Test passes if no panic occurs
}
// TestLogf_UnicodeStrings tests logging of unicode strings
func TestLogf_UnicodeStrings(t *testing.T) {
logString := Logf[string]("Unicode: %s")
logString("Hello, 世界")(t)()
logString("Emoji: 🎉🎊")(t)()
// Test passes if no panic occurs
}
// TestLogf_MultilineStrings tests logging of multiline strings
func TestLogf_MultilineStrings(t *testing.T) {
logString := Logf[string]("Multiline:\n%s")
multiline := `Line 1
Line 2
Line 3`
logString(multiline)(t)()
// Test passes if no panic occurs
}
// TestLogf_ReuseLogger tests reusing the same logger multiple times
func TestLogf_ReuseLogger(t *testing.T) {
logInt := Logf[int]("Value: %d")
for i := 0; i < 5; i++ {
logInt(i)(t)()
}
// Test passes if no panic occurs
}
// TestLogf_ConditionalLogging tests conditional logging based on values
func TestLogf_ConditionalLogging(t *testing.T) {
logDebug := Logf[string]("DEBUG: %s")
values := []int{1, 2, 3, 4, 5}
for _, v := range values {
if v%2 == 0 {
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
}
}
// Test passes if no panic occurs
}
// TestLogf_WithAssertions tests combining logging with assertions
func TestLogf_WithAssertions(t *testing.T) {
logInt := Logf[int]("Testing value: %d")
value := 42
logInt(value)(t)()
// Perform assertion after logging
Equal(42)(value)(t)
// Test passes if assertion passes
}
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
func TestLogf_DebuggingFailures(t *testing.T) {
logSlice := Logf[[]int]("Input slice: %v")
logInt := Logf[int]("Computed sum: %d")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
sum := 0
for _, n := range numbers {
sum += n
}
logInt(sum)(t)()
Equal(15)(sum)(t)
// Test passes if assertion passes
}
// TestLogf_ComplexDataStructures tests logging of complex nested data
func TestLogf_ComplexDataStructures(t *testing.T) {
type Metadata struct {
Version string
Tags []string
}
type Document struct {
ID int
Title string
Metadata Metadata
}
logDoc := Logf[Document]("Document: %+v")
doc := Document{
ID: 1,
Title: "Test Document",
Metadata: Metadata{
Version: "1.0",
Tags: []string{"test", "example"},
},
}
logDoc(doc)(t)()
// Test passes if no panic occurs
}
// TestLogf_ArrayTypes tests logging of array types
func TestLogf_ArrayTypes(t *testing.T) {
logArray := Logf[[5]int]("Array: %v")
arr := [5]int{1, 2, 3, 4, 5}
logArray(arr)(t)()
// Test passes if no panic occurs
}
// TestLogf_ChannelTypes tests logging of channel types
func TestLogf_ChannelTypes(t *testing.T) {
logChan := Logf[chan int]("Channel: %v")
ch := make(chan int, 1)
logChan(ch)(t)()
close(ch)
// Test passes if no panic occurs
}
// TestLogf_FunctionTypes tests logging of function types
func TestLogf_FunctionTypes(t *testing.T) {
logFunc := Logf[func() int]("Function: %v")
fn := func() int { return 42 }
logFunc(fn)(t)()
// Test passes if no panic occurs
}

152
v2/assert/monoid.go Normal file
View File

@@ -0,0 +1,152 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"testing"
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/reader"
)
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
//
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
// meaning all assertions must pass for the combined assertion to pass. It leverages the
// applicative structure of Reader to execute multiple assertions with the same testing.T
// context and combines their boolean results using boolean.MonoidAll (logical AND).
//
// The monoid provides:
// - Concat: Combines two assertions such that both must pass (logical AND)
// - Empty: Returns an assertion that always passes (identity element)
//
// This is particularly useful for:
// - Composing multiple test assertions into a single assertion
// - Building complex test conditions from simpler ones
// - Creating reusable assertion combinators
// - Implementing test assertion DSLs
//
// # Monoid Laws
//
// The returned monoid satisfies the standard monoid laws:
//
// 1. Associativity:
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
//
// 2. Left Identity:
// Concat(Empty(), a) ≡ a
//
// 3. Right Identity:
// Concat(a, Empty()) ≡ a
//
// # Returns
//
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
//
// # Example - Basic Usage
//
// func TestUserValidation(t *testing.T) {
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
// m := assert.ApplicativeMonoid()
//
// // Combine multiple assertions
// assertion := m.Concat(
// assert.Equal("Alice")(user.Name),
// m.Concat(
// assert.Equal(30)(user.Age),
// assert.StringNotEmpty(user.Email),
// ),
// )
//
// // Execute combined assertion
// assertion(t) // All three assertions must pass
// }
//
// # Example - Building Reusable Validators
//
// func TestWithReusableValidators(t *testing.T) {
// m := assert.ApplicativeMonoid()
//
// // Create a reusable validator
// validateUser := func(u User) assert.Reader {
// return m.Concat(
// assert.StringNotEmpty(u.Name),
// m.Concat(
// assert.True(u.Age > 0),
// assert.StringContains("@")(u.Email),
// ),
// )
// }
//
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
// validateUser(user)(t)
// }
//
// # Example - Using Empty for Identity
//
// func TestEmptyIdentity(t *testing.T) {
// m := assert.ApplicativeMonoid()
// assertion := assert.Equal(42)(42)
//
// // Empty is the identity - these are equivalent
// result1 := m.Concat(m.Empty(), assertion)(t)
// result2 := m.Concat(assertion, m.Empty())(t)
// result3 := assertion(t)
// // All three produce the same result
// }
//
// # Example - Combining with AllOf
//
// func TestCombiningWithAllOf(t *testing.T) {
// // ApplicativeMonoid provides the underlying mechanism for AllOf
// arr := []int{1, 2, 3, 4, 5}
//
// // These are conceptually equivalent:
// m := assert.ApplicativeMonoid()
// manual := m.Concat(
// assert.ArrayNotEmpty(arr),
// m.Concat(
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// ),
// )
//
// // AllOf uses ApplicativeMonoid internally
// convenient := assert.AllOf([]assert.Reader{
// assert.ArrayNotEmpty(arr),
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// })
//
// manual(t)
// convenient(t)
// }
//
// # Related Functions
//
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
//
// # References
//
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
func ApplicativeMonoid() monoid.Monoid[Reader] {
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
}

454
v2/assert/monoid_test.go Normal file
View File

@@ -0,0 +1,454 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"testing"
)
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
func TestApplicativeMonoid_Empty(t *testing.T) {
m := ApplicativeMonoid()
empty := m.Empty()
result := empty(t)
if !result {
t.Error("Expected Empty() to return an assertion that always passes")
}
}
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(t)
if !result {
t.Error("Expected Concat to pass when both assertions pass")
}
}
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when first assertion fails")
}
}
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when second assertion fails")
}
}
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when both assertions fail")
}
}
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(Empty(), assertion) should behave the same as assertion
combined := m.Concat(m.Empty(), assertion)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
}
}
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(assertion, Empty()) should behave the same as assertion
combined := m.Concat(assertion, m.Empty())
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
}
}
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
func TestApplicativeMonoid_Associativity(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(t)
result2 := right(t)
if result1 != result2 {
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
}
}
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(3) // This will fail
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(mockT)
result2 := right(mockT)
if result1 != result2 {
t.Error("Associativity law violated even with failures")
}
if result1 || result2 {
t.Error("Expected both to fail when one assertion fails")
}
}
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(t)
if !result {
t.Error("Expected complex combined assertions to pass")
}
}
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
arr := []int{1, 2, 3}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(mockT)
if result {
t.Error("Expected complex combined assertions to fail when one assertion fails")
}
}
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
a4 := Equal(4)(4)
combined := m.Concat(
m.Concat(a1, a2),
m.Concat(a3, a4),
)
result := combined(t)
if !result {
t.Error("Expected multiple Concat operations to pass when all assertions pass")
}
}
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
m := ApplicativeMonoid()
str := "hello world"
combined := m.Concat(
StringNotEmpty(str),
StringLength[any, any](11)(str),
)
result := combined(t)
if !result {
t.Error("Expected string assertions to pass")
}
}
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
Equal(true)(true),
m.Concat(
Equal(false)(false),
Equal(true)(true),
),
)
result := combined(t)
if !result {
t.Error("Expected boolean assertions to pass")
}
}
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
NoError(nil),
Equal("test")("test"),
)
result := combined(t)
if !result {
t.Error("Expected error assertions to pass")
}
}
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Multiple Empty values should still act as identity
combined := m.Concat(
m.Empty(),
m.Concat(
assertion,
m.Empty(),
),
)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Multiple Empty values should still act as identity")
}
}
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
m := ApplicativeMonoid()
// Concat of Empty values should still be Empty (identity)
combined := m.Concat(m.Empty(), m.Empty())
result := combined(t)
if !result {
t.Error("Expected Concat of Empty values to pass")
}
}
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
result := validateUser(validUser)(t)
if !result {
t.Error("Expected valid user to pass all validations")
}
}
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
mockT := &testing.T{}
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
result := validateUser(invalidUser)(mockT)
if result {
t.Error("Expected invalid user to fail validation")
}
}
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
// Using ApplicativeMonoid directly
manualCombination := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
// Using AllOf (which uses ApplicativeMonoid internally)
allOfCombination := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
})
result1 := manualCombination(t)
result2 := allOfCombination(t)
if result1 != result2 {
t.Error("Expected manual combination and AllOf to produce same result")
}
if !result1 || !result2 {
t.Error("Expected both combinations to pass")
}
}

650
v2/assert/traverse.go Normal file
View File

@@ -0,0 +1,650 @@
// 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
import (
"testing"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
)
// TraverseArray transforms an array of values into a test suite by applying a function
// that generates named test cases for each element.
//
// This function enables data-driven testing where you have a collection of test inputs
// and want to run a named subtest for each one. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each element of the array, applies the provided function to generate
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
// Go's t.Run. All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A function that takes a value of type T and returns a [Pair] containing:
// - Head: The test name (string) for the subtest
// - Tail: The test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
// - Executes each element as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with multiple test cases
// - Parameterized tests where each parameter gets its own subtest
// - Testing collections where each element needs validation
// - Property-based testing with generated test data
//
// # Example - Basic Data-Driven Testing
//
// func TestMathOperations(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// {Input: 4, Expected: 16},
// }
//
// square := func(n int) int { return n * n }
//
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// # Example - String Validation
//
// func TestStringValidation(t *testing.T) {
// inputs := []string{"hello", "world", "test"}
//
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("validate_%s", s),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(s),
// assert.That(func(str string) bool { return len(str) > 0 })(s),
// }),
// )
// })
//
// traverse(inputs)(t)
// }
//
// # Example - Complex Object Testing
//
// func TestUsers(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// users := []User{
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
// }
//
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("user_%s", u.Name),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 })(u.Age),
// assert.That(func(email string) bool {
// return len(email) > 0 && strings.Contains(email, "@")
// })(u.Email),
// }),
// )
// })
//
// traverse(users)(t)
// }
//
// # Comparison with RunAll
//
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
//
// - TraverseArray: Generates test cases from an array of data
//
// - Input: Array of values + function to generate test cases
//
// - Use when: You have test data and need to generate test cases from it
//
// - RunAll: Executes pre-defined named test cases
//
// - Input: Map of test names to assertions
//
// - Use when: You have already defined test cases with names
//
// # Related Functions
//
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
return func(ts []T) Reader {
return func(t *testing.T) bool {
ok := true
for _, src := range ts {
test := f(src)
res := t.Run(pair.Head(test), func(t *testing.T) {
pair.Tail(test)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
//
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
// requiring an array.
//
// The function iterates through all test cases, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - s: A [Seq2] iterator that yields pairs of:
// - Key: Test name (string) for the subtest
// - Value: Test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each test case as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Working with iterator-based test data
// - Lazy evaluation of test cases
// - Integration with Go 1.23+ iterator patterns
// - Memory-efficient testing of large test suites
//
// # Example - Basic Usage with Iterator
//
// func TestWithIterator(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// assert.SequenceSeq2(testCases)(t)
// }
//
// # Example - Generated Test Cases
//
// func TestGeneratedCases(t *testing.T) {
// // Generate test cases on the fly
// generateTests := func(yield func(string, assert.Reader) bool) {
// for i := 1; i <= 5; i++ {
// name := fmt.Sprintf("test_%d", i)
// assertion := assert.Equal(i*i)(i * i)
// if !yield(name, assertion) {
// return
// }
// }
// }
//
// assert.SequenceSeq2(generateTests)(t)
// }
//
// # Example - Filtering Test Cases
//
// func TestFilteredCases(t *testing.T) {
// type TestCase struct {
// Name string
// Input int
// Expected int
// Skip bool
// }
//
// allCases := []TestCase{
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
// }
//
// // Create iterator that filters out skipped tests
// activeTests := func(yield func(string, assert.Reader) bool) {
// for _, tc := range allCases {
// if !tc.Skip {
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// if !yield(tc.Name, assertion) {
// return
// }
// }
// }
// }
//
// assert.SequenceSeq2(activeTests)(t)
// }
//
// # Comparison with TraverseArray
//
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
//
// - SequenceSeq2: Works with iterators (Seq2)
//
// - Input: Iterator yielding (name, assertion) pairs
//
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
//
// - Memory: More efficient for large test suites (lazy evaluation)
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + transformation function
//
// - Use when: You have an array of test data
//
// - Memory: All test data must be in memory
//
// # Comparison with RunAll
//
// SequenceSeq2 and [RunAll] are very similar:
//
// - SequenceSeq2: Takes an iterator (Seq2)
// - RunAll: Takes a map[string]Reader
//
// Both execute named test cases as subtests. Choose based on your data structure:
// use SequenceSeq2 for iterators, RunAll for maps.
//
// # Related Functions
//
// - [TraverseArray]: Similar but works with arrays instead of iterators
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Go iterators: https://go.dev/blog/range-functions
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
return func(t *testing.T) bool {
ok := true
for name, test := range s {
res := t.Run(name, func(t *testing.T) {
test(t)
})
ok = ok && res
}
return ok
}
}
// TraverseRecord transforms a map of values into a test suite by applying a function
// that generates test assertions for each map entry.
//
// This function enables data-driven testing where you have a map of test data and want
// to run a named subtest for each entry. The map keys become test names, and the function
// transforms each value into a test assertion. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each key-value pair from the map, applies the provided function to
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
// All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
//
// # Returns
//
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
// - Executes each map entry as a named subtest (using the key as the test name)
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with named test cases in a map
// - Testing configuration maps where keys are meaningful names
// - Validating collections where natural keys exist
// - Property-based testing with named scenarios
//
// # Example - Basic Configuration Testing
//
// func TestConfigurations(t *testing.T) {
// configs := map[string]int{
// "timeout": 30,
// "maxRetries": 3,
// "bufferSize": 1024,
// }
//
// validatePositive := assert.That(func(n int) bool { return n > 0 })
//
// traverse := assert.TraverseRecord(validatePositive)
// traverse(configs)(t)
// }
//
// # Example - User Validation
//
// func TestUserMap(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// users := map[string]User{
// "alice": {Name: "Alice", Age: 30},
// "bob": {Name: "Bob", Age: 25},
// "carol": {Name: "Carol", Age: 35},
// }
//
// validateUser := func(u User) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
// })
// }
//
// traverse := assert.TraverseRecord(validateUser)
// traverse(users)(t)
// }
//
// # Example - API Endpoint Testing
//
// func TestEndpoints(t *testing.T) {
// type Endpoint struct {
// Path string
// Method string
// }
//
// endpoints := map[string]Endpoint{
// "get_users": {Path: "/api/users", Method: "GET"},
// "create_user": {Path: "/api/users", Method: "POST"},
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
// }
//
// validateEndpoint := func(e Endpoint) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(e.Path),
// assert.That(func(path string) bool {
// return strings.HasPrefix(path, "/api/")
// })(e.Path),
// assert.That(func(method string) bool {
// return method == "GET" || method == "POST" ||
// method == "PUT" || method == "DELETE"
// })(e.Method),
// })
// }
//
// traverse := assert.TraverseRecord(validateEndpoint)
// traverse(endpoints)(t)
// }
//
// # Comparison with TraverseArray
//
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
//
// - TraverseRecord: Works with maps (records)
//
// - Input: Map with string keys + transformation function
//
// - Use when: You have named test data in a map
//
// - Test names: Derived from map keys
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + function that generates names and assertions
//
// - Use when: You have sequential test data
//
// - Test names: Generated by the transformation function
//
// # Comparison with SequenceRecord
//
// TraverseRecord and [SequenceRecord] are closely related:
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader
//
// - Use when: Assertions are already defined
//
// # Related Functions
//
// - [SequenceRecord]: Similar but takes pre-defined assertions
// - [TraverseArray]: Similar but works with arrays
// - [RunAll]: Alias for SequenceRecord
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
return func(m map[string]T) Reader {
return func(t *testing.T) bool {
ok := true
for name, src := range m {
res := t.Run(name, func(t *testing.T) {
f(src)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceRecord executes a map of named test cases as subtests.
//
// This function takes a map where keys are test names and values are test assertions
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
// convenience.
//
// The function iterates through all map entries, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - m: A map[string]Reader where:
// - Keys: Test names (strings) for the subtests
// - Values: Test assertions ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each map entry as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Executing a collection of pre-defined named test cases
// - Organizing related tests in a map structure
// - Running multiple assertions with descriptive names
// - Building test suites programmatically
//
// # Example - Basic Named Tests
//
// func TestMathOperations(t *testing.T) {
// tests := map[string]assert.Reader{
// "addition": assert.Equal(4)(2 + 2),
// "subtraction": assert.Equal(1)(3 - 2),
// "multiplication": assert.Equal(6)(2 * 3),
// "division": assert.Equal(2)(6 / 3),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - String Validation Suite
//
// func TestStringValidations(t *testing.T) {
// testString := "hello world"
//
// tests := map[string]assert.Reader{
// "not_empty": assert.StringNotEmpty(testString),
// "correct_length": assert.StringLength[any, any](11)(testString),
// "has_space": assert.That(func(s string) bool {
// return strings.Contains(s, " ")
// })(testString),
// "lowercase": assert.That(func(s string) bool {
// return s == strings.ToLower(s)
// })(testString),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Complex Object Validation
//
// func TestUserValidation(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
//
// tests := map[string]assert.Reader{
// "name_not_empty": assert.StringNotEmpty(user.Name),
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
// "email_valid": assert.That(func(email string) bool {
// return strings.Contains(email, "@") && strings.Contains(email, ".")
// })(user.Email),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Array Validation Suite
//
// func TestArrayValidations(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
//
// tests := map[string]assert.Reader{
// "not_empty": assert.ArrayNotEmpty(numbers),
// "correct_length": assert.ArrayLength[int](5)(numbers),
// "contains_three": assert.ArrayContains(3)(numbers),
// "all_positive": assert.That(func(arr []int) bool {
// for _, n := range arr {
// if n <= 0 {
// return false
// }
// }
// return true
// })(numbers),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Comparison with TraverseRecord
//
// SequenceRecord and [TraverseRecord] are closely related:
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader (assertions already created)
//
// - Use when: You have already defined test cases with assertions
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// # Comparison with SequenceSeq2
//
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
//
// - SequenceRecord: Works with maps
//
// - Input: map[string]Reader
//
// - Use when: You have named test cases in a map
//
// - Iteration order: Non-deterministic (map iteration)
//
// - SequenceSeq2: Works with iterators
//
// - Input: Seq2[string, Reader]
//
// - Use when: You have test cases in an iterator
//
// - Iteration order: Deterministic (iterator order)
//
// # Note on Map Iteration Order
//
// Go maps have non-deterministic iteration order. If test execution order matters,
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
// or use [TraverseArray] with a slice of test cases.
//
// # Related Functions
//
// - [RunAll]: Alias for SequenceRecord
// - [TraverseRecord]: Similar but transforms values into assertions
// - [SequenceSeq2]: Similar but works with iterators
// - [TraverseArray]: Similar but works with arrays
//
// # References
//
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceRecord(m map[string]Reader) Reader {
return TraverseRecord(reader.Ask[Reader]())(m)
}

960
v2/assert/traverse_test.go Normal file
View File

@@ -0,0 +1,960 @@
// 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
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/pair"
)
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
func TestTraverseArray_EmptyArray(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n)(n),
)
})
result := traverse([]int{})(t)
if !result {
t.Error("Expected TraverseArray to pass with empty array")
}
}
// TestTraverseArray_SingleElement tests TraverseArray with a single element
func TestTraverseArray_SingleElement(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n*2)(n*2),
)
})
result := traverse([]int{5})(t)
if !result {
t.Error("Expected TraverseArray to pass with single element")
}
}
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
func TestTraverseArray_MultipleElements(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square_%d", n),
Equal(n*n)(n*n),
)
})
result := traverse([]int{1, 2, 3, 4, 5})(t)
if !result {
t.Error("Expected TraverseArray to pass with all passing elements")
}
}
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
func TestTraverseArray_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(10)(n), // Will fail for all except 10
)
})
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
result := traverse([]int{1, 2, 3})(t)
// The traverse should return false because assertions fail
if result {
t.Error("Expected traverse to return false when elements don't match")
}
}
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
func TestTraverseArray_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("is_even_%d", n),
Equal(0)(n%2), // Only passes for even numbers
)
})
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
// The traverse should return false because one assertion fails
if result {
t.Error("Expected traverse to return false when some elements fail")
}
}
// TestTraverseArray_StringData tests TraverseArray with string data
func TestTraverseArray_StringData(t *testing.T) {
words := []string{"hello", "world", "test"}
traverse := TraverseArray(func(s string) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("validate_%s", s),
AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
}),
)
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid strings")
}
}
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
func TestTraverseArray_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
}),
)
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid users")
}
}
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "", Age: 25}, // Invalid: empty name
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
}),
)
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testCases := []TestCase{
{Input: 2, Expected: 4},
{Input: 3, Expected: 9},
{Input: 4, Expected: 16},
{Input: 5, Expected: 25},
}
square := func(n int) int { return n * n }
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
Equal(tc.Expected)(square(tc.Input)),
)
})
result := traverse(testCases)(t)
if !result {
t.Error("Expected all test cases to pass")
}
}
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
func TestSequenceSeq2_EmptySequence(t *testing.T) {
emptySeq := func(yield func(string, Reader) bool) {
// Empty - yields nothing
}
result := SequenceSeq2[Reader](emptySeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with empty sequence")
}
}
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
func TestSequenceSeq2_SingleTest(t *testing.T) {
singleSeq := func(yield func(string, Reader) bool) {
yield("test_one", Equal(42)(42))
}
result := SequenceSeq2[Reader](singleSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with single test")
}
}
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
func TestSequenceSeq2_MultipleTests(t *testing.T) {
multiSeq := func(yield func(string, Reader) bool) {
if !yield("test_addition", Equal(4)(2+2)) {
return
}
if !yield("test_subtraction", Equal(1)(3-2)) {
return
}
if !yield("test_multiplication", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](multiSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with all passing tests")
}
}
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
func TestSequenceSeq2_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
failSeq := func(yield func(string, Reader) bool) {
if !yield("test_pass", Equal(4)(2+2)) {
return
}
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
return
}
if !yield("test_pass2", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](failSeq)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
generateTests := func(yield func(string, Reader) bool) {
for i := 1; i <= 5; i++ {
name := fmt.Sprintf("test_%d", i)
assertion := Equal(i * i)(i * i)
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](generateTests)(t)
if !result {
t.Error("Expected all generated tests to pass")
}
}
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
func TestSequenceSeq2_StringTests(t *testing.T) {
stringSeq := func(yield func(string, Reader) bool) {
if !yield("test_hello", StringNotEmpty("hello")) {
return
}
if !yield("test_world", StringNotEmpty("world")) {
return
}
if !yield("test_length", StringLength[any, any](5)("hello")) {
return
}
}
result := SequenceSeq2[Reader](stringSeq)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
func TestSequenceSeq2_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
arraySeq := func(yield func(string, Reader) bool) {
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
return
}
if !yield("test_length", ArrayLength[int](5)(arr)) {
return
}
if !yield("test_contains", ArrayContains(3)(arr)) {
return
}
}
result := SequenceSeq2[Reader](arraySeq)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
userSeq := func(yield func(string, Reader) bool) {
if !yield("test_name", StringNotEmpty(user.Name)) {
return
}
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
return
}
if !yield("test_email", That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(user.Email)) {
return
}
}
result := SequenceSeq2[Reader](userSeq)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
executionCount := 0
earlyTermSeq := func(yield func(string, Reader) bool) {
executionCount++
if !yield("test_1", Equal(1)(1)) {
return
}
executionCount++
if !yield("test_2", Equal(2)(2)) {
return
}
executionCount++
// This should execute even though we don't check the return
yield("test_3", Equal(3)(3))
executionCount++
}
SequenceSeq2[Reader](earlyTermSeq)(t)
// All iterations should execute since we're not terminating early
if executionCount != 4 {
t.Errorf("Expected 4 executions, got %d", executionCount)
}
}
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
testMap := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_multiplication": Equal(6)(2 * 3),
"test_subtraction": Equal(1)(3 - 2),
}
// Convert map to Seq2
mapSeq := func(yield func(string, Reader) bool) {
for name, assertion := range testMap {
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](mapSeq)(t)
if !result {
t.Error("Expected all map-based tests to pass")
}
}
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
type TestCase struct {
Name string
Input int
Expected int
}
testCases := []TestCase{
{Name: "test_1", Input: 2, Expected: 4},
{Name: "test_2", Input: 3, Expected: 9},
{Name: "test_3", Input: 4, Expected: 16},
}
// Using TraverseArray
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
})(testCases)(t)
// Using SequenceSeq2
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
for _, tc := range testCases {
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
return
}
}
})(t)
if traverseResult != seqResult {
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
func TestTraverseRecord_EmptyMap(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n)(n)
})
result := traverse(map[string]int{})(t)
if !result {
t.Error("Expected TraverseRecord to pass with empty map")
}
}
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
func TestTraverseRecord_SingleEntry(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * 2)(n * 2)
})
result := traverse(map[string]int{"test_5": 5})(t)
if !result {
t.Error("Expected TraverseRecord to pass with single entry")
}
}
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
func TestTraverseRecord_MultipleEntries(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * n)(n * n)
})
result := traverse(map[string]int{
"square_1": 1,
"square_2": 2,
"square_3": 3,
"square_4": 4,
"square_5": 5,
})(t)
if !result {
t.Error("Expected TraverseRecord to pass with all passing entries")
}
}
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
func TestTraverseRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(10)(n) // Will fail for all except 10
})
result := traverse(map[string]int{
"test_1": 1,
"test_2": 2,
"test_3": 3,
})(t)
// The traverse should return false because entries don't match
if result {
t.Error("Expected traverse to return false when entries don't match")
}
}
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
func TestTraverseRecord_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(0)(n % 2) // Only passes for even numbers
})
result := traverse(map[string]int{
"even_2": 2,
"odd_3": 3,
"even_4": 4,
})(t)
// The traverse should return false because some entries fail
if result {
t.Error("Expected traverse to return false when some entries fail")
}
}
// TestTraverseRecord_StringData tests TraverseRecord with string data
func TestTraverseRecord_StringData(t *testing.T) {
words := map[string]string{
"greeting": "hello",
"world": "world",
"test": "test",
}
traverse := TraverseRecord(func(s string) Reader {
return AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
})
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid strings")
}
}
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
func TestTraverseRecord_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"bob": {Name: "Bob", Age: 25},
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
})
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid users")
}
}
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"invalid": {Name: "", Age: 25}, // Invalid: empty name
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
})
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
configs := map[string]int{
"timeout": 30,
"maxRetries": 3,
"bufferSize": 1024,
}
validatePositive := That(func(n int) bool { return n > 0 })
traverse := TraverseRecord(validatePositive)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configuration values to be positive")
}
}
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
type Endpoint struct {
Path string
Method string
}
endpoints := map[string]Endpoint{
"get_users": {Path: "/api/users", Method: "GET"},
"create_user": {Path: "/api/users", Method: "POST"},
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
}
validateEndpoint := func(e Endpoint) Reader {
return AllOf([]Reader{
StringNotEmpty(e.Path),
That(func(path string) bool {
return len(path) > 0 && path[0] == '/'
})(e.Path),
That(func(method string) bool {
return method == "GET" || method == "POST" ||
method == "PUT" || method == "DELETE"
})(e.Method),
})
}
traverse := TraverseRecord(validateEndpoint)
result := traverse(endpoints)(t)
if !result {
t.Error("Expected all endpoints to be valid")
}
}
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
func TestSequenceRecord_EmptyMap(t *testing.T) {
result := SequenceRecord(map[string]Reader{})(t)
if !result {
t.Error("Expected SequenceRecord to pass with empty map")
}
}
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
func TestSequenceRecord_SingleTest(t *testing.T) {
tests := map[string]Reader{
"test_one": Equal(42)(42),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with single test")
}
}
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
func TestSequenceRecord_MultipleTests(t *testing.T) {
tests := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_subtraction": Equal(1)(3 - 2),
"test_multiplication": Equal(6)(2 * 3),
"test_division": Equal(2)(6 / 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with all passing tests")
}
}
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
func TestSequenceRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
tests := map[string]Reader{
"test_pass": Equal(4)(2 + 2),
"test_fail": Equal(5)(2 + 2), // This will fail
"test_pass2": Equal(6)(2 * 3),
}
result := SequenceRecord(tests)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
func TestSequenceRecord_StringTests(t *testing.T) {
testString := "hello world"
tests := map[string]Reader{
"not_empty": StringNotEmpty(testString),
"correct_length": StringLength[any, any](11)(testString),
"has_space": That(func(s string) bool {
for _, ch := range s {
if ch == ' ' {
return true
}
}
return false
})(testString),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
func TestSequenceRecord_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"not_empty": ArrayNotEmpty(arr),
"correct_length": ArrayLength[int](5)(arr),
"contains_three": ArrayContains(3)(arr),
"all_positive": That(func(arr []int) bool {
for _, n := range arr {
if n <= 0 {
return false
}
}
return true
})(arr),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
tests := map[string]Reader{
"name_not_empty": StringNotEmpty(user.Name),
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
"email_valid": That(func(email string) bool {
hasAt := false
hasDot := false
for _, ch := range email {
if ch == '@' {
hasAt = true
}
if ch == '.' {
hasDot = true
}
}
return hasAt && hasDot
})(user.Email),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
func TestSequenceRecord_MathOperations(t *testing.T) {
tests := map[string]Reader{
"addition": Equal(4)(2 + 2),
"subtraction": Equal(1)(3 - 2),
"multiplication": Equal(6)(2 * 3),
"division": Equal(2)(6 / 3),
"modulo": Equal(1)(7 % 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all math operations to pass")
}
}
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
func TestSequenceRecord_BooleanTests(t *testing.T) {
tests := map[string]Reader{
"true_is_true": Equal(true)(true),
"false_is_false": Equal(false)(false),
"not_true": Equal(false)(!true),
"not_false": Equal(true)(!false),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all boolean tests to pass")
}
}
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
func TestSequenceRecord_ErrorTests(t *testing.T) {
tests := map[string]Reader{
"no_error": NoError(nil),
"equal_value": Equal("test")("test"),
"not_empty": StringNotEmpty("hello"),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all error tests to pass")
}
}
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testData := map[string]TestCase{
"test_1": {Input: 2, Expected: 4},
"test_2": {Input: 3, Expected: 9},
"test_3": {Input: 4, Expected: 16},
}
// Using TraverseRecord
traverseResult := TraverseRecord(func(tc TestCase) Reader {
return Equal(tc.Expected)(tc.Input * tc.Input)
})(testData)(t)
// Using SequenceRecord (manually creating the map)
tests := make(map[string]Reader)
for name, tc := range testData {
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
}
seqResult := SequenceRecord(tests)(t)
if traverseResult != seqResult {
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
func TestSequenceRecord_WithAllOf(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"array_validations": AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
}),
"element_checks": AllOf([]Reader{
That(func(a []int) bool { return a[0] == 1 })(arr),
That(func(a []int) bool { return a[4] == 5 })(arr),
}),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected combined assertions to pass")
}
}
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
func TestTraverseRecord_ConfigValidation(t *testing.T) {
type Config struct {
Value int
Min int
Max int
}
configs := map[string]Config{
"timeout": {Value: 30, Min: 1, Max: 60},
"maxRetries": {Value: 3, Min: 1, Max: 10},
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
}
validateConfig := func(c Config) Reader {
return AllOf([]Reader{
That(func(val int) bool { return val >= c.Min })(c.Value),
That(func(val int) bool { return val <= c.Max })(c.Value),
})
}
traverse := TraverseRecord(validateConfig)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configurations to be within valid ranges")
}
}
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
func TestSequenceRecord_RealWorldExample(t *testing.T) {
type Response struct {
StatusCode int
Body string
}
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
tests := map[string]Reader{
"status_ok": Equal(200)(response.StatusCode),
"body_not_empty": StringNotEmpty(response.Body),
"body_is_json": That(func(s string) bool {
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
})(response.Body),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected response validation to pass")
}
}

View File

@@ -1,11 +1,32 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package assert
import (
"iter"
"testing"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -13,23 +34,506 @@ import (
type (
// Result represents a computation that may fail with an error.
//
// This is an alias for [result.Result][T], which encapsulates either a successful
// value of type T or an error. It's commonly used in test assertions to represent
// operations that might fail, allowing for functional error handling without exceptions.
//
// A Result can be in one of two states:
// - Success: Contains a value of type T
// - Failure: Contains an error
//
// This type is particularly useful in testing scenarios where you need to:
// - Test functions that return results
// - Chain operations that might fail
// - Handle errors functionally
//
// Example:
//
// func TestResultHandling(t *testing.T) {
// successResult := result.Of[int](42)
// assert.Success(successResult)(t) // Passes
//
// failureResult := result.Error[int](errors.New("failed"))
// assert.Failure(failureResult)(t) // Passes
// }
//
// See also:
// - [Success]: Asserts a Result is successful
// - [Failure]: Asserts a Result contains an error
// - [result.Result]: The underlying Result type
Result[T any] = result.Result[T]
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
//
// This is the core type for all assertions in this package. It's an alias for
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
// and produces a boolean result indicating whether the assertion passed.
//
// The Reader pattern enables:
// - Composable assertions that can be combined using functional operators
// - Deferred execution - assertions are defined but not executed until applied to a test
// - Reusable assertion logic that can be applied to multiple tests
// - Functional composition of complex test conditions
//
// All assertion functions in this package return a Reader, which must be applied
// to a *testing.T to execute the assertion:
//
// assertion := assert.Equal(42)(result) // Creates a Reader
// assertion(t) // Executes the assertion
//
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
// functional operators from the reader package.
//
// Example:
//
// func TestReaderComposition(t *testing.T) {
// // Create individual assertions
// assertion1 := assert.Equal(42)(42)
// assertion2 := assert.StringNotEmpty("hello")
//
// // Combine them
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
//
// // Execute the combined assertion
// combined(t)
// }
//
// See also:
// - [Kleisli]: Function that produces a Reader from a value
// - [AllOf]: Combines multiple Readers
// - [ApplicativeMonoid]: Monoid for combining Readers
Reader = reader.Reader[*testing.T, bool]
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
//
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
// "data last" principle used throughout this package.
//
// Kleisli functions enable:
// - Partial application of assertions - configure the expected value first, apply actual value later
// - Reusable assertion builders that can be applied to different values
// - Functional composition of assertion pipelines
// - Point-free style programming with assertions
//
// Most assertion functions in this package return a Kleisli, which must be applied
// to the actual value being tested, and then to a *testing.T:
//
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
// reader := kleisli(result) // Reader - assertion ready to execute
// reader(t) // Execute the assertion
//
// Or more concisely:
//
// assert.Equal(42)(result)(t)
//
// Example:
//
// func TestKleisliPattern(t *testing.T) {
// // Create a reusable assertion for positive numbers
// isPositive := assert.That(func(n int) bool { return n > 0 })
//
// // Apply it to different values
// isPositive(42)(t) // Passes
// isPositive(100)(t) // Passes
// // isPositive(-5)(t) would fail
//
// // Can be used with Local for property testing
// type User struct { Age int }
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
// checkAge(User{Age: 25})(t) // Passes
// }
//
// See also:
// - [Reader]: The assertion type produced by Kleisli
// - [Local]: Focuses a Kleisli on a property of a larger structure
Kleisli[T any] = reader.Reader[T, Reader]
// Predicate represents a function that tests a value of type T and returns a boolean.
//
// This is an alias for [predicate.Predicate][T], which is a simple function that
// takes a value and returns true or false based on some condition. Predicates are
// used with the [That] function to create custom assertions.
//
// Predicates enable:
// - Custom validation logic for any type
// - Reusable test conditions
// - Composition of complex validation rules
// - Integration with functional programming patterns
//
// Example:
//
// func TestPredicates(t *testing.T) {
// // Simple predicate
// isEven := func(n int) bool { return n%2 == 0 }
// assert.That(isEven)(42)(t) // Passes
//
// // String predicate
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
// assert.That(hasPrefix)("test_file.go")(t) // Passes
//
// // Complex predicate
// isValidEmail := func(s string) bool {
// return strings.Contains(s, "@") && strings.Contains(s, ".")
// }
// assert.That(isValidEmail)("user@example.com")(t) // Passes
// }
//
// See also:
// - [That]: Creates an assertion from a Predicate
// - [predicate.Predicate]: The underlying predicate type
Predicate[T any] = predicate.Predicate[T]
// Lens is a functional reference to a subpart of a data structure.
//
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
// on a specific field within a larger structure. Lenses enable getting and setting
// values in nested data structures in a functional, immutable way.
//
// In the context of testing, lenses are used with [LocalL] to focus assertions
// on specific properties of complex objects without manually extracting those properties.
//
// A Lens[S, T] focuses on a value of type T within a structure of type S.
//
// Example:
//
// func TestLensUsage(t *testing.T) {
// type Address struct { City string }
// type User struct { Name string; Address Address }
//
// // Define lenses (typically generated)
// addressLens := lens.Lens[User, Address]{...}
// cityLens := lens.Lens[Address, string]{...}
//
// // Compose lenses to focus on nested field
// userCityLens := lens.Compose(addressLens, cityLens)
//
// // Use with LocalL to assert on nested property
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
// }
//
// See also:
// - [LocalL]: Uses a Lens to focus assertions on a property
// - [lens.Lens]: The underlying lens type
// - [Optional]: Similar but for values that may not exist
Lens[S, T any] = lens.Lens[S, T]
// Optional is an optic that focuses on a value that may or may not be present.
//
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
// handles cases where the focused value might not exist. Optionals are useful for
// working with nullable fields, optional properties, or values that might be absent.
//
// In testing, Optionals are used with [FromOptional] to create assertions that
// verify whether an optional value is present and, if so, whether it satisfies
// certain conditions.
//
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
//
// Example:
//
// func TestOptionalUsage(t *testing.T) {
// type Config struct { Timeout *int }
//
// // Define optional (typically generated)
// timeoutOptional := optional.Optional[Config, int]{...}
//
// // Test when value is present
// config1 := Config{Timeout: ptr(30)}
// assert.FromOptional(timeoutOptional)(
// assert.Equal(30),
// )(config1)(t) // Passes
//
// // Test when value is absent
// config2 := Config{Timeout: nil}
// // FromOptional would fail because value is not present
// }
//
// See also:
// - [FromOptional]: Creates assertions for optional values
// - [optional.Optional]: The underlying optional type
// - [Lens]: Similar but for values that always exist
Optional[S, T any] = optional.Optional[S, T]
// Prism is an optic that focuses on a case of a sum type.
//
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
// matching and extraction of values from sum types in a functional way.
//
// In testing, Prisms are used with [FromPrism] to create assertions that verify
// whether a value matches a specific case and, if so, whether the contained value
// satisfies certain conditions.
//
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
//
// Example:
//
// func TestPrismUsage(t *testing.T) {
// // Prism for extracting success value from Result
// successPrism := prism.Success[int]()
//
// // Test successful result
// successResult := result.Of[int](42)
// assert.FromPrism(successPrism)(
// assert.Equal(42),
// )(successResult)(t) // Passes
//
// // Prism for extracting error from Result
// failurePrism := prism.Failure[int]()
//
// // Test failed result
// failureResult := result.Error[int](errors.New("failed"))
// assert.FromPrism(failurePrism)(
// assert.Error,
// )(failureResult)(t) // Passes
// }
//
// See also:
// - [FromPrism]: Creates assertions for prism-focused values
// - [prism.Prism]: The underlying prism type
// - [Optional]: Similar but for optional values
Prism[S, T any] = prism.Prism[S, T]
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
//
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
// computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like file I/O, network calls)
// - Result: May fail with an error
//
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
// context-aware, effectful computations into test assertions. This is useful
// when your test assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations (database queries, API calls, file access)
// - Handle potential errors gracefully
//
// Example:
//
// func TestReaderIOResult(t *testing.T) {
// // Create a ReaderIOResult that performs IO and may fail
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Perform database check with context
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
//
// See also:
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
// - [ReaderIO]: Similar but without error handling
// - [readerioresult.ReaderIOResult]: The underlying type
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// ReaderIO represents a context-aware, IO-based computation.
//
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like logging, metrics)
//
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
// effectful computations into test assertions. This is useful when your test
// assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware utilities
//
// Example:
//
// func TestReaderIO(t *testing.T) {
// // Create a ReaderIO that performs IO
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log with context
// logger.InfoContext(ctx, "Running test")
// // Return assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
//
// See also:
// - [FromReaderIO]: Converts ReaderIO to Reader
// - [ReaderIOResult]: Similar but with error handling
// - [readerio.ReaderIO]: The underlying type
ReaderIO[A any] = readerio.ReaderIO[A]
// Seq2 represents a Go iterator that yields key-value pairs.
//
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
// iterated over using a for-range loop.
//
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
// test cases provided as an iterator. This enables:
// - Lazy evaluation of test cases
// - Memory-efficient testing of large test suites
// - Integration with Go's iterator patterns
// - Dynamic generation of test cases
//
// Example:
//
// func TestSeq2Usage(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// // Execute all test cases
// assert.SequenceSeq2[assert.Reader](testCases)(t)
// }
//
// See also:
// - [SequenceSeq2]: Executes a Seq2 of test cases
// - [TraverseArray]: Similar but for arrays
// - [iter.Seq2]: The underlying iterator type
Seq2[K, A any] = iter.Seq2[K, A]
// Pair represents a tuple of two values with potentially different types.
//
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
// related values together without defining a custom struct.
//
// In testing, Pairs are used with [TraverseArray] to associate test names with
// their corresponding assertions. Each element in the array is transformed into
// a Pair[string, Reader] where the string is the test name and the Reader is
// the assertion to execute.
//
// Example:
//
// func TestPairUsage(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// }
//
// // Transform each test case into a named assertion
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// See also:
// - [TraverseArray]: Uses Pairs to create named test cases
// - [pair.Pair]: The underlying pair type
// - [pair.MakePair]: Creates a Pair
// - [pair.Head]: Extracts the first value
// - [pair.Tail]: Extracts the second value
Pair[L, R any] = pair.Pair[L, R]
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
//
// This is an alias for [function.Void], which is used to represent operations that don't
// return a meaningful value but may perform side effects. In the context of testing, Void
// is used with IO operations that perform actions without producing a result.
//
// Void is conceptually similar to:
// - Unit type in functional languages (Haskell's (), Scala's Unit)
// - void in languages like C/Java (but as a value, not just a type)
// - Empty struct{} in Go (but with clearer semantic meaning)
//
// Example:
//
// func TestWithSideEffect(t *testing.T) {
// // An IO operation that logs but returns Void
// logOperation := func() function.Void {
// log.Println("Test executed")
// return function.Void{}
// }
//
// // Execute the operation
// logOperation()
// }
//
// See also:
// - [IO]: Wraps side-effecting operations
// - [function.Void]: The underlying void type
Void = function.Void
// IO represents a side-effecting computation that produces a value of type A.
//
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
// computation - it describes an effect but doesn't execute it until explicitly run.
//
// In testing, IO is used to:
// - Defer execution of side effects until needed
// - Compose multiple side-effecting operations
// - Maintain referential transparency in test setup
// - Separate effect description from effect execution
//
// An IO[A] is essentially a function `func() A` that:
// - Encapsulates a side effect
// - Returns a value of type A when executed
// - Can be composed with other IO operations
//
// Example:
//
// func TestIOOperation(t *testing.T) {
// // Define an IO operation that reads a file
// readConfig := func() io.IO[string] {
// return func() string {
// data, _ := os.ReadFile("config.txt")
// return string(data)
// }
// }
//
// // The IO is not executed yet - it's just a description
// configIO := readConfig()
//
// // Execute the IO to get the result
// config := configIO()
// assert.StringNotEmpty(config)(t)
// }
//
// Example with composition:
//
// func TestIOComposition(t *testing.T) {
// // Chain multiple IO operations
// pipeline := io.Map(
// func(s string) int { return len(s) },
// )(readFileIO)
//
// // Execute the composed operation
// length := pipeline()
// assert.That(func(n int) bool { return n > 0 })(length)(t)
// }
//
// See also:
// - [ReaderIO]: Combines Reader and IO effects
// - [ReaderIOResult]: Adds error handling to ReaderIO
// - [io.IO]: The underlying IO type
// - [Void]: Represents operations without meaningful return values
IO[A any] = io.IO[A]
)

View File

@@ -87,7 +87,9 @@ type templateData struct {
}
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
// {{.Name}}Lenses provides [lenses] for accessing fields of [{{.Name}}]
//
// [lenses]: __lens.Lens
type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
@@ -101,7 +103,10 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
{{- 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}}]
//
//
// [lenses]: __lens.Lens
type {{.Name}}RefLenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
@@ -112,23 +117,32 @@ type {{.Name}}RefLenses{{.TypeParams}} struct {
{{- if .IsComparable}}
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
// prisms
{{- range .Fields}}
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
// {{.Name}}Prisms provides [prisms] for accessing fields of [{{.Name}}]
//
// [prisms]: __prism.Prism
type {{.Name}}Prisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}RefPrisms provides [prisms] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
//
// [prisms]: __prism.Prism
type {{.Name}}RefPrisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
`
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
//
// [lenses]:__lens.Lens
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
@@ -158,7 +172,9 @@ func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
}
}
// 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
//
// [lenses]:__lens.Lens
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
@@ -196,7 +212,9 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
}
}
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
// Make{{.Name}}Prisms creates a new [{{.Name}}Prisms] with [prisms] for all fields
//
// [prisms]:__prism.Prism
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
{{- range .Fields}}
{{- if .IsComparable}}
@@ -236,6 +254,49 @@ func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
{{- end}}
}
}
// Make{{.Name}}RefPrisms creates a new [{{.Name}}RefPrisms] with [prisms] for all fields
//
// [prisms]:__prism.Prism
func Make{{.Name}}RefPrisms{{.TypeParams}}() {{.Name}}RefPrisms{{.TypeParamNames}} {
{{- range .Fields}}
{{- if .IsComparable}}
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
_prism{{.Name}} := __prism.MakePrismWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return &result
{{- else}}
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- else}}
_prism{{.Name}} := __prism.MakePrismWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return &result
{{- else}}
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- end}}
{{- end}}
return {{.Name}}RefPrisms{{.TypeParamNames}} {
{{- range .Fields}}
{{.Name}}: _prism{{.Name}},
{{- end}}
}
}
`
var (
@@ -536,9 +597,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
fieldTypeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := fieldTypeName
@@ -698,9 +759,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
typeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := typeName
isComparable := false

View File

@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
}
func TestParseFileWithUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Config struct {
PublicName string
privateName string
PublicValue int
privateValue *int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
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 Config struct
config := structs[0]
assert.Equal(t, "Config", config.Name)
assert.Len(t, config.Fields, 4, "Should include both exported and unexported fields")
// Check exported field
assert.Equal(t, "PublicName", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Check unexported field
assert.Equal(t, "privateName", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.False(t, config.Fields[1].IsOptional)
// Check exported int field
assert.Equal(t, "PublicValue", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.False(t, config.Fields[2].IsOptional)
// Check unexported pointer field
assert.Equal(t, "privateValue", config.Fields[3].Name)
assert.Equal(t, "*int", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
}
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type MixedStruct struct {
PublicField string
privateField int
OptionalPrivate *string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, 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, "MixedStructLenses")
assert.Contains(t, contentStr, "MakeMixedStructLenses")
// Check that lenses are generated for all fields (exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
// Check lens constructors
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
// Check setters
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
}
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type PrivateConfig struct {
name string
value int
enabled bool
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
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 PrivateConfig struct
config := structs[0]
assert.Equal(t, "PrivateConfig", config.Name)
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
// Check all fields are unexported
assert.Equal(t, "name", config.Fields[0].Name)
assert.Equal(t, "value", config.Fields[1].Name)
assert.Equal(t, "enabled", config.Fields[2].Name)
}
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
type BaseConfig struct {
publicBase string
privateBase int
}
// fp-go:Lens
type ExtendedConfig struct {
BaseConfig
PublicField string
privateField bool
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, 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, "ExtendedConfigLenses")
// Check that lenses are generated for embedded unexported fields
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
// Check that lenses are generated for direct fields (both exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
}
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
// Create a temporary test file with various field visibility patterns
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type ComplexStruct struct {
// Exported fields
Name string
Age int
Email *string
// Unexported fields
password string
secretKey []byte
internalID *int
// Mixed with tags
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
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 ComplexStruct
complex := structs[0]
assert.Equal(t, "ComplexStruct", complex.Name)
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
// Verify field names and types
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
for i, expectedName := range fieldNames {
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
}
// Check optional fields
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
}

View File

@@ -46,7 +46,7 @@ func TestBuilderWithQuery(t *testing.T) {
RIOE.Map(func(r *http.Request) *url.URL {
return r.URL
}),
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[any] {
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[Void] {
return IO.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))

View File

@@ -0,0 +1,7 @@
package builder
import "github.com/IBM/fp-go/v2/function"
type (
Void = function.Void
)

View File

@@ -158,7 +158,7 @@ func MakeClient(httpClient *http.Client) Client {
// request := MakeGetRequest("https://api.example.com/data")
// fullResp := ReadFullResponse(client)(request)
// result := fullResp(t.Context())()
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
func ReadFullResponse(client Client) RIOE.Operator[*http.Request, H.FullResponse] {
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
return F.Flow3(
client.Do(req),
@@ -195,7 +195,7 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
// request := MakeGetRequest("https://api.example.com/data")
// readBytes := ReadAll(client)
// result := readBytes(request)(t.Context())()
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
func ReadAll(client Client) RIOE.Operator[*http.Request, []byte] {
return F.Flow2(
ReadFullResponse(client),
RIOE.Map(H.Body),
@@ -219,7 +219,7 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
// request := MakeGetRequest("https://api.example.com/text")
// readText := ReadText(client)
// result := readText(request)(t.Context())()
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
func ReadText(client Client) RIOE.Operator[*http.Request, string] {
return F.Flow2(
ReadAll(client),
RIOE.Map(B.ToString),
@@ -231,7 +231,7 @@ func ReadText(client Client) RIOE.Kleisli[Requester, string] {
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
// but will be removed in a future version. The capitalized version follows Go naming
// conventions for acronyms.
func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
func ReadJson[A any](client Client) RIOE.Operator[*http.Request, A] {
return ReadJSON[A](client)
}
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
// 3. Reads the response body as bytes
//
// This function is used internally by ReadJSON to ensure proper JSON response handling.
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
func readJSON(client Client) RIOE.Operator[*http.Request, []byte] {
return F.Flow3(
ReadFullResponse(client),
RIOE.ChainFirstEitherK(F.Flow2(
@@ -278,7 +278,7 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
// request := MakeGetRequest("https://api.example.com/user/1")
// readUser := ReadJSON[User](client)
// result := readUser(request)(t.Context())()
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
func ReadJSON[A any](client Client) RIOE.Operator[*http.Request, A] {
return F.Flow2(
readJSON(client),
RIOE.ChainEitherK(J.Unmarshal[A]),

View File

@@ -65,7 +65,7 @@ var (
// This function assumes the context contains logging information; it will panic if not present.
getLoggingContext = F.Flow3(
loggingContextValue,
option.ToType[loggingContext],
option.InstanceOf[loggingContext],
option.GetOrElse(getDefaultLoggingContext),
)
)

View File

@@ -222,7 +222,7 @@ func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOResult[A])
return function.Pipe3(
ma,
ioresult.Swap[A],
ioeither.ChainFirstIOK[A](func(err error) func() any {
ioeither.ChainFirstIOK[A](func(err error) func() Void {
return io.FromImpure(func() { cancel(err) })
}),
ioeither.Swap[A],
@@ -452,7 +452,7 @@ func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
// Returns a function that chains Option-returning functions into ReaderIOResult.
//
//go:inline
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainOptionK[context.Context, A, B](onNone)
}
@@ -800,7 +800,7 @@ func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
}
//go:inline
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
func FromReaderOption[A any](onNone Lazy[error]) Kleisli[ReaderOption[context.Context, A], A] {
return RIOR.FromReaderOption[context.Context, A](onNone)
}
@@ -895,17 +895,17 @@ func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
func ChainReaderOptionK[A, B any](onNone Lazy[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] {
func ChainFirstReaderOptionK[A, B any](onNone Lazy[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] {
func TapReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
}

View File

@@ -48,7 +48,7 @@ func WithLock[A any](lock ReaderIOResult[context.CancelFunc]) Operator[A, A] {
function.Constant1[context.CancelFunc, ReaderIOResult[A]],
WithResource[A](lock, function.Flow2(
io.FromImpure[context.CancelFunc],
FromIO[any],
FromIO[Void],
)),
)
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioref"
@@ -152,4 +153,6 @@ type (
IORef[A any] = ioref.IORef[A]
State[S, A any] = state.State[S, A]
Void = function.Void
)

View File

@@ -0,0 +1,168 @@
package readerreaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
"github.com/IBM/fp-go/v2/result"
)
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.Local[context.Context, error, A](f)
}
// LocalIOK transforms the outer environment of a ReaderReaderIOResult using an IO-based Kleisli arrow.
// It allows you to modify the outer environment through an effectful computation before
// passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources,
// but these effects cannot fail (or failures are not relevant).
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOK[context.Context, error, A](f)
}
// LocalIOEitherK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
// It allows you to modify the outer environment through an effectful computation that can fail before
// passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation itself requires IO effects that can fail,
// such as reading from a file that might not exist, making a network call that might timeout,
// or parsing data that might be invalid.
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOEitherK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOEitherK[context.Context, A](f)
}
// LocalIOResultK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
// This is a type-safe alias for LocalIOEitherK specialized for Result types (which use error as the error type).
//
// It allows you to modify the outer environment through an effectful computation that can fail before
// passing it to the ReaderReaderIOResult.
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOEitherK[context.Context, A](f)
}
//go:inline
func LocalResultK[A, R1, R2 any](f result.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalEitherK[context.Context, A](f)
}
// LocalReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
// It allows you to modify the outer environment through a computation that depends on the inner context
// and can perform IO effects that may fail.
//
// This is useful when the outer environment transformation requires access to the inner context (e.g., context.Context)
// and may perform IO operations that can fail, such as database queries, API calls, or file operations.
//
// The transformation happens in three stages:
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalReaderIOEitherK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderIOEitherK[A](f)
}
// LocalReaderIOResultK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
// This is a type-safe alias for LocalReaderIOEitherK specialized for Result types (which use error as the error type).
//
// It allows you to modify the outer environment through a computation that depends on the inner context
// and can perform IO effects that may fail.
//
// The transformation happens in three stages:
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderIOEitherK[A](f)
}
//go:inline
func LocalReaderReaderIOEitherK[A, R1, R2 any](f Kleisli[R2, R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderReaderIOEitherK[A](f)
}

View File

@@ -0,0 +1,428 @@
// 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 readerreaderioresult
import (
"context"
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type SimpleConfig struct {
Port int
}
type DetailedConfig struct {
Host string
Port int
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
ctx := context.Background()
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOK
adapted := LocalIOK[string](loadConfig)(useConfig)
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) io.IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Processed: %d", n))
}
}
}
adapted := LocalIOK[string](loadData)(processData)
res := adapted("test")(ctx)()
assert.Equal(t, result.Of("Processed: 40"), res)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
}
adapted := LocalIOK[string](loadConfig)(failingOperation)
res := adapted("config.json")(ctx)()
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOEitherK tests LocalIOEitherK functionality
func TestLocalIOEitherK(t *testing.T) {
ctx := context.Background()
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
return result.Left[SimpleConfig](errors.New("file not found"))
}
}
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
res := adapted("missing.json")(ctx)()
// Error from loadConfig should propagate
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOResultK tests LocalIOResultK functionality
func TestLocalIOResultK(t *testing.T) {
ctx := context.Background()
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOResultK
adapted := LocalIOResultK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
// First transformation: string -> int (can fail)
parseID := func(s string) ioresult.IOResult[int] {
return func() result.Result[int] {
if s == "" {
return result.Left[int](errors.New("empty string"))
}
return result.Of(len(s) * 10)
}
}
// Second transformation: int -> SimpleConfig (can fail)
loadConfig := func(id int) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if id < 0 {
return result.Left[SimpleConfig](errors.New("invalid ID"))
}
return result.Of(SimpleConfig{Port: 8000 + id})
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose transformations
step1 := LocalIOResultK[string](loadConfig)(formatConfig)
step2 := LocalIOResultK[string](parseID)(step1)
// Success case
res := step2("test")(ctx)()
assert.Equal(t, result.Of("Port: 8040"), res)
// Failure in first transformation
resErr1 := step2("")(ctx)()
assert.True(t, result.IsLeft(resErr1))
})
}
// TestLocalReaderIOEitherK tests LocalReaderIOEitherK functionality
func TestLocalReaderIOEitherK(t *testing.T) {
ctx := context.Background()
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
// ReaderIOResult effect that loads config from a path (can fail, uses context)
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
// Could use context here for cancellation, logging, etc.
return result.Of(SimpleConfig{Port: 8080})
}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderIOEitherK
adapted := LocalReaderIOEitherK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("context propagation", func(t *testing.T) {
type ctxKey string
const key ctxKey = "test-key"
// ReaderIOResult that reads from context
loadFromContext := func(path string) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
if val := ctx.Value(key); val != nil {
return result.Of(val.(string))
}
return result.Left[string](errors.New("key not found in context"))
}
}
}
// ReaderReaderIOResult that uses the loaded value
useValue := func(val string) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of("Loaded: " + val)
}
}
}
adapted := LocalReaderIOEitherK[string](loadFromContext)(useValue)
// With context value
ctxWithValue := context.WithValue(ctx, key, "test-value")
res := adapted("ignored")(ctxWithValue)()
assert.Equal(t, result.Of("Loaded: test-value"), res)
// Without context value
resErr := adapted("ignored")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
}
// TestLocalReaderIOResultK tests LocalReaderIOResultK functionality
func TestLocalReaderIOResultK(t *testing.T) {
ctx := context.Background()
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
// ReaderIOResult effect that loads config from a path (can fail, uses context)
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderIOResultK
adapted := LocalReaderIOResultK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("real-world: load and validate config with context", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Read file with context (can fail, uses context for cancellation)
readFile := func(cf ConfigFile) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
// Check context cancellation
select {
case <-ctx.Done():
return result.Left[string](ctx.Err())
default:
}
if cf.Path == "" {
return result.Left[string](errors.New("empty path"))
}
return result.Of(`{"port":9000}`)
}
}
}
// Parse config with context (can fail)
parseConfig := func(content string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if content == "" {
return result.Left[SimpleConfig](errors.New("empty content"))
}
return result.Of(SimpleConfig{Port: 9000})
}
}
}
// Use the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
}
}
}
// Compose the pipeline
step1 := LocalReaderIOResultK[string](parseConfig)(useConfig)
step2 := LocalReaderIOResultK[string](readFile)(step1)
// Success case
res := step2(ConfigFile{Path: "app.json"})(ctx)()
assert.Equal(t, result.Of("Using port: 9000"), res)
// Failure case
resErr := step2(ConfigFile{Path: ""})(ctx)()
assert.True(t, result.IsLeft(resErr))
})
}

View File

@@ -37,6 +37,7 @@ import (
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
"github.com/IBM/fp-go/v2/result"
)
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
@@ -170,6 +171,15 @@ func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B]
)
}
//go:inline
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return fromeither.ChainEitherK(
Chain[R, A, B],
FromEither[R, B],
f,
)
}
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
// Useful for validation or side effects that may fail.
// This is the monadic version that takes the computation as the first parameter.
@@ -837,14 +847,6 @@ func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
return RRIOE.MapLeft[R, context.Context, A](f)
}
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.Local[context.Context, error, A](f)
}
// Read provides a specific outer environment value to a computation.
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
//
@@ -892,3 +894,8 @@ func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
return reader.Map[R](RIOE.Delay[A](delay))
}
//go:inline
func Defer[R, A any](fa Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
return RRIOE.Defer(fa)
}

View File

@@ -0,0 +1,9 @@
package readerreaderioresult
import (
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
)
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
return RRIOE.TraverseArray(f)
}

View File

@@ -87,8 +87,8 @@ var (
// assembleProviders constructs the provider map for item and non-item providers
assembleProviders = F.Flow3(
A.Partition(isItemProvider),
T.Map2(collectProviders, collectItemProviders),
T.Tupled2(mergeProviders.Concat),
pair.BiMap(collectProviders, collectItemProviders),
pair.Paired(mergeProviders.Concat),
)
)

View File

@@ -30,8 +30,8 @@ import (
type (
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
InjectableFactory = func(Dependency) IOResult[any]
ProviderFactory = func(InjectableFactory) IOResult[any]
InjectableFactory = ReaderIOResult[Dependency, any]
ProviderFactory = ReaderIOResult[InjectableFactory, any]
paramIndex = map[int]int
paramValue = map[int]any

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/record"
)
@@ -12,4 +13,5 @@ type (
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
ReaderIOResult[R, T any] = readerioresult.ReaderIOResult[R, T]
)

264
v2/effect/bind.go Normal file
View File

@@ -0,0 +1,264 @@
// 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 effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func Do[C, S any](
empty S,
) Effect[C, S] {
return readerreaderioresult.Of[C](empty)
}
//go:inline
func Bind[C, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.Bind(setter, f)
}
//go:inline
func Let[C, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[C, S1, S2] {
return readerreaderioresult.Let[C](setter, f)
}
//go:inline
func LetTo[C, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[C, S1, S2] {
return readerreaderioresult.LetTo[C](setter, b)
}
//go:inline
func BindTo[C, S1, T any](
setter func(T) S1,
) Operator[C, T, S1] {
return readerreaderioresult.BindTo[C](setter)
}
//go:inline
func ApS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Effect[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApS(setter, fa)
}
//go:inline
func ApSL[C, S, T any](
lens Lens[S, T],
fa Effect[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApSL(lens, fa)
}
//go:inline
func BindL[C, S, T any](
lens Lens[S, T],
f func(T) Effect[C, T],
) Operator[C, S, S] {
return readerreaderioresult.BindL(lens, f)
}
//go:inline
func LetL[C, S, T any](
lens Lens[S, T],
f func(T) T,
) Operator[C, S, S] {
return readerreaderioresult.LetL[C](lens, f)
}
//go:inline
func LetToL[C, S, T any](
lens Lens[S, T],
b T,
) Operator[C, S, S] {
return readerreaderioresult.LetToL[C](lens, b)
}
//go:inline
func BindIOEitherK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f ioeither.Kleisli[error, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOEitherK[C](setter, f)
}
//go:inline
func BindIOResultK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f ioresult.Kleisli[S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOResultK[C](setter, f)
}
//go:inline
func BindIOK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f io.Kleisli[S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOK[C](setter, f)
}
//go:inline
func BindReaderK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindReaderK(setter, f)
}
//go:inline
func BindReaderIOK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindReaderIOK(setter, f)
}
//go:inline
func BindEitherK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f either.Kleisli[error, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindEitherK[C](setter, f)
}
//go:inline
func BindIOEitherKL[C, S, T any](
lens Lens[S, T],
f ioeither.Kleisli[error, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindIOEitherKL[C](lens, f)
}
//go:inline
func BindIOKL[C, S, T any](
lens Lens[S, T],
f io.Kleisli[T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindIOKL[C](lens, f)
}
//go:inline
func BindReaderKL[C, S, T any](
lens Lens[S, T],
f reader.Kleisli[C, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindReaderKL(lens, f)
}
//go:inline
func BindReaderIOKL[C, S, T any](
lens Lens[S, T],
f readerio.Kleisli[C, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindReaderIOKL(lens, f)
}
//go:inline
func ApIOEitherS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa IOEither[error, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApIOEitherS[C](setter, fa)
}
//go:inline
func ApIOS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa IO[T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApIOS[C](setter, fa)
}
//go:inline
func ApReaderS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApReaderS(setter, fa)
}
//go:inline
func ApReaderIOS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApReaderIOS(setter, fa)
}
//go:inline
func ApEitherS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Either[error, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApEitherS[C](setter, fa)
}
//go:inline
func ApIOEitherSL[C, S, T any](
lens Lens[S, T],
fa IOEither[error, T],
) Operator[C, S, S] {
return readerreaderioresult.ApIOEitherSL[C](lens, fa)
}
//go:inline
func ApIOSL[C, S, T any](
lens Lens[S, T],
fa IO[T],
) Operator[C, S, S] {
return readerreaderioresult.ApIOSL[C](lens, fa)
}
//go:inline
func ApReaderSL[C, S, T any](
lens Lens[S, T],
fa Reader[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderSL(lens, fa)
}
//go:inline
func ApReaderIOSL[C, S, T any](
lens Lens[S, T],
fa ReaderIO[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderIOSL(lens, fa)
}
//go:inline
func ApEitherSL[C, S, T any](
lens Lens[S, T],
fa Either[error, T],
) Operator[C, S, S] {
return readerreaderioresult.ApEitherSL[C](lens, fa)
}

768
v2/effect/bind_test.go Normal file
View File

@@ -0,0 +1,768 @@
// 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 effect
import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
)
type BindState struct {
Name string
Age int
Email string
}
func TestDo(t *testing.T) {
t.Run("creates effect with initial state", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 30}
eff := Do[TestContext](initial)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, initial, result)
})
t.Run("creates effect with empty struct", func(t *testing.T) {
type Empty struct{}
eff := Do[TestContext](Empty{})
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, Empty{}, result)
})
}
func TestBind(t *testing.T) {
t.Run("binds effect result to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("chains multiple binds", func(t *testing.T) {
initial := BindState{}
eff := Bind(
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext]("alice@example.com")
},
)(Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext](30)
},
)(Bind(
func(name string) func(BindState) BindState {
return func(s BindState) BindState {
s.Name = name
return s
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext]("Alice")
},
)(Do[TestContext](initial))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
assert.Equal(t, "alice@example.com", result.Email)
})
t.Run("propagates errors", func(t *testing.T) {
expectedErr := errors.New("bind error")
initial := BindState{Name: "Alice"}
eff := Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLet(t *testing.T) {
t.Run("computes value and binds to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := Let[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) int {
return len(s.Name) * 10
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 50, result.Age) // len("Alice") * 10
})
t.Run("chains with Bind", func(t *testing.T) {
initial := BindState{Name: "Bob"}
eff := Let[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
func(s BindState) string {
return s.Name + "@example.com"
},
)(Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext](25)
},
)(Do[TestContext](initial)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Bob", result.Name)
assert.Equal(t, 25, result.Age)
assert.Equal(t, "Bob@example.com", result.Email)
})
}
func TestLetTo(t *testing.T) {
t.Run("binds constant value to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := LetTo[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
42,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 42, result.Age)
})
t.Run("chains multiple LetTo", func(t *testing.T) {
initial := BindState{}
eff := LetTo[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
"test@example.com",
)(LetTo[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
30,
)(LetTo[TestContext](
func(name string) func(BindState) BindState {
return func(s BindState) BindState {
s.Name = name
return s
}
},
"Alice",
)(Do[TestContext](initial))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
assert.Equal(t, "test@example.com", result.Email)
})
}
func TestBindTo(t *testing.T) {
t.Run("wraps value in state", func(t *testing.T) {
type SimpleState struct {
Value int
}
eff := BindTo[TestContext](func(v int) SimpleState {
return SimpleState{Value: v}
})(Of[TestContext](42))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result.Value)
})
t.Run("starts a bind chain", func(t *testing.T) {
type State struct {
X int
Y string
}
eff := Let[TestContext](
func(y string) func(State) State {
return func(s State) State {
s.Y = y
return s
}
},
func(s State) string {
return "computed"
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext](10)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
assert.Equal(t, "computed", result.Y)
})
}
func TestApS(t *testing.T) {
t.Run("applies effect and binds result to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
ageEffect := Of[TestContext](30)
eff := ApS(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ageEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates errors from applied effect", func(t *testing.T) {
expectedErr := errors.New("aps error")
initial := BindState{Name: "Alice"}
ageEffect := Fail[TestContext, int](expectedErr)
eff := ApS(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ageEffect,
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestBindIOK(t *testing.T) {
t.Run("binds IO operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) io.IO[int] {
return func() int {
return 30
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindIOEitherK(t *testing.T) {
t.Run("binds successful IOEither to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Of[error](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates IOEither error", func(t *testing.T) {
expectedErr := errors.New("ioeither error")
initial := BindState{Name: "Alice"}
eff := BindIOEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Left[int](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestBindIOResultK(t *testing.T) {
t.Run("binds successful IOResult to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOResultK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioresult.IOResult[int] {
return ioresult.Of(30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindReaderK(t *testing.T) {
t.Run("binds Reader operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderK(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) reader.Reader[TestContext, int] {
return func(ctx TestContext) int {
return 30
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindReaderIOK(t *testing.T) {
t.Run("binds ReaderIO operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderIOK(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) readerio.ReaderIO[TestContext, int] {
return func(ctx TestContext) io.IO[int] {
return func() int {
return 30
}
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindEitherK(t *testing.T) {
t.Run("binds successful Either to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) either.Either[error, int] {
return either.Of[error](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates Either error", func(t *testing.T) {
expectedErr := errors.New("either error")
initial := BindState{Name: "Alice"}
eff := BindEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) either.Either[error, int] {
return either.Left[int](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLensOperations(t *testing.T) {
// Create lenses for BindState
nameLens := lens.MakeLens(
func(s BindState) string { return s.Name },
func(s BindState, name string) BindState {
s.Name = name
return s
},
)
ageLens := lens.MakeLens(
func(s BindState) int { return s.Age },
func(s BindState, age int) BindState {
s.Age = age
return s
},
)
t.Run("ApSL applies effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
ageEffect := Of[TestContext](30)
eff := ApSL(ageLens, ageEffect)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("BindL binds effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := BindL(
ageLens,
func(age int) Effect[TestContext, int] {
return Of[TestContext](age + 5)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("LetL computes value using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := LetL[TestContext](
ageLens,
func(age int) int {
return age * 2
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 50, result.Age)
})
t.Run("LetToL sets constant using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := LetToL[TestContext](ageLens, 100)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 100, result.Age)
})
t.Run("chains lens operations", func(t *testing.T) {
initial := BindState{}
eff := LetToL[TestContext](
ageLens,
30,
)(LetToL[TestContext](
nameLens,
"Bob",
)(Do[TestContext](initial)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Bob", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestApOperations(t *testing.T) {
t.Run("ApIOS applies IO effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
ioEffect := func() int { return 30 }
eff := ApIOS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ioEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
t.Run("ApReaderS applies Reader effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
readerEffect := func(ctx TestContext) int { return 30 }
eff := ApReaderS(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
readerEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
t.Run("ApEitherS applies Either effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eitherEffect := either.Of[error](30)
eff := ApEitherS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
eitherEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
}
func TestComplexBindChain(t *testing.T) {
t.Run("builds complex state with multiple operations", func(t *testing.T) {
type ComplexState struct {
Name string
Age int
Email string
IsAdmin bool
Score int
}
eff := LetTo[TestContext](
func(score int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Score = score
return s
}
},
100,
)(Let[TestContext](
func(isAdmin bool) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.IsAdmin = isAdmin
return s
}
},
func(s ComplexState) bool {
return s.Age >= 18
},
)(Let[TestContext](
func(email string) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Email = email
return s
}
},
func(s ComplexState) string {
return s.Name + "@example.com"
},
)(Bind(
func(age int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Age = age
return s
}
},
func(s ComplexState) Effect[TestContext, int] {
return Of[TestContext](25)
},
)(BindTo[TestContext](func(name string) ComplexState {
return ComplexState{Name: name}
})(Of[TestContext]("Alice"))))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 25, result.Age)
assert.Equal(t, "Alice@example.com", result.Email)
assert.True(t, result.IsAdmin)
assert.Equal(t, 100, result.Score)
})
}

110
v2/effect/dependencies.go Normal file
View File

@@ -0,0 +1,110 @@
package effect
import (
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
)
//go:inline
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
//go:inline
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
//go:inline
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOK[A](f)
}
//go:inline
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOResultK[A](f)
}
//go:inline
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalResultK[A](f)
}
//go:inline
func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderIOResultK[A](f)
}
// LocalEffectK transforms the context of an Effect using an Effect-returning function.
// This is the most powerful context transformation function, allowing the transformation
// itself to be effectful (can fail, perform I/O, and access the outer context).
//
// LocalEffectK takes a Kleisli arrow that:
// - Accepts the outer context C2
// - Returns an Effect that produces the inner context C1
// - Can fail with an error during context transformation
// - Can perform I/O operations during transformation
//
// This is useful when:
// - Context transformation requires I/O (e.g., loading config from a file)
// - Context transformation can fail (e.g., validating or parsing context)
// - Context transformation needs to access the outer context
//
// Type Parameters:
// - A: The value type produced by the effect
// - C1: The inner context type (required by the original effect)
// - C2: The outer context type (provided to the transformed effect)
//
// Parameters:
// - f: A Kleisli arrow (C2 -> Effect[C2, C1]) that transforms C2 to C1 effectfully
//
// Returns:
// - A function that transforms Effect[C1, A] to Effect[C2, A]
//
// Example:
//
// type DatabaseConfig struct {
// ConnectionString string
// }
//
// type AppConfig struct {
// ConfigPath string
// }
//
// // Effect that needs DatabaseConfig
// dbEffect := effect.Of[DatabaseConfig, string]("query result")
//
// // Transform AppConfig to DatabaseConfig effectfully
// // (e.g., load config from file, which can fail)
// loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
// return effect.Chain[AppConfig](func(_ AppConfig) Effect[AppConfig, DatabaseConfig] {
// // Simulate loading config from file (can fail)
// return effect.Of[AppConfig, DatabaseConfig](DatabaseConfig{
// ConnectionString: "loaded from " + app.ConfigPath,
// })
// })(effect.Of[AppConfig, AppConfig](app))
// }
//
// // Apply the transformation
// transform := effect.LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
// appEffect := transform(dbEffect)
//
// // Run with AppConfig
// ioResult := effect.Provide(AppConfig{ConfigPath: "/etc/app.conf"})(appEffect)
// readerResult := effect.RunSync(ioResult)
// result, err := readerResult(context.Background())
//
// Comparison with other Local functions:
// - Local/Contramap: Pure context transformation (C2 -> C1)
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
//
//go:inline
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
}

View File

@@ -0,0 +1,620 @@
// 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 effect
import (
"context"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/stretchr/testify/assert"
)
type OuterContext struct {
Value string
Number int
}
type InnerContext struct {
Value string
}
func TestLocal(t *testing.T) {
t.Run("transforms context for inner effect", func(t *testing.T) {
// Create an effect that uses InnerContext
innerEffect := Of[InnerContext]("result")
// Transform OuterContext to InnerContext
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
// Apply Local to transform the context
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("allows accessing outer context fields", func(t *testing.T) {
// Create an effect that reads from InnerContext
innerEffect := Chain(func(_ string) Effect[InnerContext, string] {
return Of[InnerContext]("inner value")
})(Of[InnerContext]("start"))
// Transform context
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value + " transformed"}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
Value: "original",
Number: 100,
})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner value", result)
})
t.Run("propagates errors from inner effect", func(t *testing.T) {
expectedErr := assert.AnError
innerEffect := Fail[InnerContext, string](expectedErr)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple Local transformations", func(t *testing.T) {
type Level1 struct {
A string
}
type Level2 struct {
B string
}
type Level3 struct {
C string
}
// Effect at deepest level
level3Effect := Of[Level3]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
return Level3{C: l2.B + "-c"}
})
// Transform Level1 -> Level2
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
return Level2{B: l1.A + "-b"}
})
// Compose transformations
level2Effect := local23(level3Effect)
level1Effect := local12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "deep result", result)
})
t.Run("works with complex context transformations", func(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
Database string
}
type AppConfig struct {
DB DatabaseConfig
APIKey string
Timeout int
}
// Effect that needs only DatabaseConfig
dbEffect := Of[DatabaseConfig]("connected")
// Extract DB config from AppConfig
accessor := func(app AppConfig) DatabaseConfig {
return app.DB
}
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
appEffect := kleisli(dbEffect)
// Run with full AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
DB: DatabaseConfig{
Host: "localhost",
Port: 5432,
Database: "mydb",
},
APIKey: "secret",
Timeout: 30,
})(appEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
}
func TestContramap(t *testing.T) {
t.Run("is equivalent to Local", func(t *testing.T) {
innerEffect := Of[InnerContext](42)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
// Test Local
localKleisli := Local[OuterContext, InnerContext, int](accessor)
localEffect := localKleisli(innerEffect)
// Test Contramap
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
contramapEffect := contramapKleisli(innerEffect)
outerCtx := OuterContext{Value: "test", Number: 100}
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localReader := RunSync(localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapReader := RunSync(contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
assert.NoError(t, localErr)
assert.NoError(t, contramapErr)
assert.Equal(t, localResult, contramapResult)
})
t.Run("transforms context correctly", func(t *testing.T) {
innerEffect := Of[InnerContext]("success")
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value + " modified"}
}
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
Value: "original",
Number: 50,
})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "success", result)
})
t.Run("handles errors from inner effect", func(t *testing.T) {
expectedErr := assert.AnError
innerEffect := Fail[InnerContext, int](expectedErr)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, int](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLocalAndContramapInteroperability(t *testing.T) {
t.Run("can be used interchangeably", func(t *testing.T) {
type Config1 struct {
Value string
}
type Config2 struct {
Data string
}
type Config3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Config3]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
return Config3{Info: c2.Data}
})
// Use Contramap for second transformation
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
return Config2{Data: c1.Value}
})
// Compose them
effect2 := local23(effect3)
effect1 := contramap12(effect2)
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
}
func TestLocalEffectK(t *testing.T) {
t.Run("transforms context using effectful function", func(t *testing.T) {
type DatabaseConfig struct {
ConnectionString string
}
type AppConfig struct {
ConfigPath string
}
// Effect that needs DatabaseConfig
dbEffect := Of[DatabaseConfig]("query result")
// Transform AppConfig to DatabaseConfig effectfully
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
return Of[AppConfig](DatabaseConfig{
ConnectionString: "loaded from " + app.ConfigPath,
})
}
// Apply the transformation
transform := LocalEffectK[string](loadConfig)
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "query result", result)
})
t.Run("propagates errors from context transformation", func(t *testing.T) {
type InnerCtx struct {
Value string
}
type OuterCtx struct {
Path string
}
innerEffect := Of[InnerCtx]("success")
expectedErr := assert.AnError
// Context transformation that fails
failingTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Fail[OuterCtx, InnerCtx](expectedErr)
}
transform := LocalEffectK[string](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates errors from inner effect", func(t *testing.T) {
type InnerCtx struct {
Value string
}
type OuterCtx struct {
Path string
}
expectedErr := assert.AnError
innerEffect := Fail[InnerCtx, string](expectedErr)
// Successful context transformation
transform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx](InnerCtx{Value: outer.Path})
}
transformK := LocalEffectK[string](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("allows effectful context transformation with IO operations", func(t *testing.T) {
type Config struct {
Data string
}
type AppContext struct {
ConfigFile string
}
// Effect that uses Config
configEffect := Chain(func(cfg Config) Effect[Config, string] {
return Of[Config]("processed: " + cfg.Data)
})(readerreaderioresult.Ask[Config]())
// Effectful transformation that simulates loading config
loadConfigEffect := func(app AppContext) Effect[AppContext, Config] {
// Simulate IO operation (e.g., reading file)
return Of[AppContext](Config{
Data: "loaded from " + app.ConfigFile,
})
}
transform := LocalEffectK[string](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "processed: loaded from config.json", result)
})
t.Run("chains multiple LocalEffectK transformations", func(t *testing.T) {
type Level1 struct {
A string
}
type Level2 struct {
B string
}
type Level3 struct {
C string
}
// Effect at deepest level
level3Effect := Of[Level3]("deep result")
// Transform Level2 -> Level3 effectfully
transform23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2](Level3{C: l2.B + "-c"})
})
// Transform Level1 -> Level2 effectfully
transform12 := LocalEffectK[string](func(l1 Level1) Effect[Level1, Level2] {
return Of[Level1](Level2{B: l1.A + "-b"})
})
// Compose transformations
level2Effect := transform23(level3Effect)
level1Effect := transform12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "deep result", result)
})
t.Run("accesses outer context during transformation", func(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
}
type AppConfig struct {
Environment string
DBHost string
DBPort int
}
// Effect that needs DatabaseConfig
dbEffect := Chain(func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
return Of[DatabaseConfig](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DatabaseConfig]())
// Transform using outer context
transformWithContext := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
// Access outer context to build inner context
prefix := ""
if app.Environment == "prod" {
prefix = "prod-"
}
return Of[AppConfig](DatabaseConfig{
Host: prefix + app.DBHost,
Port: app.DBPort,
})
}
transform := LocalEffectK[string](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
})(appEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Contains(t, result, "prod-localhost")
})
t.Run("validates context during transformation", func(t *testing.T) {
type ValidatedConfig struct {
APIKey string
}
type RawConfig struct {
APIKey string
}
innerEffect := Of[ValidatedConfig]("success")
// Validation that can fail
validateConfig := func(raw RawConfig) Effect[RawConfig, ValidatedConfig] {
if raw.APIKey == "" {
return Fail[RawConfig, ValidatedConfig](assert.AnError)
}
return Of[RawConfig](ValidatedConfig{
APIKey: raw.APIKey,
})
}
transform := LocalEffectK[string](validateConfig)
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync(ioResult2)
result, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "success", result)
})
t.Run("composes with other Local functions", func(t *testing.T) {
type Level1 struct {
Value string
}
type Level2 struct {
Data string
}
type Level3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Level3]("result")
// Use LocalEffectK for first transformation (effectful)
localEffectK23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2](Level3{Info: l2.Data})
})
// Use Local for second transformation (pure)
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
// Compose them
effect2 := localEffectK23(effect3)
effect1 := local12(effect2)
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("handles complex nested effects in transformation", func(t *testing.T) {
type InnerCtx struct {
Value int
}
type OuterCtx struct {
Multiplier int
}
// Effect that uses InnerCtx
innerEffect := Chain(func(ctx InnerCtx) Effect[InnerCtx, int] {
return Of[InnerCtx](ctx.Value * 2)
})(readerreaderioresult.Ask[InnerCtx]())
// Complex transformation with nested effects
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx](InnerCtx{
Value: outer.Multiplier * 10,
})
}
transform := LocalEffectK[int](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 60, result) // 3 * 10 * 2
})
}

222
v2/effect/doc.go Normal file
View File

@@ -0,0 +1,222 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package effect provides a functional effect system for managing side effects in Go.
# Overview
The effect package is a high-level abstraction for composing effectful computations
that may fail, require dependencies (context), and perform I/O operations. It is built
on top of ReaderReaderIOResult, providing a clean API for dependency injection and
error handling.
# Naming Conventions
The naming conventions in this package are modeled after effect-ts (https://effect.website/),
a popular TypeScript library for functional effect systems. This alignment helps developers
familiar with effect-ts to quickly understand and use this Go implementation.
# Core Type
The central type is Effect[C, A], which represents:
- C: The context/dependency type required by the effect
- A: The success value type produced by the effect
An Effect can:
- Succeed with a value of type A
- Fail with an error
- Require a context of type C
- Perform I/O operations
# Basic Operations
Creating Effects:
// Create a successful effect
effect.Succeed[MyContext, string]("hello")
// Create a failed effect
effect.Fail[MyContext, string](errors.New("failed"))
// Lift a pure value into an effect
effect.Of[MyContext, int](42)
Transforming Effects:
// Map over the success value
effect.Map[MyContext](func(x int) string {
return strconv.Itoa(x)
})
// Chain effects together (flatMap)
effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
return effect.Succeed[MyContext, string](strconv.Itoa(x))
})
// Tap into an effect without changing its value
effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
return effect.Succeed[MyContext, any](fmt.Println(x))
})
# Dependency Injection
Effects can access their required context:
// Transform the context before passing it to an effect
effect.Local[OuterCtx, InnerCtx](func(outer OuterCtx) InnerCtx {
return outer.Inner
})
// Provide a context to run an effect
effect.Provide[MyContext, string](myContext)
# Do Notation
The package provides "do notation" for composing effects in a sequential, imperative style:
type State struct {
X int
Y string
}
result := effect.Do[MyContext](State{}).
Bind(func(y string) func(State) State {
return func(s State) State {
s.Y = y
return s
}
}, fetchString).
Let(func(x int) func(State) State {
return func(s State) State {
s.X = x
return s
}
}, func(s State) int {
return len(s.Y)
})
# Bind Operations
The package provides various bind operations for integrating with other effect types:
- BindIOK: Bind an IO operation
- BindIOEitherK: Bind an IOEither operation
- BindIOResultK: Bind an IOResult operation
- BindReaderK: Bind a Reader operation
- BindReaderIOK: Bind a ReaderIO operation
- BindEitherK: Bind an Either operation
Each bind operation has a corresponding "L" variant for working with lenses:
- BindL, BindIOKL, BindReaderKL, etc.
# Applicative Operations
Apply effects in parallel:
// Apply a function effect to a value effect
effect.Ap[string, MyContext](valueEffect)(functionEffect)
// Apply effects to build up a structure
effect.ApS[MyContext](setter, effect1)
# Traversal
Traverse collections with effects:
// Map an array with an effectful function
effect.TraverseArray[MyContext](func(x int) Effect[MyContext, string] {
return effect.Succeed[MyContext, string](strconv.Itoa(x))
})
# Retry Logic
Retry effects with configurable policies:
effect.Retrying[MyContext, string](
retryPolicy,
func(status retry.RetryStatus) Effect[MyContext, string] {
return fetchData()
},
func(result Result[string]) bool {
return result.IsLeft() // retry on error
},
)
# Monoids
Combine effects using monoid operations:
// Combine effects using applicative semantics
effect.ApplicativeMonoid[MyContext](stringMonoid)
// Combine effects using alternative semantics (first success)
effect.AlternativeMonoid[MyContext](stringMonoid)
# Running Effects
To execute an effect:
// Provide the context
ioResult := effect.Provide[MyContext, string](myContext)(myEffect)
// Run synchronously
readerResult := effect.RunSync(ioResult)
// Execute with a context.Context
value, err := readerResult(ctx)
# Integration with Other Packages
The effect package integrates seamlessly with other fp-go packages:
- either: For error handling
- io: For I/O operations
- reader: For dependency injection
- result: For result types
- retry: For retry logic
- monoid: For combining effects
# Example
type Config struct {
APIKey string
BaseURL string
}
func fetchUser(id int) Effect[Config, User] {
return effect.Chain[Config](func(cfg Config) Effect[Config, User] {
// Use cfg.APIKey and cfg.BaseURL
return effect.Succeed[Config, User](User{ID: id})
})(effect.Of[Config, Config](Config{}))
}
func main() {
cfg := Config{APIKey: "key", BaseURL: "https://api.example.com"}
userEffect := fetchUser(42)
// Run the effect
ioResult := effect.Provide(cfg)(userEffect)
readerResult := effect.RunSync(ioResult)
user, err := readerResult(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)
}
*/
package effect
//go:generate go run ../main.go lens --dir . --filename gen_lens.go --include-test-files

51
v2/effect/effect.go Normal file
View File

@@ -0,0 +1,51 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
func Succeed[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
func Fail[C, A any](err error) Effect[C, A] {
return readerreaderioresult.Left[C, A](err)
}
func Of[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
return readerreaderioresult.Map[C](f)
}
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.Chain(f)
}
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
return readerreaderioresult.Ap[B](fa)
}
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
return readerreaderioresult.Defer(fa)
}
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
return readerreaderioresult.Tap(f)
}
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
return function.Ternary(pred, onTrue, onFalse)
}
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainResultK[C](f)
}
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
return readerreaderioresult.Read[A](c)
}

506
v2/effect/effect_test.go Normal file
View File

@@ -0,0 +1,506 @@
// 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 effect
import (
"context"
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type TestContext struct {
Value string
}
func runEffect[A any](eff Effect[TestContext, A], ctx TestContext) (A, error) {
ioResult := Provide[TestContext, A](ctx)(eff)
readerResult := RunSync(ioResult)
return readerResult(context.Background())
}
func TestSucceed(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Succeed[TestContext](42)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestContext]("hello")
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "hello", result)
})
t.Run("creates successful effect with struct", func(t *testing.T) {
type User struct {
Name string
Age int
}
user := User{Name: "Alice", Age: 30}
eff := Succeed[TestContext](user)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, user, result)
})
}
func TestFail(t *testing.T) {
t.Run("creates failed effect with error", func(t *testing.T) {
expectedErr := errors.New("test error")
eff := Fail[TestContext, int](expectedErr)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("creates failed effect with custom error", func(t *testing.T) {
expectedErr := fmt.Errorf("custom error: %s", "details")
eff := Fail[TestContext, string](expectedErr)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestOf(t *testing.T) {
t.Run("lifts value into effect", func(t *testing.T) {
eff := Of[TestContext](100)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 100, result)
})
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test value"
eff1 := Of[TestContext](value)
eff2 := Succeed[TestContext](value)
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
result2, err2 := runEffect(eff2, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, result1, result2)
})
}
func TestMap(t *testing.T) {
t.Run("maps over successful effect", func(t *testing.T) {
eff := Of[TestContext](10)
mapped := Map[TestContext](func(x int) int {
return x * 2
})(eff)
result, err := runEffect(mapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("maps to different type", func(t *testing.T) {
eff := Of[TestContext](42)
mapped := Map[TestContext](func(x int) string {
return fmt.Sprintf("value: %d", x)
})(eff)
result, err := runEffect(mapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", result)
})
t.Run("preserves error in failed effect", func(t *testing.T) {
expectedErr := errors.New("original error")
eff := Fail[TestContext, int](expectedErr)
mapped := Map[TestContext](func(x int) int {
return x * 2
})(eff)
_, err := runEffect(mapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := Of[TestContext](5)
result := Map[TestContext](func(x int) int {
return x + 1
})(Map[TestContext](func(x int) int {
return x * 2
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 11, value) // (5 * 2) + 1
})
}
func TestChain(t *testing.T) {
t.Run("chains successful effects", func(t *testing.T) {
eff := Of[TestContext](10)
chained := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("chains to different type", func(t *testing.T) {
eff := Of[TestContext](42)
chained := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext](fmt.Sprintf("number: %d", x))
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "number: 42", result)
})
t.Run("propagates first error", func(t *testing.T) {
expectedErr := errors.New("first error")
eff := Fail[TestContext, int](expectedErr)
chained := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedErr := errors.New("second error")
eff := Of[TestContext](10)
chained := Chain(func(x int) Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple operations", func(t *testing.T) {
eff := Of[TestContext](5)
result := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x + 10)
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, value) // (5 * 2) + 10
})
}
func TestAp(t *testing.T) {
t.Run("applies function effect to value effect", func(t *testing.T) {
fn := Of[TestContext](func(x int) int {
return x * 2
})
value := Of[TestContext](21)
result := Ap[int](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
t.Run("applies function to different type", func(t *testing.T) {
fn := Of[TestContext](func(x int) string {
return fmt.Sprintf("value: %d", x)
})
value := Of[TestContext](42)
result := Ap[string](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", val)
})
t.Run("propagates error from function effect", func(t *testing.T) {
expectedErr := errors.New("function error")
fn := Fail[TestContext, func(int) int](expectedErr)
value := Of[TestContext](42)
result := Ap[int](value)(fn)
_, err := runEffect(result, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates error from value effect", func(t *testing.T) {
expectedErr := errors.New("value error")
fn := Of[TestContext](func(x int) int {
return x * 2
})
value := Fail[TestContext, int](expectedErr)
result := Ap[int](value)(fn)
_, err := runEffect(result, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestSuspend(t *testing.T) {
t.Run("suspends effect computation", func(t *testing.T) {
callCount := 0
eff := Suspend(func() Effect[TestContext, int] {
callCount++
return Of[TestContext](42)
})
// Effect not executed yet
assert.Equal(t, 0, callCount)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 1, callCount)
})
t.Run("suspends failing effect", func(t *testing.T) {
expectedErr := errors.New("suspended error")
eff := Suspend(func() Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("allows lazy evaluation", func(t *testing.T) {
var value int
eff := Suspend(func() Effect[TestContext, int] {
return Of[TestContext](value)
})
value = 10
result1, err1 := runEffect(eff, TestContext{Value: "test"})
value = 20
result2, err2 := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 10, result1)
assert.Equal(t, 20, result2)
})
}
func TestTap(t *testing.T) {
t.Run("executes side effect without changing value", func(t *testing.T) {
sideEffectValue := 0
eff := Of[TestContext](42)
tapped := Tap(func(x int) Effect[TestContext, any] {
sideEffectValue = x * 2
return Of[TestContext, any](nil)
})(eff)
result, err := runEffect(tapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 84, sideEffectValue)
})
t.Run("propagates original error", func(t *testing.T) {
expectedErr := errors.New("original error")
eff := Fail[TestContext, int](expectedErr)
tapped := Tap(func(x int) Effect[TestContext, any] {
return Of[TestContext, any](nil)
})(eff)
_, err := runEffect(tapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates tap error", func(t *testing.T) {
expectedErr := errors.New("tap error")
eff := Of[TestContext](42)
tapped := Tap(func(x int) Effect[TestContext, any] {
return Fail[TestContext, any](expectedErr)
})(eff)
_, err := runEffect(tapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple taps", func(t *testing.T) {
values := []int{}
eff := Of[TestContext](10)
result := Tap(func(x int) Effect[TestContext, any] {
values = append(values, x+2)
return Of[TestContext, any](nil)
})(Tap(func(x int) Effect[TestContext, any] {
values = append(values, x+1)
return Of[TestContext, any](nil)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 10, value)
assert.Equal(t, []int{11, 12}, values)
})
}
func TestTernary(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext]("less or equal")
},
)
result, err := runEffect(kleisli(15), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "greater", result)
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext]("less or equal")
},
)
result, err := runEffect(kleisli(5), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "less or equal", result)
})
t.Run("handles errors in onTrue branch", func(t *testing.T) {
expectedErr := errors.New("true branch error")
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
},
func(x int) Effect[TestContext, string] {
return Of[TestContext]("less or equal")
},
)
_, err := runEffect(kleisli(15), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("handles errors in onFalse branch", func(t *testing.T) {
expectedErr := errors.New("false branch error")
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext]("greater")
},
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
},
)
_, err := runEffect(kleisli(5), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestEffectComposition(t *testing.T) {
t.Run("composes Map and Chain", func(t *testing.T) {
eff := Of[TestContext](5)
result := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext](fmt.Sprintf("result: %d", x))
})(Map[TestContext](func(x int) int {
return x * 2
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "result: 10", value)
})
t.Run("composes Chain and Tap", func(t *testing.T) {
sideEffect := 0
eff := Of[TestContext](10)
result := Tap(func(x int) Effect[TestContext, any] {
sideEffect = x
return Of[TestContext, any](nil)
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, value)
assert.Equal(t, 20, sideEffect)
})
}
func TestEffectWithResult(t *testing.T) {
t.Run("converts result to effect", func(t *testing.T) {
res := result.Of(42)
// This demonstrates integration with result package
assert.True(t, result.IsRight(res))
})
}

118
v2/effect/gen_lens_test.go Normal file
View File

@@ -0,0 +1,118 @@
package effect
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-27 22:19:41.6840253 +0100 CET m=+0.008579701
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"
__option "github.com/IBM/fp-go/v2/option"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
)
// ComplexServiceLenses provides lenses for accessing fields of ComplexService
type ComplexServiceLenses struct {
// mandatory fields
service1 __lens.Lens[ComplexService, Service1]
service2 __lens.Lens[ComplexService, Service2]
// optional fields
service1O __lens_option.LensO[ComplexService, Service1]
service2O __lens_option.LensO[ComplexService, Service2]
}
// ComplexServiceRefLenses provides lenses for accessing fields of ComplexService via a reference to ComplexService
type ComplexServiceRefLenses struct {
// mandatory fields
service1 __lens.Lens[*ComplexService, Service1]
service2 __lens.Lens[*ComplexService, Service2]
// optional fields
service1O __lens_option.LensO[*ComplexService, Service1]
service2O __lens_option.LensO[*ComplexService, Service2]
// prisms
service1P __prism.Prism[*ComplexService, Service1]
service2P __prism.Prism[*ComplexService, Service2]
}
// ComplexServicePrisms provides prisms for accessing fields of ComplexService
type ComplexServicePrisms struct {
service1 __prism.Prism[ComplexService, Service1]
service2 __prism.Prism[ComplexService, Service2]
}
// MakeComplexServiceLenses creates a new ComplexServiceLenses with lenses for all fields
func MakeComplexServiceLenses() ComplexServiceLenses {
// mandatory lenses
lensservice1 := __lens.MakeLensWithName(
func(s ComplexService) Service1 { return s.service1 },
func(s ComplexService, v Service1) ComplexService { s.service1 = v; return s },
"ComplexService.service1",
)
lensservice2 := __lens.MakeLensWithName(
func(s ComplexService) Service2 { return s.service2 },
func(s ComplexService, v Service2) ComplexService { s.service2 = v; return s },
"ComplexService.service2",
)
// optional lenses
lensservice1O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
lensservice2O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
return ComplexServiceLenses{
// mandatory lenses
service1: lensservice1,
service2: lensservice2,
// optional lenses
service1O: lensservice1O,
service2O: lensservice2O,
}
}
// MakeComplexServiceRefLenses creates a new ComplexServiceRefLenses with lenses for all fields
func MakeComplexServiceRefLenses() ComplexServiceRefLenses {
// mandatory lenses
lensservice1 := __lens.MakeLensStrictWithName(
func(s *ComplexService) Service1 { return s.service1 },
func(s *ComplexService, v Service1) *ComplexService { s.service1 = v; return s },
"(*ComplexService).service1",
)
lensservice2 := __lens.MakeLensStrictWithName(
func(s *ComplexService) Service2 { return s.service2 },
func(s *ComplexService, v Service2) *ComplexService { s.service2 = v; return s },
"(*ComplexService).service2",
)
// optional lenses
lensservice1O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
lensservice2O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
return ComplexServiceRefLenses{
// mandatory lenses
service1: lensservice1,
service2: lensservice2,
// optional lenses
service1O: lensservice1O,
service2O: lensservice2O,
}
}
// MakeComplexServicePrisms creates a new ComplexServicePrisms with prisms for all fields
func MakeComplexServicePrisms() ComplexServicePrisms {
_fromNonZeroservice1 := __option.FromNonZero[Service1]()
_prismservice1 := __prism.MakePrismWithName(
func(s ComplexService) __option.Option[Service1] { return _fromNonZeroservice1(s.service1) },
func(v Service1) ComplexService {
return ComplexService{ service1: v }
},
"ComplexService.service1",
)
_fromNonZeroservice2 := __option.FromNonZero[Service2]()
_prismservice2 := __prism.MakePrismWithName(
func(s ComplexService) __option.Option[Service2] { return _fromNonZeroservice2(s.service2) },
func(v Service2) ComplexService {
return ComplexService{ service2: v }
},
"ComplexService.service2",
)
return ComplexServicePrisms {
service1: _prismservice1,
service2: _prismservice2,
}
}

207
v2/effect/injection_test.go Normal file
View File

@@ -0,0 +1,207 @@
// Package effect demonstrates dependency injection using the Effect pattern.
//
// This test file shows how to build a type-safe dependency injection system where:
// - An InjectionContainer can resolve services by ID (InjectionToken)
// - Services are generic effects that depend on the container
// - Lookup methods convert from untyped container to typed dependencies
// - Handler functions depend type-safely on specific service interfaces
package effect
import (
"errors"
"fmt"
"testing"
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
type (
// InjectionToken is a unique identifier for services in the container
InjectionToken string
// InjectionContainer is an Effect that resolves services by their token.
// It takes an InjectionToken and returns a Thunk that produces any type.
// This allows the container to store and retrieve services of different types.
InjectionContainer = Effect[InjectionToken, any]
// Service is a generic Effect that depends on the InjectionContainer.
// It represents a computation that needs access to the dependency injection
// container to resolve its dependencies before producing a string result.
Service[T any] = Effect[InjectionContainer, T]
// Service1 is an example service interface that can be resolved from the container
Service1 interface {
GetService1() string
}
// Service2 is another example service interface
Service2 interface {
GetService2() string
}
// impl1 is a concrete implementation of Service1
impl1 struct{}
// impl2 is a concrete implementation of Service2
impl2 struct{}
)
// ComplexService demonstrates a more complex dependency injection scenario
// where a service depends on multiple other services. This struct aggregates
// Service1 and Service2, showing how to compose dependencies.
// The fp-go:Lens directive generates lens functions for type-safe field access.
//
// fp-go:Lens
type ComplexService struct {
service1 Service1
service2 Service2
}
func (_ *impl1) GetService1() string {
return "service1"
}
func (_ *impl2) GetService2() string {
return "service2"
}
const (
// service1 is the injection token for Service1
service1 = InjectionToken("service1")
// service2 is the injection token for Service2
service2 = InjectionToken("service2")
)
var (
// complexServiceLenses provides type-safe accessors for ComplexService fields,
// generated by the fp-go:Lens directive. These lenses are used in applicative
// composition to build the ComplexService from individual dependencies.
complexServiceLenses = MakeComplexServiceLenses()
)
// makeSampleInjectionContainer creates an InjectionContainer that can resolve services by ID.
// The container maps InjectionTokens to their corresponding service implementations.
// It returns an error if a requested service is not available.
func makeSampleInjectionContainer() InjectionContainer {
return func(token InjectionToken) Thunk[any] {
switch token {
case service1:
return thunk.Of(any(&impl1{}))
case service2:
return thunk.Of(any(&impl2{}))
default:
return thunk.Left[any](errors.New("dependency not available"))
}
}
}
// handleService1 is an Effect that depends type-safely on Service1.
// It demonstrates how to write handlers that work with specific service interfaces
// rather than the untyped container, providing compile-time type safety.
func handleService1() Effect[Service1, string] {
return func(ctx Service1) ReaderIOResult[string] {
return thunk.Of(fmt.Sprintf("Service1: %s", ctx.GetService1()))
}
}
// handleComplexService is an Effect that depends on ComplexService, which itself
// aggregates multiple service dependencies (Service1 and Service2).
// This demonstrates how to work with composite dependencies in a type-safe manner.
func handleComplexService() Effect[ComplexService, string] {
return func(ctx ComplexService) ReaderIOResult[string] {
return thunk.Of(fmt.Sprintf("ComplexService: %s x %s", ctx.service1.GetService1(), ctx.service2.GetService2()))
}
}
// lookupService1 is a lookup method that converts from an untyped InjectionContainer
// to a typed Service1 dependency. It performs two steps:
// 1. Read[any](service1) - retrieves the service from the container by token
// 2. ChainResultK(result.InstanceOf[Service1]) - safely casts from any to Service1
// This conversion provides type safety when moving from the untyped container to typed handlers.
var lookupService1 = F.Flow2(
Read[any](service1),
thunk.ChainResultK(result.InstanceOf[Service1]),
)
// lookupService2 is a lookup method for Service2, following the same pattern as lookupService1.
// It retrieves Service2 from the container and safely casts it to the correct type.
var lookupService2 = F.Flow2(
Read[any](service2),
thunk.ChainResultK(result.InstanceOf[Service2]),
)
// lookupComplexService demonstrates applicative composition for complex dependency injection.
// It builds a ComplexService by composing multiple service lookups:
// 1. Do[InjectionContainer](ComplexService{}) - starts with an empty ComplexService in the Effect context
// 2. ApSL(complexServiceLenses.service1, lookupService1) - looks up Service1 and sets it using the lens
// 3. ApSL(complexServiceLenses.service2, lookupService2) - looks up Service2 and sets it using the lens
//
// This applicative style allows parallel composition of independent dependencies,
// building the complete ComplexService from its constituent parts in a type-safe way.
var lookupComplexService = F.Pipe2(
Do[InjectionContainer](ComplexService{}),
ApSL(complexServiceLenses.service1, lookupService1),
ApSL(complexServiceLenses.service2, lookupService2),
)
// handleResult is a curried function that combines results from two services.
// It demonstrates how to compose the outputs of multiple effects into a final result.
// The curried form allows it to be used with applicative composition (ApS).
func handleResult(s1 string) func(string) string {
return func(s2 string) string {
return fmt.Sprintf("Final Result: %s : %s", s1, s2)
}
}
// TestDependencyLookup demonstrates both simple and complex dependency injection patterns:
//
// Simple Pattern (handle1):
// 1. Create an InjectionContainer with registered services
// 2. Define a handler (handleService1) that depends on a single typed service interface
// 3. Use a lookup method (lookupService1) to resolve the dependency from the container
// 4. Compose the handler with the lookup using LocalThunkK to inject the dependency
//
// Complex Pattern (handleComplex):
// 1. Define a handler (handleComplexService) that depends on a composite service (ComplexService)
// 2. Use applicative composition (lookupComplexService) to build the composite from multiple lookups
// 3. Each sub-dependency is resolved independently and combined using lenses
// 4. LocalThunkK injects the complete composite dependency into the handler
//
// Service Composition:
// - ApS combines the results of handle1 and handleComplex using handleResult
// - This demonstrates how to compose multiple independent effects that share the same container
// - The final result aggregates outputs from both simple and complex dependency patterns
func TestDependencyLookup(t *testing.T) {
// Create the dependency injection container
container := makeSampleInjectionContainer()
// Simple dependency injection: single service lookup
// LocalThunkK transforms the handler to work with the container
handle1 := F.Pipe1(
handleService1(),
LocalThunkK[string](lookupService1),
)
// Complex dependency injection: composite service with multiple dependencies
// lookupComplexService uses applicative composition to build ComplexService
handleComplex := F.Pipe1(
handleComplexService(),
LocalThunkK[string](lookupComplexService),
)
// Compose both services using applicative style
// ApS applies handleResult to combine outputs from handle1 and handleComplex
result := F.Pipe1(
handle1,
ApS(handleResult, handleComplex),
)
// Execute: provide container, then context, then run the IO operation
res := result(container)(t.Context())()
fmt.Println(res)
}

14
v2/effect/monoid.go Normal file
View File

@@ -0,0 +1,14 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/monoid"
)
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.ApplicativeMonoid[C](m)
}
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.AlternativeMonoid[C](m)
}

350
v2/effect/monoid_test.go Normal file
View File

@@ -0,0 +1,350 @@
// 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 effect
import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/monoid"
"github.com/stretchr/testify/assert"
)
func TestApplicativeMonoid(t *testing.T) {
t.Run("combines successful effects with string monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext]("Hello")
eff2 := Of[TestContext](" ")
eff3 := Of[TestContext]("World")
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Hello World", result)
})
t.Run("combines successful effects with int monoid", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
eff1 := Of[TestContext](10)
eff2 := Of[TestContext](20)
eff3 := Of[TestContext](30)
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 60, result)
})
t.Run("returns empty value for empty monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"empty",
)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "empty", result)
})
t.Run("propagates first error", func(t *testing.T) {
expectedErr := errors.New("first error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
eff1 := Fail[TestContext, string](expectedErr)
eff2 := Of[TestContext]("World")
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedErr := errors.New("second error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext]("Hello")
eff2 := Fail[TestContext, string](expectedErr)
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("combines multiple effects", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a * b },
1,
)
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
effects := []Effect[TestContext, int]{
Of[TestContext](2),
Of[TestContext](3),
Of[TestContext](4),
Of[TestContext](5),
}
combined := effectMonoid.Empty()
for _, eff := range effects {
combined = effectMonoid.Concat(combined, eff)
}
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 120, result) // 1 * 2 * 3 * 4 * 5
})
t.Run("works with custom types", func(t *testing.T) {
type Counter struct {
Count int
}
counterMonoid := monoid.MakeMonoid(
func(a, b Counter) Counter {
return Counter{Count: a.Count + b.Count}
},
Counter{Count: 0},
)
effectMonoid := ApplicativeMonoid[TestContext](counterMonoid)
eff1 := Of[TestContext](Counter{Count: 5})
eff2 := Of[TestContext](Counter{Count: 10})
eff3 := Of[TestContext](Counter{Count: 15})
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Count)
})
}
func TestAlternativeMonoid(t *testing.T) {
t.Run("combines successful effects with monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext]("First")
eff2 := Of[TestContext]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "FirstSecond", result) // Alternative still combines when both succeed
})
t.Run("tries second effect if first fails", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first failed"))
eff2 := Of[TestContext]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Second", result)
})
t.Run("returns error if all effects fail", func(t *testing.T) {
expectedErr := errors.New("second error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first error"))
eff2 := Fail[TestContext, string](expectedErr)
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("returns empty value for empty monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"default",
)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "default", result)
})
t.Run("chains multiple alternatives", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
effectMonoid := AlternativeMonoid[TestContext](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Fail[TestContext, int](errors.New("error 2"))
eff3 := Of[TestContext](42)
eff4 := Of[TestContext](100)
combined := effectMonoid.Concat(
effectMonoid.Concat(eff1, eff2),
effectMonoid.Concat(eff3, eff4),
)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 142, result) // Combines successful values: 42 + 100
})
t.Run("works with custom types", func(t *testing.T) {
type Result struct {
Value string
Code int
}
resultMonoid := monoid.MakeMonoid(
func(a, b Result) Result {
return Result{Value: a.Value + b.Value, Code: a.Code + b.Code}
},
Result{Value: "", Code: 0},
)
effectMonoid := AlternativeMonoid[TestContext](resultMonoid)
eff1 := Fail[TestContext, Result](errors.New("failed"))
eff2 := Of[TestContext](Result{Value: "success", Code: 200})
eff3 := Of[TestContext](Result{Value: "backup", Code: 201})
combined := effectMonoid.Concat(effectMonoid.Concat(eff1, eff2), eff3)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "successbackup", result.Value) // Combines both successful values
assert.Equal(t, 401, result.Code) // 200 + 201
})
}
func TestMonoidComparison(t *testing.T) {
t.Run("ApplicativeMonoid vs AlternativeMonoid with all success", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + "," + b },
"",
)
applicativeMonoid := ApplicativeMonoid[TestContext](stringMonoid)
alternativeMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext]("A")
eff2 := Of[TestContext]("B")
// Applicative combines values
applicativeResult, err1 := runEffect(
applicativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
// Alternative takes first
alternativeResult, err2 := runEffect(
alternativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, "A,B", applicativeResult) // Combined with comma separator
assert.Equal(t, "A,B", alternativeResult) // Also combined (Alternative uses Alt semigroup)
})
t.Run("ApplicativeMonoid vs AlternativeMonoid with failures", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
applicativeMonoid := ApplicativeMonoid[TestContext](intMonoid)
alternativeMonoid := AlternativeMonoid[TestContext](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Of[TestContext](42)
// Applicative fails on first error
_, err1 := runEffect(
applicativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
// Alternative tries second on first failure
result2, err2 := runEffect(
alternativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
assert.Error(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 42, result2)
})
}

14
v2/effect/retry.go Normal file
View File

@@ -0,0 +1,14 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/retry"
)
func Retrying[C, A any](
policy retry.RetryPolicy,
action Kleisli[C, retry.RetryStatus, A],
check Predicate[Result[A]],
) Effect[C, A] {
return readerreaderioresult.Retrying(policy, action, check)
}

377
v2/effect/retry_test.go Normal file
View File

@@ -0,0 +1,377 @@
// 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 effect
import (
"errors"
"testing"
"time"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
func TestRetrying(t *testing.T) {
t.Run("succeeds on first attempt", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Of[TestContext]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.Equal(t, 1, attemptCount)
})
t.Run("retries on failure and eventually succeeds", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("temporary error"))
}
return Of[TestContext]("success after retries")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "success after retries", result)
assert.Equal(t, 3, attemptCount)
})
t.Run("exhausts retry limit", func(t *testing.T) {
attemptCount := 0
maxRetries := uint(3)
policy := retry.LimitRetries(maxRetries)
eff := Retrying(
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Fail[TestContext, string](errors.New("persistent error"))
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, int(maxRetries+1), attemptCount) // initial attempt + retries
})
t.Run("does not retry on success", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext](42)
},
func(res Result[int]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 1, attemptCount)
})
t.Run("uses custom retry predicate", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext](attemptCount * 10)
},
func(res Result[int]) bool {
// Retry if value is less than 30
if result.IsRight(res) {
val, _ := result.Unwrap(res)
return val < 30
}
return true
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result)
assert.Equal(t, 3, attemptCount)
})
t.Run("tracks retry status", func(t *testing.T) {
var statuses []retry.RetryStatus
policy := retry.LimitRetries(3)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
statuses = append(statuses, status)
if len(statuses) < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext]("done")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "done", result)
assert.Len(t, statuses, 3)
// First attempt has iteration 0
assert.Equal(t, uint(0), statuses[0].IterNumber)
assert.Equal(t, uint(1), statuses[1].IterNumber)
assert.Equal(t, uint(2), statuses[2].IterNumber)
})
t.Run("works with exponential backoff", func(t *testing.T) {
attemptCount := 0
policy := retry.Monoid.Concat(
retry.LimitRetries(3),
retry.ExponentialBackoff(10*time.Millisecond),
)
startTime := time.Now()
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
elapsed := time.Since(startTime)
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.Equal(t, 3, attemptCount)
// Should have some delay due to backoff
assert.Greater(t, elapsed, 10*time.Millisecond)
})
t.Run("combines with other effect operations", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Map[TestContext](func(s string) string {
return "mapped: " + s
})(Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "mapped: success", result)
assert.Equal(t, 2, attemptCount)
})
t.Run("retries with different error types", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
errors := []error{
errors.New("error 1"),
errors.New("error 2"),
errors.New("error 3"),
}
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
if attemptCount < len(errors) {
err := errors[attemptCount]
attemptCount++
return Fail[TestContext, string](err)
}
attemptCount++
return Of[TestContext]("finally succeeded")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "finally succeeded", result)
assert.Equal(t, 4, attemptCount)
})
t.Run("no retry when predicate returns false", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying(
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Fail[TestContext, string](errors.New("error"))
},
func(res Result[string]) bool {
return false // never retry
},
)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, 1, attemptCount) // only initial attempt
})
t.Run("retries with context access", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
ctx := TestContext{Value: "retry-context"}
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext]("success with context")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, ctx)
assert.NoError(t, err)
assert.Equal(t, "success with context", result)
assert.Equal(t, 2, attemptCount)
})
}
func TestRetryingWithComplexScenarios(t *testing.T) {
t.Run("retry with state accumulation", func(t *testing.T) {
type State struct {
Attempts []int
Value string
}
policy := retry.LimitRetries(4)
eff := Retrying[TestContext, State](
policy,
func(status retry.RetryStatus) Effect[TestContext, State] {
state := State{
Attempts: make([]int, status.IterNumber+1),
Value: "attempt",
}
for i := uint(0); i <= status.IterNumber; i++ {
state.Attempts[i] = int(i)
}
if status.IterNumber < 2 {
return Fail[TestContext, State](errors.New("retry"))
}
return Of[TestContext](state)
},
func(res Result[State]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "attempt", result.Value)
assert.Equal(t, []int{0, 1, 2}, result.Attempts)
})
t.Run("retry with chain operations", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext]("final: " + string(rune('0'+x)))
})(Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, int](errors.New("retry"))
}
return Of[TestContext](attemptCount)
},
func(res Result[int]) bool {
return result.IsLeft(res)
},
))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Contains(t, result, "final:")
})
}

19
v2/effect/run.go Normal file
View File

@@ -0,0 +1,19 @@
package effect
import (
"context"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
"github.com/IBM/fp-go/v2/result"
)
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
return readerreaderioresult.Read[A](c)
}
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return result.Unwrap(fa(ctx)())
}
}

326
v2/effect/run_test.go Normal file
View File

@@ -0,0 +1,326 @@
// 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 effect
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProvide(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
ctx := TestContext{Value: "test-value"}
eff := Of[TestContext]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("provides context with specific values", func(t *testing.T) {
type Config struct {
Host string
Port int
}
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config]("connected")
ioResult := Provide[Config, string](cfg)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
t.Run("propagates errors", func(t *testing.T) {
expectedErr := errors.New("provide error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("works with different context types", func(t *testing.T) {
type SimpleContext struct {
ID int
}
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, result)
})
t.Run("provides context to chained effects", func(t *testing.T) {
ctx := TestContext{Value: "base"}
eff := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext]("result")
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("provides context to mapped effects", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Map[TestContext](func(x int) string {
return "mapped"
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "mapped", result)
})
}
func TestRunSync(t *testing.T) {
t.Run("runs effect synchronously", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("runs effect with context.Context", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync(ioResult)
bgCtx := context.Background()
result, err := readerResult(bgCtx)
assert.NoError(t, err)
assert.Equal(t, "hello", result)
})
t.Run("propagates errors synchronously", func(t *testing.T) {
expectedErr := errors.New("sync error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("runs complex effect chains", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x + 10)
})(Of[TestContext](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 30, result) // (5 + 10) * 2
})
t.Run("handles multiple sequential runs", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync(ioResult)
// Run multiple times
result1, err1 := readerResult(context.Background())
result2, err2 := readerResult(context.Background())
result3, err3 := readerResult(context.Background())
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
assert.Equal(t, 42, result1)
assert.Equal(t, 42, result2)
assert.Equal(t, 42, result3)
})
t.Run("works with different result types", func(t *testing.T) {
type User struct {
Name string
Age int
}
ctx := TestContext{Value: "test"}
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext](user)
ioResult := Provide[TestContext, User](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, user, result)
})
}
func TestProvideAndRunSyncIntegration(t *testing.T) {
t.Run("complete workflow with success", func(t *testing.T) {
type AppConfig struct {
APIKey string
Timeout int
}
cfg := AppConfig{APIKey: "secret", Timeout: 30}
// Create an effect that uses the config
eff := Of[AppConfig]("API call successful")
// Provide config and run
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
})
t.Run("complete workflow with error", func(t *testing.T) {
type AppConfig struct {
APIKey string
}
expectedErr := errors.New("API error")
cfg := AppConfig{APIKey: "secret"}
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("workflow with transformations", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Map[TestContext](func(x int) string {
return "final"
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(Of[TestContext](21)))
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
})
t.Run("workflow with bind operations", func(t *testing.T) {
type State struct {
X int
Y int
}
ctx := TestContext{Value: "test"}
eff := Bind(
func(y int) func(State) State {
return func(s State) State {
s.Y = y
return s
}
},
func(s State) Effect[TestContext, int] {
return Of[TestContext](s.X * 2)
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext](10)))
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
assert.Equal(t, 20, result.Y)
})
t.Run("workflow with context transformation", func(t *testing.T) {
type OuterCtx struct {
Value string
}
type InnerCtx struct {
Data string
}
outerCtx := OuterCtx{Value: "outer"}
innerEff := Of[InnerCtx]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
})
t.Run("workflow with array traversal", func(t *testing.T) {
ctx := TestContext{Value: "test"}
input := []int{1, 2, 3, 4, 5}
eff := TraverseArray(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(input)
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
}

7
v2/effect/traverse.go Normal file
View File

@@ -0,0 +1,7 @@
package effect
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
return readerreaderioresult.TraverseArray(f)
}

266
v2/effect/traverse_test.go Normal file
View File

@@ -0,0 +1,266 @@
// 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 effect
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTraverseArray(t *testing.T) {
t.Run("traverses empty array", func(t *testing.T) {
input := []int{}
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("traverses array with single element", func(t *testing.T) {
input := []int{42}
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"42"}, result)
})
t.Run("traverses array with multiple elements", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, result)
})
t.Run("transforms to different type", func(t *testing.T) {
input := []string{"hello", "world", "test"}
kleisli := TraverseArray(func(s string) Effect[TestContext, int] {
return Of[TestContext](len(s))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{5, 5, 4}, result)
})
t.Run("stops on first error", func(t *testing.T) {
expectedErr := errors.New("traverse error")
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
if x == 3 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
input := []int{1, 2, 3}
kleisli := TraverseArray(func(id int) Effect[TestContext, User] {
return Of[TestContext](User{
ID: id,
Name: fmt.Sprintf("User%d", id),
})
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Len(t, result, 3)
assert.Equal(t, 1, result[0].ID)
assert.Equal(t, "User1", result[0].Name)
assert.Equal(t, 2, result[1].ID)
assert.Equal(t, "User2", result[1].Name)
assert.Equal(t, 3, result[2].ID)
assert.Equal(t, "User3", result[2].Name)
})
t.Run("chains with other operations", func(t *testing.T) {
input := []int{1, 2, 3}
eff := Chain(func(strings []string) Effect[TestContext, int] {
total := 0
for _, s := range strings {
val, _ := strconv.Atoi(s)
total += val
}
return Of[TestContext](total)
})(TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x * 2))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 12, result) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
})
t.Run("uses context in transformation", func(t *testing.T) {
input := []int{1, 2, 3}
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Chain(func(ctx TestContext) Effect[TestContext, string] {
return Of[TestContext](fmt.Sprintf("%s-%d", ctx.Value, x))
})(Of[TestContext](TestContext{Value: "prefix"}))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"prefix-1", "prefix-2", "prefix-3"}, result)
})
t.Run("preserves order", func(t *testing.T) {
input := []int{5, 3, 8, 1, 9, 2}
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 10)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{50, 30, 80, 10, 90, 20}, result)
})
t.Run("handles large arrays", func(t *testing.T) {
size := 1000
input := make([]int, size)
for i := 0; i < size; i++ {
input[i] = i
}
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Len(t, result, size)
assert.Equal(t, 0, result[0])
assert.Equal(t, 1998, result[999])
})
t.Run("composes multiple traversals", func(t *testing.T) {
input := []int{1, 2, 3}
// First traversal: int -> string
kleisli1 := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
// Second traversal: string -> int (length)
kleisli2 := TraverseArray(func(s string) Effect[TestContext, int] {
return Of[TestContext](len(s))
})
eff := Chain(kleisli2)(kleisli1(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{1, 1, 1}, result) // All single-digit numbers have length 1
})
t.Run("handles nil array", func(t *testing.T) {
var input []int
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Empty(t, result) // TraverseArray returns empty slice for nil input
})
t.Run("works with Map for post-processing", func(t *testing.T) {
input := []int{1, 2, 3}
eff := Map[TestContext](func(strings []string) string {
result := ""
for _, s := range strings {
result += s + ","
}
return result
})(TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "1,2,3,", result)
})
t.Run("error in middle of array", func(t *testing.T) {
expectedErr := errors.New("middle error")
input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("error at end of array", func(t *testing.T) {
expectedErr := errors.New("end error")
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}

37
v2/effect/types.go Normal file
View File

@@ -0,0 +1,37 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
type (
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
IO[A any] = io.IO[A]
IOEither[E, A any] = ioeither.IOEither[E, A]
Lazy[A any] = lazy.Lazy[A]
IOResult[A any] = ioresult.IOResult[A]
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
Monoid[A any] = monoid.Monoid[A]
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
Thunk[A any] = ReaderIOResult[A]
Predicate[A any] = predicate.Predicate[A]
Result[A any] = result.Result[A]
Lens[S, T any] = lens.Lens[S, T]
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
)

View File

@@ -188,6 +188,81 @@ func MonadChain[E, A, B any](fa Either[E, A], f Kleisli[E, A, B]) Either[E, B] {
return f(fa.r)
}
// MonadChainLeft sequences a computation on the Left (error) value, allowing error recovery or transformation.
// If the Either is Left, applies the provided function to the error value, which returns a new Either.
// If the Either is Right, returns the Right value unchanged with the new error type.
//
// This is the dual of [MonadChain] - while MonadChain operates on Right values (success),
// MonadChainLeft operates on Left values (errors). It's useful for error recovery, error transformation,
// or chaining alternative computations when an error occurs.
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// The error type can be transformed from EA to EB, allowing flexible error type conversions.
//
// Example:
//
// // Error recovery: convert specific errors to success
// result := either.MonadChainLeft(
// either.Left[int](errors.New("not found")),
// func(err error) either.Either[string, int] {
// if err.Error() == "not found" {
// return either.Right[string](0) // default value
// }
// return either.Left[int](err.Error()) // transform error
// },
// ) // Right(0)
//
// // Error transformation: change error type
// result := either.MonadChainLeft(
// either.Left[int](404),
// func(code int) either.Either[string, int] {
// return either.Left[int](fmt.Sprintf("Error code: %d", code))
// },
// ) // Left("Error code: 404")
//
// // Right values pass through unchanged
// result := either.MonadChainLeft(
// either.Right[error](42),
// func(err error) either.Either[string, int] {
// return either.Left[int]("error")
// },
// ) // Right(42)
//
//go:inline
func MonadChainLeft[EA, EB, A any](fa Either[EA, A], f Kleisli[EB, EA, A]) Either[EB, A] {
return MonadFold(fa, f, Of[EB])
}
// ChainLeft is the curried version of [MonadChainLeft].
// Returns a function that sequences a computation on the Left (error) value.
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is useful for creating reusable error handlers or transformers that can be
// composed with other Either operations using pipes or function composition.
//
// Example:
//
// // Create a reusable error handler
// handleNotFound := either.ChainLeft[error, string](func(err error) either.Either[string, int] {
// if err.Error() == "not found" {
// return either.Right[string](0)
// }
// return either.Left[int](err.Error())
// })
//
// // Use in a pipeline
// result := F.Pipe1(
// either.Left[int](errors.New("not found")),
// handleNotFound,
// ) // Right(0)
//
//go:inline
func ChainLeft[EA, EB, A any](f Kleisli[EB, EA, A]) Kleisli[EB, Either[EA, A], A] {
return Fold(f, Of[EB])
}
// MonadChainFirst executes a side-effect computation but returns the original value.
// Useful for performing actions (like logging) without changing the value.
//
@@ -471,6 +546,8 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
// If the Either is Left, it applies the provided function to the error value,
// which returns a new Either that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative computations.
// The error type can be widened from E1 to E2, allowing transformation of error types.
//
@@ -504,7 +581,7 @@ func ToType[A, E any](onError func(any) E) func(any) Either[E, A] {
return func(value any) Either[E, A] {
return F.Pipe2(
value,
O.ToType[A],
O.InstanceOf[A],
O.Fold(F.Nullary3(F.Constant(value), onError, Left[A, E]), Right[E, A]),
)
}

View File

@@ -124,79 +124,52 @@ func TestStringer(t *testing.T) {
func TestZeroWithIntegers(t *testing.T) {
e := Zero[error, int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, 0, value, "Right value should be zero for int")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](0), e, "Zero should create a Right value with zero for int")
}
// TestZeroWithStrings tests Zero function with string types
func TestZeroWithStrings(t *testing.T) {
e := Zero[error, string]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, "", value, "Right value should be empty string")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](""), e, "Zero should create a Right value with empty string")
}
// TestZeroWithBooleans tests Zero function with boolean types
func TestZeroWithBooleans(t *testing.T) {
e := Zero[error, bool]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Equal(t, false, value, "Right value should be false for bool")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](false), e, "Zero should create a Right value with false for bool")
}
// TestZeroWithFloats tests Zero function with float types
func TestZeroWithFloats(t *testing.T) {
e := Zero[error, float64]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](0.0), e, "Zero should create a Right value with 0.0 for float64")
}
// TestZeroWithPointers tests Zero function with pointer types
func TestZeroWithPointers(t *testing.T) {
e := Zero[error, *int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for pointer type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilPtr *int
assert.Equal(t, Of[error](nilPtr), e, "Zero should create a Right value with nil pointer")
}
// TestZeroWithSlices tests Zero function with slice types
func TestZeroWithSlices(t *testing.T) {
e := Zero[error, []int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for slice type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilSlice []int
assert.Equal(t, Of[error](nilSlice), e, "Zero should create a Right value with nil slice")
}
// TestZeroWithMaps tests Zero function with map types
func TestZeroWithMaps(t *testing.T) {
e := Zero[error, map[string]int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for map type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilMap map[string]int
assert.Equal(t, Of[error](nilMap), e, "Zero should create a Right value with nil map")
}
// TestZeroWithStructs tests Zero function with struct types
@@ -208,23 +181,16 @@ func TestZeroWithStructs(t *testing.T) {
e := Zero[error, TestStruct]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
expected := TestStruct{Field1: 0, Field2: ""}
assert.Equal(t, expected, value, "Right value should be zero value for struct")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for struct")
}
// TestZeroWithInterfaces tests Zero function with interface types
func TestZeroWithInterfaces(t *testing.T) {
e := Zero[error, interface{}]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for interface type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilInterface interface{}
assert.Equal(t, Of[error](nilInterface), e, "Zero should create a Right value with nil interface")
}
// TestZeroWithCustomErrorType tests Zero function with custom error types
@@ -236,12 +202,7 @@ func TestZeroWithCustomErrorType(t *testing.T) {
e := Zero[CustomError, string]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, "", value, "Right value should be empty string")
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
assert.Equal(t, Of[CustomError](""), e, "Zero should create a Right value with empty string")
}
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
@@ -252,17 +213,13 @@ func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
mapped := MonadMap(e, func(n int) string {
return fmt.Sprintf("%d", n)
})
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
value, _ := Unwrap(mapped)
assert.Equal(t, "0", value, "Mapped value should be '0'")
assert.Equal(t, Of[error]("0"), mapped, "Mapped Zero should be Right with '0'")
// Test with Chain
chained := MonadChain(e, func(n int) Either[error, string] {
return Right[error](fmt.Sprintf("value: %d", n))
})
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
chainedValue, _ := Unwrap(chained)
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
assert.Equal(t, Of[error]("value: 0"), chained, "Chained Zero should be Right with 'value: 0'")
// Test with Fold
folded := MonadFold(e,
@@ -295,23 +252,15 @@ func TestZeroWithComplexTypes(t *testing.T) {
e := Zero[error, ComplexType]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
expected := ComplexType{Nested: nil, Ptr: nil}
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for complex struct")
}
// TestZeroWithOption tests Zero with Option type
func TestZeroWithOption(t *testing.T) {
e := Zero[error, O.Option[int]]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](O.None[int]()), e, "Zero should create a Right value with None option")
}
// TestZeroIsNotLeft tests that Zero never creates a Left value
@@ -343,3 +292,211 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
}
// TestMonadChainLeft tests the MonadChainLeft function with various scenarios
func TestMonadChainLeft(t *testing.T) {
t.Run("Left value is transformed by function", func(t *testing.T) {
// Transform error to success
result := MonadChainLeft(
Left[int](errors.New("not found")),
func(err error) Either[string, int] {
if err.Error() == "not found" {
return Right[string](0) // default value
}
return Left[int](err.Error())
},
)
assert.Equal(t, Of[string](0), result)
})
t.Run("Left value error type is transformed", func(t *testing.T) {
// Transform error type from int to string
result := MonadChainLeft(
Left[int](404),
func(code int) Either[string, int] {
return Left[int](fmt.Sprintf("Error code: %d", code))
},
)
assert.Equal(t, Left[int]("Error code: 404"), result)
})
t.Run("Right value passes through unchanged", func(t *testing.T) {
// Right value should not be affected
result := MonadChainLeft(
Right[error](42),
func(err error) Either[string, int] {
return Left[int]("should not be called")
},
)
assert.Equal(t, Of[string](42), result)
})
t.Run("Chain multiple error transformations", func(t *testing.T) {
// First transformation
step1 := MonadChainLeft(
Left[int](errors.New("error1")),
func(err error) Either[error, int] {
return Left[int](errors.New("error2"))
},
)
// Second transformation
step2 := MonadChainLeft(
step1,
func(err error) Either[string, int] {
return Left[int](err.Error())
},
)
assert.Equal(t, Left[int]("error2"), step2)
})
t.Run("Error recovery with fallback", func(t *testing.T) {
// Recover from specific errors
result := MonadChainLeft(
Left[int](errors.New("timeout")),
func(err error) Either[error, int] {
if err.Error() == "timeout" {
return Right[error](999) // fallback value
}
return Left[int](err)
},
)
assert.Equal(t, Of[error](999), result)
})
t.Run("Transform error to different Left", func(t *testing.T) {
// Transform one error to another
result := MonadChainLeft(
Left[string]("original error"),
func(s string) Either[int, string] {
return Left[string](len(s))
},
)
assert.Equal(t, Left[string](14), result) // length of "original error"
})
}
// TestChainLeft tests the curried ChainLeft function
func TestChainLeft(t *testing.T) {
t.Run("Curried function transforms Left value", func(t *testing.T) {
// Create a reusable error handler
handleNotFound := ChainLeft[error, string](func(err error) Either[string, int] {
if err.Error() == "not found" {
return Right[string](0)
}
return Left[int](err.Error())
})
result := handleNotFound(Left[int](errors.New("not found")))
assert.Equal(t, Of[string](0), result)
})
t.Run("Curried function with Right value", func(t *testing.T) {
handler := ChainLeft[error, string](func(err error) Either[string, int] {
return Left[int]("should not be called")
})
result := handler(Right[error](42))
assert.Equal(t, Of[string](42), result)
})
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
// Create error transformer
toStringError := ChainLeft[int, string](func(code int) Either[string, string] {
return Left[string](fmt.Sprintf("Error: %d", code))
})
result := F.Pipe1(
Left[string](404),
toStringError,
)
assert.Equal(t, Left[string]("Error: 404"), result)
})
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
// First handler: convert error to string
handler1 := ChainLeft[error, string](func(err error) Either[string, int] {
return Left[int](err.Error())
})
// Second handler: add prefix to string error
handler2 := ChainLeft[string, string](func(s string) Either[string, int] {
return Left[int]("Handled: " + s)
})
result := F.Pipe2(
Left[int](errors.New("original")),
handler1,
handler2,
)
assert.Equal(t, Left[int]("Handled: original"), result)
})
t.Run("Error recovery in pipeline", func(t *testing.T) {
// Handler that recovers from specific errors
recoverFromTimeout := ChainLeft(func(err error) Either[error, int] {
if err.Error() == "timeout" {
return Right[error](0) // recovered value
}
return Left[int](err) // propagate other errors
})
// Test with timeout error
result1 := F.Pipe1(
Left[int](errors.New("timeout")),
recoverFromTimeout,
)
assert.Equal(t, Of[error](0), result1)
// Test with other error
result2 := F.Pipe1(
Left[int](errors.New("other error")),
recoverFromTimeout,
)
assert.True(t, IsLeft(result2))
})
t.Run("Transform error type in pipeline", func(t *testing.T) {
// Convert numeric error codes to descriptive strings
codeToMessage := ChainLeft(func(code int) Either[string, string] {
messages := map[int]string{
404: "Not Found",
500: "Internal Server Error",
}
if msg, ok := messages[code]; ok {
return Left[string](msg)
}
return Left[string](fmt.Sprintf("Unknown error: %d", code))
})
result := F.Pipe1(
Left[string](404),
codeToMessage,
)
assert.Equal(t, Left[string]("Not Found"), result)
})
t.Run("ChainLeft with Map combination", func(t *testing.T) {
// Combine ChainLeft with Map to handle both channels
errorHandler := ChainLeft(func(err error) Either[string, int] {
return Left[int]("Error: " + err.Error())
})
valueMapper := Map[string](S.Format[int]("Value: %d"))
// Test with Left
result1 := F.Pipe2(
Left[int](errors.New("fail")),
errorHandler,
valueMapper,
)
assert.Equal(t, Left[string]("Error: fail"), result1)
// Test with Right
result2 := F.Pipe2(
Right[error](42),
errorHandler,
valueMapper,
)
assert.Equal(t, Of[string]("Value: 42"), result2)
})
}

351
v2/either/filterable.go Normal file
View File

@@ -0,0 +1,351 @@
// 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 provides implementations of the Either type and related operations.
//
// This package implements several Fantasy Land algebraic structures:
// - Filterable: https://github.com/fantasyland/fantasy-land#filterable
//
// The Filterable specification defines operations for filtering and partitioning
// data structures based on predicates and mapping functions.
package either
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
)
// Partition separates an [Either] value into a [Pair] based on a predicate function.
// It returns a function that takes an Either and produces a Pair of Either values,
// where the first element contains values that fail the predicate and the second
// contains values that pass the predicate.
//
// This function implements the Filterable specification's partition operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, both elements of the resulting Pair will be the same Left value
// - If the input is Right and the predicate returns true, the result is (Left(empty), Right(value))
// - If the input is Right and the predicate returns false, the result is (Right(value), Left(empty))
//
// This function is useful for separating Either values into two categories based on
// a condition, commonly used in filtering operations where you want to keep track of
// both the values that pass and fail a test.
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default Left value to use when creating Left instances for partitioning
//
// Returns:
//
// A function that takes an Either[E, A] and returns a Pair where:
// - First element: Either values that fail the predicate (or original Left)
// - Second element: Either values that pass the predicate (or original Left)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// N "github.com/IBM/fp-go/v2/number"
// P "github.com/IBM/fp-go/v2/pair"
// )
//
// // Partition positive and non-positive numbers
// isPositive := N.MoreThan(0)
// partition := E.Partition(isPositive, "not positive")
//
// // Right value that passes predicate
// result1 := partition(E.Right[string](5))
// // result1 = Pair(Left("not positive"), Right(5))
// left1, right1 := P.Unpack(result1)
// // left1 = Left("not positive"), right1 = Right(5)
//
// // Right value that fails predicate
// result2 := partition(E.Right[string](-3))
// // result2 = Pair(Right(-3), Left("not positive"))
// left2, right2 := P.Unpack(result2)
// // left2 = Right(-3), right2 = Left("not positive")
//
// // Left value passes through unchanged in both positions
// result3 := partition(E.Left[int]("error"))
// // result3 = Pair(Left("error"), Left("error"))
// left3, right3 := P.Unpack(result3)
// // left3 = Left("error"), right3 = Left("error")
func Partition[E, A any](p Predicate[A], empty E) func(Either[E, A]) Pair[Either[E, A], Either[E, A]] {
l := Left[A](empty)
return func(e Either[E, A]) Pair[Either[E, A], Either[E, A]] {
if e.isLeft {
return pair.Of(e)
}
if p(e.r) {
return pair.MakePair(l, e)
}
return pair.MakePair(e, l)
}
}
// Filter creates a filtering operation for [Either] values based on a predicate function.
// It returns a function that takes an Either and produces an Either, where Right values
// that fail the predicate are converted to Left values with the provided empty value.
//
// This function implements the Filterable specification's filter operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, it passes through unchanged
// - If the input is Right and the predicate returns true, the Right value passes through unchanged
// - If the input is Right and the predicate returns false, it's converted to Left(empty)
//
// This function is useful for conditional validation or filtering of Either values,
// where you want to reject Right values that don't meet certain criteria by converting
// them to Left values with a default error.
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default Left value to use when filtering out Right values that fail the predicate
//
// Returns:
//
// An Operator function that takes an Either[E, A] and returns an Either[E, A] where:
// - Left values pass through unchanged
// - Right values that pass the predicate remain as Right
// - Right values that fail the predicate become Left(empty)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// N "github.com/IBM/fp-go/v2/number"
// )
//
// // Filter to keep only positive numbers
// isPositive := N.MoreThan(0)
// filterPositive := E.Filter(isPositive, "not positive")
//
// // Right value that passes predicate - remains Right
// result1 := filterPositive(E.Right[string](5))
// // result1 = Right(5)
//
// // Right value that fails predicate - becomes Left
// result2 := filterPositive(E.Right[string](-3))
// // result2 = Left("not positive")
//
// // Left value passes through unchanged
// result3 := filterPositive(E.Left[int]("original error"))
// // result3 = Left("original error")
//
// // Chaining filters
// isEven := func(n int) bool { return n%2 == 0 }
// filterEven := E.Filter(isEven, "not even")
//
// // Apply multiple filters in sequence
// result4 := filterEven(filterPositive(E.Right[string](4)))
// // result4 = Right(4) - passes both filters
//
// result5 := filterEven(filterPositive(E.Right[string](3)))
// // result5 = Left("not even") - passes first, fails second
func Filter[E, A any](p Predicate[A], empty E) Operator[E, A, A] {
l := Left[A](empty)
return func(e Either[E, A]) Either[E, A] {
if e.isLeft || p(e.r) {
return e
}
return l
}
}
// FilterMap combines filtering and mapping operations for [Either] values using an [Option]-returning function.
// It returns a function that takes an Either[E, A] and produces an Either[E, B], where Right values
// are transformed by applying the function f. If f returns Some(B), the result is Right(B). If f returns
// None, the result is Left(empty).
//
// This function implements the Filterable specification's filterMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, it passes through with its error value preserved as Left[B]
// - If the input is Right and f returns Some(B), the result is Right(B)
// - If the input is Right and f returns None, the result is Left(empty)
//
// This function is useful for operations that combine validation/filtering with transformation,
// such as parsing strings to numbers (where invalid strings result in None), or extracting
// optional fields from structures.
//
// Parameters:
// - f: An Option Kleisli function that transforms values of type A to Option[B]
// - empty: The default Left value to use when f returns None
//
// Returns:
//
// An Operator function that takes an Either[E, A] and returns an Either[E, B] where:
// - Left values pass through with error preserved
// - Right values are transformed by f: Some(B) becomes Right(B), None becomes Left(empty)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// O "github.com/IBM/fp-go/v2/option"
// "strconv"
// )
//
// // Parse string to int, filtering out invalid values
// parseInt := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
// filterMapInt := E.FilterMap(parseInt, "invalid number")
//
// // Valid number string - transforms to Right(int)
// result1 := filterMapInt(E.Right[string]("42"))
// // result1 = Right(42)
//
// // Invalid number string - becomes Left
// result2 := filterMapInt(E.Right[string]("abc"))
// // result2 = Left("invalid number")
//
// // Left value passes through with error preserved
// result3 := filterMapInt(E.Left[string]("original error"))
// // result3 = Left("original error")
//
// // Extract optional field from struct
// type Person struct {
// Name string
// Email O.Option[string]
// }
// extractEmail := func(p Person) O.Option[string] { return p.Email }
// filterMapEmail := E.FilterMap(extractEmail, "no email")
//
// result4 := filterMapEmail(E.Right[string](Person{Name: "Alice", Email: O.Some("alice@example.com")}))
// // result4 = Right("alice@example.com")
//
// result5 := filterMapEmail(E.Right[string](Person{Name: "Bob", Email: O.None[string]()}))
// // result5 = Left("no email")
func FilterMap[E, A, B any](f option.Kleisli[A, B], empty E) Operator[E, A, B] {
l := Left[B](empty)
return func(e Either[E, A]) Either[E, B] {
if e.isLeft {
return Left[B](e.l)
}
if b, ok := option.Unwrap(f(e.r)); ok {
return Right[E](b)
}
return l
}
}
// PartitionMap separates and transforms an [Either] value into a [Pair] of Either values using a mapping function.
// It returns a function that takes an Either[E, A] and produces a Pair of Either values, where the mapping
// function f transforms the Right value into Either[B, C]. The result is partitioned based on whether f
// produces a Left or Right value.
//
// This function implements the Filterable specification's partitionMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, both elements of the resulting Pair will be Left with the original error
// - If the input is Right and f returns Left(B), the result is (Right(B), Left(empty))
// - If the input is Right and f returns Right(C), the result is (Left(empty), Right(C))
//
// This function is useful for operations that need to categorize and transform values simultaneously,
// such as separating valid and invalid data while applying different transformations to each category.
//
// Parameters:
// - f: A Kleisli function that transforms values of type A to Either[B, C]
// - empty: The default error value to use when creating Left instances for partitioning
//
// Returns:
//
// A function that takes an Either[E, A] and returns a Pair[Either[E, B], Either[E, C]] where:
// - If input is Left: (Left(original_error), Left(original_error))
// - If f returns Left(B): (Right(B), Left(empty))
// - If f returns Right(C): (Left(empty), Right(C))
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// P "github.com/IBM/fp-go/v2/pair"
// )
//
// // Classify and transform numbers: negative -> error message, positive -> squared value
// classifyNumber := func(n int) E.Either[string, int] {
// if n < 0 {
// return E.Left[int]("negative: " + strconv.Itoa(n))
// }
// return E.Right[string](n * n)
// }
// partitionMap := E.PartitionMap(classifyNumber, "not classified")
//
// // Positive number - goes to right side as squared value
// result1 := partitionMap(E.Right[string](5))
// // result1 = Pair(Left("not classified"), Right(25))
// left1, right1 := P.Unpack(result1)
// // left1 = Left("not classified"), right1 = Right(25)
//
// // Negative number - goes to left side with error message
// result2 := partitionMap(E.Right[string](-3))
// // result2 = Pair(Right("negative: -3"), Left("not classified"))
// left2, right2 := P.Unpack(result2)
// // left2 = Right("negative: -3"), right2 = Left("not classified")
//
// // Original Left value - appears in both positions
// result3 := partitionMap(E.Left[int]("original error"))
// // result3 = Pair(Left("original error"), Left("original error"))
// left3, right3 := P.Unpack(result3)
// // left3 = Left("original error"), right3 = Left("original error")
//
// // Validate and transform user input
// type ValidationError struct{ Field, Message string }
// type User struct{ Name string; Age int }
//
// validateUser := func(input map[string]string) E.Either[ValidationError, User] {
// name, hasName := input["name"]
// ageStr, hasAge := input["age"]
// if !hasName {
// return E.Left[User](ValidationError{"name", "missing"})
// }
// if !hasAge {
// return E.Left[User](ValidationError{"age", "missing"})
// }
// age, err := strconv.Atoi(ageStr)
// if err != nil {
// return E.Left[User](ValidationError{"age", "invalid"})
// }
// return E.Right[ValidationError](User{name, age})
// }
// partitionUsers := E.PartitionMap(validateUser, ValidationError{"", "not processed"})
//
// validInput := map[string]string{"name": "Alice", "age": "30"}
// result4 := partitionUsers(E.Right[string](validInput))
// // result4 = Pair(Left(ValidationError{"", "not processed"}), Right(User{"Alice", 30}))
//
// invalidInput := map[string]string{"name": "Bob"}
// result5 := partitionUsers(E.Right[string](invalidInput))
// // result5 = Pair(Right(ValidationError{"age", "missing"}), Left(ValidationError{"", "not processed"}))
func PartitionMap[E, A, B, C any](f Kleisli[B, A, C], empty E) func(Either[E, A]) Pair[Either[E, B], Either[E, C]] {
return func(e Either[E, A]) Pair[Either[E, B], Either[E, C]] {
if e.isLeft {
return pair.MakePair(Left[B](e.l), Left[C](e.l))
}
res := f(e.r)
if res.isLeft {
return pair.MakePair(Right[E](res.l), Left[C](empty))
}
return pair.MakePair(Left[B](empty), Right[E](res.r))
}
}

1433
v2/either/filterable_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ import (
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -53,4 +54,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
Pair[L, R any] = pair.Pair[L, R]
)

View File

@@ -82,7 +82,30 @@ func Bind[S1, S2, T any](
)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
// Let attaches the result of a computation to a context [S1] to produce a context [S2].
// Similar to Bind, but uses the Functor's Map operation instead of the Monad's Chain.
// This is useful when you want to add a computed value to the context without needing
// the full power of monadic composition.
//
// Example:
//
// type State struct {
// X int
// Y int
// Sum int
// }
//
// result := F.Pipe2(
// identity.Do(State{X: 10, Y: 20}),
// identity.Let(
// func(sum int) func(State) State {
// return func(s State) State { s.Sum = sum; return s }
// },
// func(s State) int {
// return s.X + s.Y
// },
// ),
// ) // State{X: 10, Y: 20, Sum: 30}
func Let[S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
@@ -94,7 +117,27 @@ func Let[S1, S2, T any](
)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
// This is a specialized version of Let that doesn't require a computation function,
// useful when you want to add a known value to the context.
//
// Example:
//
// type State struct {
// X int
// Y int
// Constant string
// }
//
// result := F.Pipe2(
// identity.Do(State{X: 10, Y: 20}),
// identity.LetTo(
// func(c string) func(State) State {
// return func(s State) State { s.Constant = c; return s }
// },
// "fixed value",
// ),
// ) // State{X: 10, Y: 20, Constant: "fixed value"}
func LetTo[S1, S2, B any](
key func(B) func(S1) S2,
b B,
@@ -106,7 +149,31 @@ func LetTo[S1, S2, B any](
)
}
// BindTo initializes a new state [S1] from a value [T]
// BindTo initializes a new state [S1] from a value [T].
// This is typically used as the first operation in a do-notation chain to convert
// a plain value into a context that can be used with subsequent Bind operations.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
//
// result := F.Pipe2(
// 42,
// identity.BindTo(func(x int) State {
// return State{X: x}
// }),
// identity.Bind(
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) int {
// return s.X * 2
// },
// ),
// ) // State{X: 42, Y: 84}
func BindTo[S1, T any](
setter func(T) S1,
) func(T) S1 {

View File

@@ -29,7 +29,7 @@ func ExampleIOResult_do() {
bar := Of(1)
// quux consumes the state of three bindings and returns an [IO] instead of an [IOResult]
quux := func(t T.Tuple3[string, int, string]) IO[any] {
quux := func(t T.Tuple3[string, int, string]) IO[Void] {
return io.FromImpure(func() {
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
})

View File

@@ -45,7 +45,7 @@ func TestBuilderWithQuery(t *testing.T) {
ioresult.Map(func(r *http.Request) *url.URL {
return r.URL
}),
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[any] {
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[Void] {
return io.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))

View File

@@ -15,8 +15,12 @@
package builder
import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
)
type (
IOResult[A any] = ioresult.IOResult[A]
Void = function.Void
)

View File

@@ -76,7 +76,7 @@ func MakeClient(httpClient *http.Client) Client {
}
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
func ReadFullResponse(client Client) Operator[*http.Request, H.FullResponse] {
return F.Flow3(
client.Do,
ioresult.ChainEitherK(H.ValidateResponse),
@@ -101,7 +101,7 @@ func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
}
// ReadAll sends a request and reads the response as bytes
func ReadAll(client Client) Kleisli[Requester, []byte] {
func ReadAll(client Client) Operator[*http.Request, []byte] {
return F.Flow2(
ReadFullResponse(client),
ioresult.Map(H.Body),
@@ -109,7 +109,7 @@ func ReadAll(client Client) Kleisli[Requester, []byte] {
}
// ReadText sends a request, reads the response and represents the response as a text string
func ReadText(client Client) Kleisli[Requester, string] {
func ReadText(client Client) Operator[*http.Request, string] {
return F.Flow2(
ReadAll(client),
ioresult.Map(B.ToString),
@@ -117,7 +117,7 @@ func ReadText(client Client) Kleisli[Requester, string] {
}
// readJSON sends a request, reads the response and parses the response as a []byte
func readJSON(client Client) Kleisli[Requester, []byte] {
func readJSON(client Client) Operator[*http.Request, []byte] {
return F.Flow3(
ReadFullResponse(client),
ioresult.ChainFirstEitherK(F.Flow2(
@@ -129,7 +129,7 @@ func readJSON(client Client) Kleisli[Requester, []byte] {
}
// ReadJSON sends a request, reads the response and parses the response as JSON
func ReadJSON[A any](client Client) Kleisli[Requester, A] {
func ReadJSON[A any](client Client) Operator[*http.Request, A] {
return F.Flow2(
readJSON(client),
ioresult.ChainEitherK(J.Unmarshal[A]),

View File

@@ -7,9 +7,10 @@ import (
)
type (
IOResult[A any] = ioresult.IOResult[A]
Kleisli[A, B any] = ioresult.Kleisli[A, B]
Requester = IOResult[*http.Request]
IOResult[A any] = ioresult.IOResult[A]
Kleisli[A, B any] = ioresult.Kleisli[A, B]
Operator[A, B any] = ioresult.Operator[A, B]
Requester = IOResult[*http.Request]
Client interface {
Do(Requester) IOResult[*http.Response]

View File

@@ -664,11 +664,11 @@ func WithResource[A, R, ANY any](
// FromImpure converts an impure side-effecting function into an IOResult.
// The function is executed when the IOResult runs, and always succeeds with nil.
func FromImpure(f func()) IOResult[any] {
func FromImpure(f func()) IOResult[Void] {
return function.Pipe2(
f,
io.FromImpure,
FromIO[any],
FromIO[Void],
)
}

View File

@@ -2,6 +2,7 @@ package ioresult
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/predicate"
@@ -39,4 +40,6 @@ type (
Operator[A, B any] = Kleisli[IOResult[A], B]
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
)

View File

@@ -19,6 +19,31 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// MonadSequenceSegment sequences a segment of an array of effects using a divide-and-conquer approach.
// It recursively splits the array segment in half, sequences each half, and concatenates the results.
//
// This function is optimized for performance by using a divide-and-conquer strategy that reduces
// the depth of nested function calls compared to a linear fold approach.
//
// Type parameters:
// - HKTB: The higher-kinded type containing values (e.g., Option[B], Either[E, B])
// - HKTRB: The higher-kinded type containing an array of values (e.g., Option[[]B], Either[E, []B])
//
// Parameters:
// - fof: Function to lift a single HKTB into HKTRB
// - empty: The empty/identity value for HKTRB
// - concat: Function to concatenate two HKTRB values
// - fbs: The array of effects to sequence
// - start: The starting index of the segment (inclusive)
// - end: The ending index of the segment (exclusive)
//
// Returns:
// - HKTRB: The sequenced result for the segment
//
// The function handles three cases:
// - Empty segment (end - start == 0): returns empty
// - Single element (end - start == 1): returns fof(fbs[start])
// - Multiple elements: recursively divides and conquers
func MonadSequenceSegment[HKTB, HKTRB any](
fof func(HKTB) HKTRB,
empty HKTRB,
@@ -41,6 +66,23 @@ func MonadSequenceSegment[HKTB, HKTRB any](
}
}
// SequenceSegment creates a function that sequences a segment of an array of effects.
// Unlike MonadSequenceSegment, this returns a curried function that can be reused.
//
// This function builds a computation tree at construction time, which can be more efficient
// when the same sequencing pattern needs to be applied multiple times to arrays of the same length.
//
// Type parameters:
// - HKTB: The higher-kinded type containing values
// - HKTRB: The higher-kinded type containing an array of values
//
// Parameters:
// - fof: Function to lift a single HKTB into HKTRB
// - empty: The empty/identity value for HKTRB
// - concat: Function to concatenate two HKTRB values
//
// Returns:
// - A function that takes an array of HKTB and returns HKTRB
func SequenceSegment[HKTB, HKTRB any](
fof func(HKTB) HKTRB,
empty HKTRB,
@@ -85,14 +127,39 @@ func SequenceSegment[HKTB, HKTRB any](
}
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverse maps each element of an array to an effect, then sequences the results.
// This is the monadic version that takes the array as a direct parameter.
//
// Traverse combines mapping and sequencing in one operation. It's useful when you want to
// transform each element of an array into an effect (like Option, Either, IO, etc.) and
// then collect all those effects into a single effect containing an array.
//
// We need to pass the members of the applicative explicitly, because golang does neither
// support higher kinded types nor template methods on structs or interfaces.
//
// Type parameters:
// - GA: The input array type (e.g., []A)
// - GB: The output array type (e.g., []B)
// - A: The input element type
// - B: The output element type
// - HKTB: HKT<B> - The effect containing B (e.g., Option[B])
// - HKTAB: HKT<func(B)GB> - Intermediate applicative type
// - HKTRB: HKT<GB> - The effect containing the result array (e.g., Option[[]B])
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - ta: The input array to traverse
// - f: The function to apply to each element, producing an effect
//
// Returns:
// - HKTRB: An effect containing the array of transformed values
//
// Example:
//
// If any element produces None, the entire result is None.
// If all elements produce Some, the result is Some containing all values.
func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -103,14 +170,20 @@ func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
return MonadTraverseReduce(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverseWithIndex is like MonadTraverse but the transformation function also receives the index.
// This is useful when the transformation depends on the element's position in the array.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - ta: The input array to traverse
// - f: The function to apply to each element with its index, producing an effect
//
// Returns:
// - HKTRB: An effect containing the array of transformed values
func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -121,6 +194,19 @@ func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
}
// Traverse creates a curried function that maps each element to an effect and sequences the results.
// This is the curried version of MonadTraverse, useful for partial application and composition.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - f: The function to apply to each element, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the transformed array
func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -133,6 +219,19 @@ func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
}
}
// TraverseWithIndex creates a curried function like Traverse but with index-aware transformation.
// This is the curried version of MonadTraverseWithIndex.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - f: The function to apply to each element with its index, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the transformed array
func TraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -231,6 +330,16 @@ func TraverseReduce[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
}
}
// TraverseReduceWithIndex creates a curried function for index-aware custom reduction during traversal.
// This is the curried version of MonadTraverseReduceWithIndex.
//
// Type parameters: Same as MonadTraverseReduce
//
// Parameters: Same as TraverseReduce, except:
// - transform: Function that takes index and element, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the accumulated value
func TraverseReduceWithIndex[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,

View File

@@ -1,10 +1,60 @@
// Copyright (c) 2024 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 iter provides functional programming utilities for working with Go 1.23+ iterators.
// It offers operations for reducing, mapping, concatenating, and transforming iterator sequences
// in a functional style, compatible with the range-over-func pattern.
package iter
import (
"slices"
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
func From[A any](as ...A) Seq[A] {
return slices.Values(as)
}
// MonadReduceWithIndex reduces an iterator sequence to a single value using a reducer function
// that receives the current index, accumulated value, and current element.
//
// The function iterates through all elements in the sequence, applying the reducer function
// at each step with the element's index. This is useful when the position of elements matters
// in the reduction logic.
//
// Parameters:
// - fa: The iterator sequence to reduce
// - f: The reducer function that takes (index, accumulator, element) and returns the new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - The final accumulated value after processing all elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(10)
// yield(20)
// yield(30)
// }
// // Sum with index multiplier: 0*10 + 1*20 + 2*30 = 80
// result := MonadReduceWithIndex(iter, func(i, acc, val int) int {
// return acc + i*val
// }, 0)
func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(int, B, A) B, initial B) B {
current := initial
var i int
@@ -15,6 +65,29 @@ func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(
return current
}
// MonadReduce reduces an iterator sequence to a single value using a reducer function.
//
// This is similar to MonadReduceWithIndex but without index tracking, making it more
// efficient when the position of elements is not needed in the reduction logic.
//
// Parameters:
// - fa: The iterator sequence to reduce
// - f: The reducer function that takes (accumulator, element) and returns the new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - The final accumulated value after processing all elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// sum := MonadReduce(iter, func(acc, val int) int {
// return acc + val
// }, 0) // Returns: 6
func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B, initial B) B {
current := initial
for a := range fa {
@@ -23,7 +96,30 @@ func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B,
return current
}
// Concat concatenates two sequences, yielding all elements from left followed by all elements from right.
// Concat concatenates two iterator sequences, yielding all elements from left followed by all elements from right.
//
// The resulting iterator will first yield all elements from the left sequence, then all elements
// from the right sequence. If the consumer stops early (yield returns false), iteration stops
// immediately without processing remaining elements.
//
// Parameters:
// - left: The first iterator sequence
// - right: The second iterator sequence
//
// Returns:
// - A new iterator that yields elements from both sequences in order
//
// Example:
//
// left := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// right := func(yield func(int) bool) {
// yield(3)
// yield(4)
// }
// combined := Concat(left, right) // Yields: 1, 2, 3, 4
func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
return func(yield func(T) bool) {
for t := range left {
@@ -39,28 +135,129 @@ func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
}
}
// Of creates an iterator sequence containing a single element.
//
// This is the unit/return operation for the iterator monad, lifting a single value
// into the iterator context.
//
// Parameters:
// - a: The element to wrap in an iterator
//
// Returns:
// - An iterator that yields exactly one element
//
// Example:
//
// iter := Of[func(yield func(int) bool)](42)
// // Yields: 42
func Of[GA ~func(yield func(A) bool), A any](a A) GA {
return func(yield func(A) bool) {
yield(a)
}
}
// MonadAppend appends a single element to the end of an iterator sequence.
//
// This creates a new iterator that yields all elements from the original sequence
// followed by the tail element.
//
// Parameters:
// - f: The original iterator sequence
// - tail: The element to append
//
// Returns:
// - A new iterator with the tail element appended
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := MonadAppend(iter, 3) // Yields: 1, 2, 3
func MonadAppend[GA ~func(yield func(A) bool), A any](f GA, tail A) GA {
return Concat(f, Of[GA](tail))
}
// Append returns a function that appends a single element to the end of an iterator sequence.
//
// This is the curried version of MonadAppend, useful for partial application and composition.
//
// Parameters:
// - tail: The element to append
//
// Returns:
// - A function that takes an iterator and returns a new iterator with the tail element appended
//
// Example:
//
// appendThree := Append[func(yield func(int) bool)](3)
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := appendThree(iter) // Yields: 1, 2, 3
func Append[GA ~func(yield func(A) bool), A any](tail A) func(GA) GA {
return F.Bind2nd(Concat[GA], Of[GA](tail))
}
// Prepend returns a function that prepends a single element to the beginning of an iterator sequence.
//
// This is the curried version for prepending, useful for partial application and composition.
//
// Parameters:
// - head: The element to prepend
//
// Returns:
// - A function that takes an iterator and returns a new iterator with the head element prepended
//
// Example:
//
// prependZero := Prepend[func(yield func(int) bool)](0)
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := prependZero(iter) // Yields: 0, 1, 2
func Prepend[GA ~func(yield func(A) bool), A any](head A) func(GA) GA {
return F.Bind1st(Concat[GA], Of[GA](head))
}
// Empty creates an empty iterator sequence that yields no elements.
//
// This is the identity element for the Concat operation and represents an empty collection
// in the iterator context.
//
// Returns:
// - An iterator that yields no elements
//
// Example:
//
// iter := Empty[func(yield func(int) bool), int]()
// // Yields nothing
func Empty[GA ~func(yield func(A) bool), A any]() GA {
return func(_ func(A) bool) {}
}
// ToArray collects all elements from an iterator sequence into a slice.
//
// This eagerly evaluates the entire iterator sequence and materializes all elements
// into memory as a slice.
//
// Parameters:
// - fa: The iterator sequence to collect
//
// Returns:
// - A slice containing all elements from the iterator
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// arr := ToArray[func(yield func(int) bool), []int](iter) // Returns: []int{1, 2, 3}
func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
bs := make(GB, 0)
for a := range fa {
@@ -69,6 +266,28 @@ func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
return bs
}
// MonadMapToArray maps each element of an iterator sequence through a function and collects the results into a slice.
//
// This combines mapping and collection into a single operation, eagerly evaluating the entire
// iterator sequence and materializing the transformed elements into memory.
//
// Parameters:
// - fa: The iterator sequence to map and collect
// - f: The mapping function to apply to each element
//
// Returns:
// - A slice containing the mapped elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// doubled := MonadMapToArray[func(yield func(int) bool), []int](iter, func(x int) int {
// return x * 2
// }) // Returns: []int{2, 4, 6}
func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(A) B) GB {
bs := make(GB, 0)
for a := range fa {
@@ -77,10 +296,54 @@ func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f f
return bs
}
// MapToArray returns a function that maps each element through a function and collects the results into a slice.
//
// This is the curried version of MonadMapToArray, useful for partial application and composition.
//
// Parameters:
// - f: The mapping function to apply to each element
//
// Returns:
// - A function that takes an iterator and returns a slice of mapped elements
//
// Example:
//
// double := MapToArray[func(yield func(int) bool), []int](func(x int) int {
// return x * 2
// })
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := double(iter) // Returns: []int{2, 4}
func MapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(A) B) func(GA) GB {
return F.Bind2nd(MonadMapToArray[GA, GB], f)
}
// MonadMapToArrayWithIndex maps each element of an iterator sequence through a function that receives
// the element's index, and collects the results into a slice.
//
// This is similar to MonadMapToArray but the mapping function also receives the zero-based index
// of each element, useful when the position matters in the transformation logic.
//
// Parameters:
// - fa: The iterator sequence to map and collect
// - f: The mapping function that takes (index, element) and returns the transformed element
//
// Returns:
// - A slice containing the mapped elements
//
// Example:
//
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// yield("c")
// }
// indexed := MonadMapToArrayWithIndex[func(yield func(string) bool), []string](iter,
// func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// }) // Returns: []string{"0:a", "1:b", "2:c"}
func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(int, A) B) GB {
bs := make(GB, 0)
var i int
@@ -91,10 +354,49 @@ func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f
return bs
}
// MapToArrayWithIndex returns a function that maps each element through an indexed function
// and collects the results into a slice.
//
// This is the curried version of MonadMapToArrayWithIndex, useful for partial application and composition.
//
// Parameters:
// - f: The mapping function that takes (index, element) and returns the transformed element
//
// Returns:
// - A function that takes an iterator and returns a slice of mapped elements
//
// Example:
//
// addIndex := MapToArrayWithIndex[func(yield func(string) bool), []string](
// func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// })
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// }
// result := addIndex(iter) // Returns: []string{"0:a", "1:b"}
func MapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
return F.Bind2nd(MonadMapToArrayWithIndex[GA, GB], f)
}
// Monoid returns a Monoid instance for iterator sequences.
//
// The monoid uses Concat as the binary operation and Empty as the identity element,
// allowing iterator sequences to be combined in an associative way with a neutral element.
// This enables generic operations that work with any monoid, such as folding a collection
// of iterators into a single iterator.
//
// Returns:
// - A Monoid instance with Concat and Empty operations
//
// Example:
//
// m := Monoid[func(yield func(int) bool), int]()
// iter1 := func(yield func(int) bool) { yield(1); yield(2) }
// iter2 := func(yield func(int) bool) { yield(3); yield(4) }
// combined := m.Concat(iter1, iter2) // Yields: 1, 2, 3, 4
// empty := m.Empty() // Yields nothing
func Monoid[GA ~func(yield func(A) bool), A any]() M.Monoid[GA] {
return M.MakeMonoid(Concat[GA], Empty[GA]())
}

View File

@@ -21,18 +21,50 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverse traverses an iterator sequence, applying an effectful function to each element
// and collecting the results in an applicative context.
//
// This is a fundamental operation in functional programming that allows you to "turn inside out"
// a structure containing effects. It maps each element through a function that produces an effect,
// then sequences all those effects together while preserving the iterator structure.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The output iterator type ~func(yield func(B) bool)
// - A: The input element type
// - B: The output element type
// - HKT_B: The higher-kinded type representing an effect containing B
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
// - HKT_GB: The higher-kinded type representing an effect containing GB (the result iterator)
//
// Parameters:
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
// - fof_gb: Lifts a GB value into the effect context (pure/of operation)
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
// - fap_gb: Applies an effectful function to an effectful value (ap operation)
// - ta: The input iterator sequence to traverse
// - f: The effectful function to apply to each element
//
// Returns:
// - An effect containing an iterator of transformed elements
//
// Note: We need to pass the applicative operations explicitly because Go doesn't support
// higher-kinded types or template methods on structs/interfaces.
//
// Example (conceptual with Option):
//
// // Traverse an iterator of strings, parsing each as an integer
// // If any parse fails, the whole result is None
// iter := func(yield func(string) bool) {
// yield("1")
// yield("2")
// yield("3")
// }
// result := MonadTraverse(..., iter, parseInt) // Some(iterator of [1,2,3]) or None
func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
fmap_b func(HKT_B, func(B) GB) HKT_GB,
fof_gb func(GB) HKT_GB,
fof_gb OfType[GB, HKT_GB],
fmap_gb func(HKT_GB, func(GB) func(GB) GB) HKT_GB_GB,
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
@@ -54,14 +86,43 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
}
// Traverse is the curried version of MonadTraverse, returning a function that traverses an iterator.
//
// This version uses type aliases for better readability and is more suitable for partial application
// and function composition. It returns a Kleisli arrow (a function from GA to HKT_GB).
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The output iterator type ~func(yield func(B) bool)
// - A: The input element type
// - B: The output element type
// - HKT_B: The higher-kinded type representing an effect containing B
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
// - HKT_GB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
// - fof_gb: Lifts a GB value into the effect context
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
// - fap_gb: Applies an effectful function to an effectful value
// - f: The effectful function to apply to each element (Kleisli arrow)
//
// Returns:
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// parseInts := Traverse[...](fmap, fof, fmap_gb, fap, parseInt)
// iter := func(yield func(string) bool) { yield("1"); yield("2") }
// result := parseInts(iter) // Effect containing iterator of integers
func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
fmap_b func(func(B) GB) func(HKT_B) HKT_GB,
fmap_b MapType[B, GB, HKT_B, HKT_GB],
fof_gb func(GB) HKT_GB,
fmap_gb func(func(GB) func(GB) GB) func(HKT_GB) HKT_GB_GB,
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
fof_gb OfType[GB, HKT_GB],
fmap_gb MapType[GB, Endomorphism[GB], HKT_GB, HKT_GB_GB],
fap_gb ApType[HKT_GB, HKT_GB, HKT_GB_GB],
f func(A) HKT_B) func(GA) HKT_GB {
f Kleisli[A, HKT_B]) Kleisli[GA, HKT_GB] {
fof := fmap_b(Of[GB])
empty := fof_gb(Empty[GB]())
@@ -69,18 +130,50 @@ func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B,
concat_gb := fmap_gb(cb)
concat := func(first, second HKT_GB) HKT_GB {
return fap_gb(concat_gb(first), second)
return fap_gb(second)(concat_gb(first))
}
return func(ma GA) HKT_GB {
// return INTA.SequenceSegment(fof, empty, concat)(MapToArray[GA, []HKT_B](f)(ma))
hktb := MonadMapToArray[GA, []HKT_B](ma, f)
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
}
return F.Flow2(
MapToArray[GA, []HKT_B](f),
INTA.SequenceSegment(fof, empty, concat),
)
}
// MonadSequence sequences an iterator of effects into an effect containing an iterator.
//
// This is a special case of traverse where the transformation function is the identity.
// It "flips" the nesting of the iterator and effect types, collecting all effects into
// a single effect containing an iterator of values.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(HKTA) bool)
// - HKTA: The higher-kinded type representing an effect containing A
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
//
// Parameters:
// - fof: Lifts an HKTA value into the HKTRA context
// - m: A monoid for combining HKTRA values
// - ta: The input iterator of effects to sequence
//
// Returns:
// - An effect containing an iterator of values
//
// Example (conceptual with Option):
//
// iter := func(yield func(Option[int]) bool) {
// yield(Some(1))
// yield(Some(2))
// yield(Some(3))
// }
// result := MonadSequence(..., iter) // Some(iterator of [1,2,3])
//
// iter2 := func(yield func(Option[int]) bool) {
// yield(Some(1))
// yield(None)
// }
// result2 := MonadSequence(..., iter2) // None
func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
fof func(HKTA) HKTRA,
fof OfType[HKTA, HKTRA],
m M.Monoid[HKTRA],
ta GA) HKTRA {
@@ -90,14 +183,37 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
// function to each element along with its index.
//
// This is similar to MonadTraverse but the transformation function receives both the element's
// zero-based index and the element itself, useful when the position matters in the transformation.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - A: The input element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
//
// Parameters:
// - fof: Lifts an HKTB value into the HKTRB context
// - m: A monoid for combining HKTRB values
// - ta: The input iterator sequence to traverse
// - f: The effectful function that takes (index, element) and returns an effect
//
// Returns:
// - An effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// }
// // Add index prefix to each element
// result := MonadTraverseWithIndex(..., iter, func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// }) // Effect containing iterator of ["0:a", "1:b"]
func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
fof func(HKTB) HKTRB,
m M.Monoid[HKTRB],
@@ -110,8 +226,29 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
}
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(HKTA) bool)
// - HKTA: The higher-kinded type representing an effect containing A
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
//
// Parameters:
// - fof: Lifts an HKTA value into the HKTRA context
// - m: A monoid for combining HKTRA values
//
// Returns:
// - A function that takes an iterator of effects and returns an effect containing an iterator
//
// Example (conceptual):
//
// sequenceOptions := Sequence[...](fof, monoid)
// iter := func(yield func(Option[int]) bool) { yield(Some(1)); yield(Some(2)) }
// result := sequenceOptions(iter) // Some(iterator of [1,2])
func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
fof func(HKTA) HKTRA,
fof OfType[HKTA, HKTRA],
m M.Monoid[HKTRA]) func(GA) HKTRA {
return func(ma GA) HKTRA {
@@ -119,6 +256,32 @@ func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
}
}
// TraverseWithIndex is the curried version of MonadTraverseWithIndex, returning a function that
// traverses an iterator with index tracking.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - A: The input element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
//
// Parameters:
// - fof: Lifts an HKTB value into the HKTRB context
// - m: A monoid for combining HKTRB values
// - f: The effectful function that takes (index, element) and returns an effect
//
// Returns:
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// addIndexPrefix := TraverseWithIndex[...](fof, monoid, func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// })
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
// result := addIndexPrefix(iter) // Effect containing iterator of ["0:a", "1:b"]
func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
fof func(HKTB) HKTRB,
m M.Monoid[HKTRB],
@@ -130,6 +293,39 @@ func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
}
}
// MonadTraverseReduce combines traversal with reduction, applying an effectful transformation
// and accumulating results using a reducer function.
//
// This is a more efficient operation when you want to both transform elements through effects
// and reduce them to a single accumulated value, avoiding intermediate collections.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - ta: The input iterator sequence to traverse and reduce
// - transform: The effectful function to apply to each element
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - An effect containing the final accumulated value
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
// // Parse strings to ints and sum them
// result := MonadTraverseReduce(..., iter, parseInt, add, 0)
// // Returns: Some(6) or None if any parse fails
func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -152,6 +348,44 @@ func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HK
}, fof(initial))
}
// MonadTraverseReduceWithIndex combines indexed traversal with reduction, applying an effectful
// transformation that receives element indices and accumulating results using a reducer function.
//
// This is similar to MonadTraverseReduce but the transformation function also receives the
// zero-based index of each element, useful when position matters in the transformation logic.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - ta: The input iterator sequence to traverse and reduce
// - transform: The effectful function that takes (index, element) and returns an effect
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - An effect containing the final accumulated value
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) { yield("a"); yield("b"); yield("c") }
// // Create indexed strings and concatenate
// result := MonadTraverseReduceWithIndex(..., iter,
// func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// },
// func(acc, s string) string { return acc + "," + s },
// "")
// // Returns: Effect containing "0:a,1:b,2:c"
func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -174,6 +408,36 @@ func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB,
}, fof(initial))
}
// TraverseReduce is the curried version of MonadTraverseReduce, returning a function that
// traverses and reduces an iterator.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - transform: The effectful function to apply to each element
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - A function that takes an iterator and returns an effect containing the accumulated value
//
// Example (conceptual):
//
// sumParsedInts := TraverseReduce[...](fof, fmap, fap, parseInt, add, 0)
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
// result := sumParsedInts(iter) // Some(6) or None if any parse fails
func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -188,6 +452,41 @@ func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB a
}
}
// TraverseReduceWithIndex is the curried version of MonadTraverseReduceWithIndex, returning a
// function that traverses and reduces an iterator with index tracking.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - transform: The effectful function that takes (index, element) and returns an effect
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - A function that takes an iterator and returns an effect containing the accumulated value
//
// Example (conceptual):
//
// concatIndexed := TraverseReduceWithIndex[...](fof, fmap, fap,
// func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// },
// func(acc, s string) string { return acc + "," + s },
// "")
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
// result := concatIndexed(iter) // Effect containing "0:a,1:b"
func TraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,

View File

@@ -2,10 +2,23 @@ package iter
import (
I "iter"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
)
type (
// Seq represents Go's standard library iterator type for single values.
// It's an alias for iter.Seq[A] and provides interoperability with Go 1.23+ range-over-func.
Seq[A any] = I.Seq[A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
OfType[A, HKT_A any] = pointed.OfType[A, HKT_A]
MapType[A, B, HKT_A, HKT_B any] = functor.MapType[A, B, HKT_A, HKT_B]
ApType[HKT_A, HKT_B, HKT_AB any] = apply.ApType[HKT_A, HKT_B, HKT_AB]
Kleisli[A, HKT_B any] = func(A) HKT_B
)

View File

@@ -247,7 +247,7 @@ func TestBracket(t *testing.T) {
return Of(x * 2)
}
release := func(x int, result int) IO[any] {
release := func(x int, result int) IO[Void] {
return FromImpure(func() {
released = true
})
@@ -271,7 +271,7 @@ func TestWithResource(t *testing.T) {
return 42
}
onRelease := func(x int) IO[any] {
onRelease := func(x int) IO[Void] {
return FromImpure(func() {
released = true
})

View File

@@ -18,6 +18,7 @@ package io
import (
"time"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
@@ -31,11 +32,6 @@ const (
useParallel = true
)
var (
// undefined represents an undefined value
undefined = struct{}{}
)
// Of wraps a pure value in an IO context, creating a computation that returns that value.
// This is the monadic return operation for IO.
//
@@ -58,10 +54,10 @@ func FromIO[A any](a IO[A]) IO[A] {
}
// FromImpure converts a side effect without a return value into a side effect that returns any
func FromImpure[ANY ~func()](f ANY) IO[any] {
return func() any {
func FromImpure[ANY ~func()](f ANY) IO[Void] {
return func() Void {
f()
return undefined
return function.VOID
}
}

View File

@@ -61,18 +61,105 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
)
}
// TraverseIter applies an IO-returning function to each element of an iterator sequence
// and collects the results into an IO of an iterator sequence. Executes in parallel by default.
//
// This function is useful for processing lazy sequences where each element requires an IO operation.
// The resulting iterator is also lazy and will only execute IO operations when iterated.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: A function that takes an element of type A and returns an IO computation producing B
//
// Returns:
// - A function that takes an iterator sequence of A and returns an IO of an iterator sequence of B
//
// Example:
//
// // Fetch user data for each ID in a sequence
// fetchUser := func(id int) io.IO[User] {
// return func() User {
// // Simulate fetching user from database
// return User{ID: id, Name: fmt.Sprintf("User%d", id)}
// }
// }
//
// // Create an iterator of user IDs
// userIDs := func(yield func(int) bool) {
// for _, id := range []int{1, 2, 3, 4, 5} {
// if !yield(id) { return }
// }
// }
//
// // Traverse the iterator, fetching each user
// fetchUsers := io.TraverseIter(fetchUser)
// usersIO := fetchUsers(userIDs)
//
// // Execute the IO to get the iterator of users
// users := usersIO()
// for user := range users {
// fmt.Printf("User: %v\n", user)
// }
func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
return INTI.Traverse[Seq[A]](
Map[B],
Of[Seq[B]],
Map[Seq[B]],
MonadAp[Seq[B]],
Ap[Seq[B]],
f,
)
}
// SequenceIter converts an iterator sequence of IO computations into an IO of an iterator sequence of results.
// All computations are executed in parallel by default when the resulting IO is invoked.
//
// This is a special case of TraverseIter where the transformation function is the identity.
// It "flips" the nesting of the iterator and IO types, executing all IO operations and collecting
// their results into a lazy iterator.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: An iterator sequence where each element is an IO computation
//
// Returns:
// - An IO computation that, when executed, produces an iterator sequence of results
//
// Example:
//
// // Create an iterator of IO operations
// operations := func(yield func(io.IO[int]) bool) {
// yield(func() int { return 1 })
// yield(func() int { return 2 })
// yield(func() int { return 3 })
// }
//
// // Sequence the operations
// resultsIO := io.SequenceIter(operations)
//
// // Execute all IO operations and get the iterator of results
// results := resultsIO()
// for result := range results {
// fmt.Printf("Result: %d\n", result)
// }
//
// Note: The IO operations are executed when resultsIO() is called, not when iterating
// over the results. The resulting iterator is lazy but the computations have already
// been performed.
func SequenceIter[A any](as Seq[IO[A]]) IO[Seq[A]] {
return INTI.MonadSequence(
Map(INTI.Of[Seq[A]]),
ApplicativeMonoid(INTI.Monoid[Seq[A]]()),
as,
)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index.
// Executes in parallel by default.
//

View File

@@ -1,9 +1,12 @@
package io
import (
"fmt"
"slices"
"strings"
"testing"
A "github.com/IBM/fp-go/v2/array"
"github.com/stretchr/testify/assert"
)
@@ -36,3 +39,265 @@ func TestTraverseCustomSlice(t *testing.T) {
assert.Equal(t, res(), []string{"A", "B"})
}
func TestTraverseIter(t *testing.T) {
t.Run("transforms all elements successfully", func(t *testing.T) {
// Create an iterator of strings
input := slices.Values(A.From("hello", "world", "test"))
// Transform each string to uppercase
transform := func(s string) IO[string] {
return Of(strings.ToUpper(s))
}
// Traverse the iterator
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
// Execute the IO and collect results
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Equal(t, []string{"HELLO", "WORLD", "TEST"}, collected)
})
t.Run("works with empty iterator", func(t *testing.T) {
// Create an empty iterator
input := func(yield func(string) bool) {}
transform := func(s string) IO[string] {
return Of(strings.ToUpper(s))
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Empty(t, collected)
})
t.Run("works with single element", func(t *testing.T) {
input := func(yield func(int) bool) {
yield(42)
}
transform := func(n int) IO[int] {
return Of(n * 2)
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{84}, collected)
})
t.Run("preserves order of elements", func(t *testing.T) {
input := func(yield func(int) bool) {
for i := 1; i <= 5; i++ {
if !yield(i) {
return
}
}
}
transform := func(n int) IO[string] {
return Of(fmt.Sprintf("item-%d", n))
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
expected := []string{"item-1", "item-2", "item-3", "item-4", "item-5"}
assert.Equal(t, expected, collected)
})
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
input := func(yield func(int) bool) {
for _, id := range []int{1, 2, 3} {
if !yield(id) {
return
}
}
}
transform := func(id int) IO[User] {
return Of(User{ID: id, Name: fmt.Sprintf("User%d", id)})
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []User
for user := range result {
collected = append(collected, user)
}
expected := []User{
{ID: 1, Name: "User1"},
{ID: 2, Name: "User2"},
{ID: 3, Name: "User3"},
}
assert.Equal(t, expected, collected)
})
}
func TestSequenceIter(t *testing.T) {
t.Run("sequences multiple IO operations", func(t *testing.T) {
// Create an iterator of IO operations
input := slices.Values(A.From(Of(1), Of(2), Of(3)))
// Sequence the operations
resultIO := SequenceIter(input)
// Execute and collect results
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{1, 2, 3}, collected)
})
t.Run("works with empty iterator", func(t *testing.T) {
input := slices.Values(A.Empty[IO[string]]())
resultIO := SequenceIter(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Empty(t, collected)
})
// TODO!!
// t.Run("executes all IO operations", func(t *testing.T) {
// // Track execution order
// var executed []int
// input := func(yield func(IO[int]) bool) {
// yield(func() int {
// executed = append(executed, 1)
// return 10
// })
// yield(func() int {
// executed = append(executed, 2)
// return 20
// })
// yield(func() int {
// executed = append(executed, 3)
// return 30
// })
// }
// resultIO := SequenceIter(input)
// // Before execution, nothing should be executed
// assert.Empty(t, executed)
// // Execute the IO
// result := resultIO()
// // Collect results
// var collected []int
// for n := range result {
// collected = append(collected, n)
// }
// // All operations should have been executed
// assert.Equal(t, []int{1, 2, 3}, executed)
// assert.Equal(t, []int{10, 20, 30}, collected)
// })
t.Run("works with single IO operation", func(t *testing.T) {
input := func(yield func(IO[string]) bool) {
yield(Of("hello"))
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Equal(t, []string{"hello"}, collected)
})
t.Run("preserves order of results", func(t *testing.T) {
input := func(yield func(IO[int]) bool) {
for i := 5; i >= 1; i-- {
n := i // capture loop variable
yield(func() int { return n * 10 })
}
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{50, 40, 30, 20, 10}, collected)
})
t.Run("works with complex types", func(t *testing.T) {
type Result struct {
Value int
Label string
}
input := func(yield func(IO[Result]) bool) {
yield(Of(Result{Value: 1, Label: "first"}))
yield(Of(Result{Value: 2, Label: "second"}))
yield(Of(Result{Value: 3, Label: "third"}))
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []Result
for r := range result {
collected = append(collected, r)
}
expected := []Result{
{Value: 1, Label: "first"},
{Value: 2, Label: "second"},
{Value: 3, Label: "third"},
}
assert.Equal(t, expected, collected)
})
}

View File

@@ -29,7 +29,7 @@ func ExampleIOEither_do() {
bar := Of[error](1)
// quux consumes the state of three bindings and returns an [IO] instead of an [IOEither]
quux := func(t T.Tuple3[string, int, string]) IO[any] {
quux := func(t T.Tuple3[string, int, string]) IO[Void] {
return io.FromImpure(func() {
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
})

View File

@@ -45,7 +45,7 @@ func TestBuilderWithQuery(t *testing.T) {
ioeither.Map[error](func(r *http.Request) *url.URL {
return r.URL
}),
ioeither.ChainFirstIOK[error](func(u *url.URL) io.IO[any] {
ioeither.ChainFirstIOK[error](func(u *url.URL) io.IO[Void] {
return io.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))

View File

@@ -15,8 +15,12 @@
package builder
import "github.com/IBM/fp-go/v2/ioeither"
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
)
type (
IOEither[A any] = ioeither.IOEither[error, A]
Void = function.Void
)

View File

@@ -428,11 +428,11 @@ func Swap[E, A any](val IOEither[E, A]) IOEither[A, E] {
}
// FromImpure converts a side effect without a return value into an [IOEither] that returns any
func FromImpure[E any](f func()) IOEither[E, any] {
func FromImpure[E any](f func()) IOEither[E, Void] {
return function.Pipe2(
f,
io.FromImpure,
FromIO[E, any],
FromIO[E, Void],
)
}
@@ -489,6 +489,8 @@ func After[E, A any](timestamp time.Time) Operator[E, A, A] {
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is useful for error recovery or error transformation scenarios where you want to handle
// errors by performing another computation that may also fail.
//
@@ -523,6 +525,8 @@ func MonadChainLeft[EA, EB, A any](fa IOEither[EA, A], f Kleisli[EB, EA, A]) IOE
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of an [IOEither].
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is particularly useful in functional composition pipelines where you want to handle
// errors by performing another computation that may also fail.
//
@@ -644,6 +648,8 @@ func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
// If the IOEither is Left, it applies the provided function to the error value,
// which returns a new IOEither that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
// The error type can be widened from E1 to E2, allowing transformation of error types.
//

View File

@@ -490,3 +490,148 @@ func TestOrElseW(t *testing.T) {
preserved := preserveRecover(preservedRight)()
assert.Equal(t, E.Right[AppError](42), preserved)
}
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
// This test verifies that both functions produce the same results for all scenarios:
// - Left values with error recovery
// - Left values with error transformation
// - Right values passing through unchanged
func TestChainLeftIdenticalToOrElse(t *testing.T) {
// Test 1: Left value with error recovery - both should recover to Right
t.Run("Left value recovery - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) IOEither[string, int] {
if e == "recoverable" {
return Right[string](42)
}
return Left[int](e)
}
input := Left[int]("recoverable")
// Using ChainLeft
resultChainLeft := ChainLeft(recoveryFn)(input)()
// Using OrElse
resultOrElse := OrElse(recoveryFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](42), resultChainLeft)
})
// Test 2: Left value with error transformation - both should transform error
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
transformFn := func(e string) IOEither[string, int] {
return Left[int]("transformed: " + e)
}
input := Left[int]("original error")
// Using ChainLeft
resultChainLeft := ChainLeft(transformFn)(input)()
// Using OrElse
resultOrElse := OrElse(transformFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
})
// Test 3: Right value - both should pass through unchanged
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
handlerFn := func(e string) IOEither[string, int] {
return Left[int]("should not be called")
}
input := Right[string](100)
// Using ChainLeft
resultChainLeft := ChainLeft(handlerFn)(input)()
// Using OrElse
resultOrElse := OrElse(handlerFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](100), resultChainLeft)
})
// Test 4: Error type widening - both should handle type transformation
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
widenFn := func(e string) IOEither[int, int] {
return Left[int](404)
}
input := Left[int]("not found")
// Using ChainLeft
resultChainLeft := ChainLeft(widenFn)(input)()
// Using OrElse
resultOrElse := OrElse(widenFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int](404), resultChainLeft)
})
// Test 5: Composition in pipeline - both should work identically in F.Pipe
t.Run("Pipeline composition - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) IOEither[string, int] {
if e == "network error" {
return Right[string](0)
}
return Left[int](e)
}
input := Left[int]("network error")
// Using ChainLeft in pipeline
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))()
// Using OrElse in pipeline
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](0), resultChainLeft)
})
// Test 6: Multiple chained operations - both should behave identically
t.Run("Multiple operations - ChainLeft equals OrElse", func(t *testing.T) {
handler1 := func(e string) IOEither[string, int] {
if e == "error1" {
return Right[string](1)
}
return Left[int](e)
}
handler2 := func(e string) IOEither[string, int] {
if e == "error2" {
return Right[string](2)
}
return Left[int](e)
}
input := Left[int]("error2")
// Using ChainLeft
resultChainLeft := F.Pipe2(
input,
ChainLeft(handler1),
ChainLeft(handler2),
)()
// Using OrElse
resultOrElse := F.Pipe2(
input,
OrElse(handler1),
OrElse(handler2),
)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](2), resultChainLeft)
})
}

View File

@@ -2,6 +2,7 @@ package ioeither
import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/tailrec"
)
@@ -18,4 +19,6 @@ type (
// Trampoline represents a tail-recursive computation that can be evaluated safely
// without stack overflow. It's used for implementing stack-safe recursive algorithms.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
Void = function.Void
)

View File

@@ -16,8 +16,10 @@
package ioref
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/readerio"
)
// MakeIORef creates a new IORef containing the given initial value.
@@ -49,6 +51,32 @@ func MakeIORef[A any](a A) IO[IORef[A]] {
}
}
// Write atomically writes a new value to an IORef and returns the written value.
//
// This function returns a Kleisli arrow that takes an IORef and produces an IO
// computation that writes the given value to the reference. The write operation
// is atomic and thread-safe, using a write lock to ensure exclusive access.
//
// Parameters:
// - a: The new value to write to the IORef
//
// Returns:
// - A Kleisli arrow from IORef[A] to IO[A] that writes the value and returns it
//
// Example:
//
// ref := ioref.MakeIORef(42)()
//
// // Write a new value
// newValue := ioref.Write(100)(ref)() // Returns 100, ref now contains 100
//
// // Chain writes
// pipe.Pipe2(
// ref,
// ioref.Write(50),
// io.Chain(ioref.Write(75)),
// )() // ref now contains 75
//
//go:inline
func Write[A any](a A) io.Kleisli[IORef[A], A] {
return func(ref IORef[A]) IO[A] {
@@ -124,20 +152,112 @@ func Read[A any](ref IORef[A]) IO[A] {
// ioref.Modify(func(x int) int { return x + 10 }),
// io.Chain(ioref.Modify(func(x int) int { return x * 2 })),
// )()
//
//go:inline
func Modify[A any](f Endomorphism[A]) io.Kleisli[IORef[A], A] {
return ModifyIOK(function.Flow2(f, io.Of))
}
// ModifyIOK atomically modifies the value in an IORef using an IO-based transformation.
//
// This is a more powerful version of Modify that allows the transformation function
// to perform IO effects. The function takes a Kleisli arrow (a function from A to IO[A])
// and returns a Kleisli arrow that modifies the IORef atomically.
//
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
// access during the read-modify-write cycle. The IO effect in the transformation
// function is executed while holding the lock.
//
// Parameters:
// - f: A Kleisli arrow (io.Kleisli[A, A]) that transforms the current value with IO effects
//
// Returns:
// - A Kleisli arrow from IORef[A] to IO[A] that returns the new value
//
// Example:
//
// ref := ioref.MakeIORef(42)()
//
// // Modify with an IO effect (e.g., logging)
// modifyWithLog := ioref.ModifyIOK(func(x int) io.IO[int] {
// return func() int {
// fmt.Printf("Old value: %d\n", x)
// return x * 2
// }
// })
// newValue := modifyWithLog(ref)() // Logs and returns 84
//
// // Chain multiple IO-based modifications
// pipe.Pipe2(
// ref,
// ioref.ModifyIOK(func(x int) io.IO[int] {
// return io.Of(x + 10)
// }),
// io.Chain(ioref.ModifyIOK(func(x int) io.IO[int] {
// return io.Of(x * 2)
// })),
// )()
func ModifyIOK[A any](f io.Kleisli[A, A]) io.Kleisli[IORef[A], A] {
return func(ref IORef[A]) IO[A] {
return func() A {
ref.mu.Lock()
defer ref.mu.Unlock()
ref.a = f(ref.a)
ref.a = f(ref.a)()
return ref.a
}
}
}
// ModifyReaderIOK atomically modifies the value in an IORef using a ReaderIO-based transformation.
//
// This is a variant of ModifyIOK that works with ReaderIO computations, allowing the
// transformation function to access an environment of type R while performing IO effects.
// This is useful when the modification logic needs access to configuration, context,
// or other shared resources.
//
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
// access during the read-modify-write cycle. The ReaderIO effect in the transformation
// function is executed while holding the lock.
//
// Parameters:
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, A]) that takes the current value
// and an environment R, and returns an IO computation producing the new value
//
// Returns:
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, A] that returns the new value
//
// Example:
//
// type Config struct {
// multiplier int
// }
//
// ref := ioref.MakeIORef(10)()
//
// // Modify using environment
// modifyWithConfig := ioref.ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
// return func(cfg Config) io.IO[int] {
// return func() int {
// return x * cfg.multiplier
// }
// }
// })
//
// config := Config{multiplier: 5}
// newValue := modifyWithConfig(ref)(config)() // Returns 50, ref now contains 50
func ModifyReaderIOK[R, A any](f readerio.Kleisli[R, A, A]) readerio.Kleisli[R, IORef[A], A] {
return func(ref IORef[A]) ReaderIO[R, A] {
return func(r R) readerio.IO[A] {
return func() A {
ref.mu.Lock()
defer ref.mu.Unlock()
ref.a = f(ref.a)(r)()
return ref.a
}
}
}
}
// ModifyWithResult atomically modifies the value in an IORef and returns both
// the new value and an additional result computed from the old value.
//
@@ -167,14 +287,122 @@ func Modify[A any](f Endomorphism[A]) io.Kleisli[IORef[A], A] {
//
//go:inline
func ModifyWithResult[A, B any](f func(A) Pair[A, B]) io.Kleisli[IORef[A], B] {
return ModifyIOKWithResult(function.Flow2(f, io.Of))
}
// ModifyIOKWithResult atomically modifies the value in an IORef and returns a result,
// using an IO-based transformation function.
//
// This is a more powerful version of ModifyWithResult that allows the transformation
// function to perform IO effects. The function takes a Kleisli arrow that transforms
// the old value into an IO computation producing a Pair of (new value, result).
//
// This is useful when you need to:
// - Both transform the stored value and compute some result based on the old value
// - Perform IO effects during the transformation (e.g., logging, validation)
// - Ensure atomicity of the entire read-transform-write-compute cycle
//
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
// access. The IO effect in the transformation function is executed while holding the lock.
//
// Parameters:
// - f: A Kleisli arrow (io.Kleisli[A, Pair[A, B]]) that takes the old value and
// returns an IO computation producing a Pair of (new value, result)
//
// Returns:
// - A Kleisli arrow from IORef[A] to IO[B] that produces the result
//
// Example:
//
// ref := ioref.MakeIORef(42)()
//
// // Increment with IO effect and return old value
// incrementWithLog := ioref.ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
// return func() pair.Pair[int, int] {
// fmt.Printf("Incrementing from %d\n", x)
// return pair.MakePair(x+1, x)
// }
// })
// oldValue := incrementWithLog(ref)() // Logs and returns 42, ref now contains 43
//
// // Swap with validation
// swapWithValidation := ioref.ModifyIOKWithResult(func(old int) io.IO[pair.Pair[int, string]] {
// return func() pair.Pair[int, string] {
// if old < 0 {
// return pair.MakePair(0, "reset negative")
// }
// return pair.MakePair(100, fmt.Sprintf("swapped %d", old))
// }
// })
// message := swapWithValidation(ref)()
func ModifyIOKWithResult[A, B any](f io.Kleisli[A, Pair[A, B]]) io.Kleisli[IORef[A], B] {
return func(ref IORef[A]) IO[B] {
return func() B {
ref.mu.Lock()
defer ref.mu.Unlock()
result := f(ref.a)
result := f(ref.a)()
ref.a = pair.Head(result)
return pair.Tail(result)
}
}
}
// ModifyReaderIOKWithResult atomically modifies the value in an IORef and returns a result,
// using a ReaderIO-based transformation function.
//
// This combines the capabilities of ModifyIOKWithResult and ModifyReaderIOK, allowing the
// transformation function to:
// - Access an environment of type R (like configuration or context)
// - Perform IO effects during the transformation
// - Both update the stored value and compute a result based on the old value
// - Ensure atomicity of the entire read-transform-write-compute cycle
//
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
// access. The ReaderIO effect in the transformation function is executed while holding the lock.
//
// Parameters:
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, Pair[A, B]]) that takes the old value
// and an environment R, and returns an IO computation producing a Pair of (new value, result)
//
// Returns:
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, B] that produces the result
//
// Example:
//
// type Config struct {
// logEnabled bool
// }
//
// ref := ioref.MakeIORef(42)()
//
// // Increment with conditional logging, return old value
// incrementWithLog := ioref.ModifyReaderIOKWithResult(
// func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
// return func(cfg Config) io.IO[pair.Pair[int, int]] {
// return func() pair.Pair[int, int] {
// if cfg.logEnabled {
// fmt.Printf("Incrementing from %d\n", x)
// }
// return pair.MakePair(x+1, x)
// }
// }
// },
// )
//
// config := Config{logEnabled: true}
// oldValue := incrementWithLog(ref)(config)() // Logs and returns 42, ref now contains 43
func ModifyReaderIOKWithResult[R, A, B any](f readerio.Kleisli[R, A, Pair[A, B]]) readerio.Kleisli[R, IORef[A], B] {
return func(ref IORef[A]) ReaderIO[R, B] {
return func(r R) readerio.IO[B] {
return func() B {
ref.mu.Lock()
defer ref.mu.Unlock()
result := f(ref.a)(r)()
ref.a = pair.Head(result)
return pair.Tail(result)
}
}
}
}

919
v2/ioref/ioref_test.go Normal file
View File

@@ -0,0 +1,919 @@
// 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 ioref
import (
"fmt"
"sync"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
)
func TestMakeIORef(t *testing.T) {
t.Run("creates IORef with integer value", func(t *testing.T) {
ref := MakeIORef(42)()
assert.NotNil(t, ref)
assert.Equal(t, 42, Read(ref)())
})
t.Run("creates IORef with string value", func(t *testing.T) {
ref := MakeIORef("hello")()
assert.NotNil(t, ref)
assert.Equal(t, "hello", Read(ref)())
})
t.Run("creates IORef with slice value", func(t *testing.T) {
slice := []int{1, 2, 3}
ref := MakeIORef(slice)()
assert.NotNil(t, ref)
assert.Equal(t, slice, Read(ref)())
})
t.Run("creates IORef with struct value", func(t *testing.T) {
type Person struct {
Name string
Age int
}
person := Person{Name: "Alice", Age: 30}
ref := MakeIORef(person)()
assert.NotNil(t, ref)
assert.Equal(t, person, Read(ref)())
})
t.Run("creates IORef with zero value", func(t *testing.T) {
ref := MakeIORef(0)()
assert.NotNil(t, ref)
assert.Equal(t, 0, Read(ref)())
})
t.Run("creates IORef with nil pointer", func(t *testing.T) {
var ptr *int
ref := MakeIORef(ptr)()
assert.NotNil(t, ref)
assert.Nil(t, Read(ref)())
})
t.Run("multiple IORefs are independent", func(t *testing.T) {
ref1 := MakeIORef(10)()
ref2 := MakeIORef(20)()
assert.Equal(t, 10, Read(ref1)())
assert.Equal(t, 20, Read(ref2)())
Write(30)(ref1)()
assert.Equal(t, 30, Read(ref1)())
assert.Equal(t, 20, Read(ref2)()) // ref2 unchanged
})
}
func TestRead(t *testing.T) {
t.Run("reads initial value", func(t *testing.T) {
ref := MakeIORef(42)()
value := Read(ref)()
assert.Equal(t, 42, value)
})
t.Run("reads updated value", func(t *testing.T) {
ref := MakeIORef(10)()
Write(20)(ref)()
value := Read(ref)()
assert.Equal(t, 20, value)
})
t.Run("multiple reads return same value", func(t *testing.T) {
ref := MakeIORef(100)()
value1 := Read(ref)()
value2 := Read(ref)()
value3 := Read(ref)()
assert.Equal(t, 100, value1)
assert.Equal(t, 100, value2)
assert.Equal(t, 100, value3)
})
t.Run("concurrent reads are thread-safe", func(t *testing.T) {
ref := MakeIORef(42)()
var wg sync.WaitGroup
iterations := 100
results := make([]int, iterations)
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
results[idx] = Read(ref)()
}(i)
}
wg.Wait()
// All reads should return the same value
for _, v := range results {
assert.Equal(t, 42, v)
}
})
t.Run("reads during concurrent writes", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 50
// Start concurrent writes
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
Write(val)(ref)()
}(i)
}
// Start concurrent reads
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
value := Read(ref)()
// Value should be valid (between 0 and iterations-1)
assert.GreaterOrEqual(t, value, 0)
assert.Less(t, value, iterations)
}()
}
wg.Wait()
})
}
func TestWrite(t *testing.T) {
t.Run("writes new value", func(t *testing.T) {
ref := MakeIORef(42)()
result := Write(100)(ref)()
assert.Equal(t, 100, result)
assert.Equal(t, 100, Read(ref)())
})
t.Run("overwrites existing value", func(t *testing.T) {
ref := MakeIORef(10)()
Write(20)(ref)()
Write(30)(ref)()
assert.Equal(t, 30, Read(ref)())
})
t.Run("returns written value", func(t *testing.T) {
ref := MakeIORef(0)()
result := Write(42)(ref)()
assert.Equal(t, 42, result)
})
t.Run("writes string value", func(t *testing.T) {
ref := MakeIORef("hello")()
result := Write("world")(ref)()
assert.Equal(t, "world", result)
assert.Equal(t, "world", Read(ref)())
})
t.Run("chained writes", func(t *testing.T) {
ref := MakeIORef(1)()
Write(2)(ref)()
Write(3)(ref)()
result := Write(4)(ref)()
assert.Equal(t, 4, result)
assert.Equal(t, 4, Read(ref)())
})
t.Run("concurrent writes are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
Write(val)(ref)()
}(i)
}
wg.Wait()
// Final value should be one of the written values
finalValue := Read(ref)()
assert.GreaterOrEqual(t, finalValue, 0)
assert.Less(t, finalValue, iterations)
})
t.Run("write with zero value", func(t *testing.T) {
ref := MakeIORef(42)()
Write(0)(ref)()
assert.Equal(t, 0, Read(ref)())
})
}
func TestModify(t *testing.T) {
t.Run("modifies value with simple function", func(t *testing.T) {
ref := MakeIORef(10)()
result := Modify(func(x int) int { return x * 2 })(ref)()
assert.Equal(t, 20, result)
assert.Equal(t, 20, Read(ref)())
})
t.Run("modifies with addition", func(t *testing.T) {
ref := MakeIORef(5)()
Modify(func(x int) int { return x + 10 })(ref)()
assert.Equal(t, 15, Read(ref)())
})
t.Run("modifies string value", func(t *testing.T) {
ref := MakeIORef("hello")()
result := Modify(func(s string) string { return s + " world" })(ref)()
assert.Equal(t, "hello world", result)
assert.Equal(t, "hello world", Read(ref)())
})
t.Run("chained modifications", func(t *testing.T) {
ref := MakeIORef(2)()
Modify(func(x int) int { return x * 3 })(ref)() // 6
Modify(func(x int) int { return x + 4 })(ref)() // 10
result := Modify(func(x int) int { return x / 2 })(ref)()
assert.Equal(t, 5, result)
assert.Equal(t, 5, Read(ref)())
})
t.Run("concurrent modifications are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Modify(func(x int) int { return x + 1 })(ref)()
}()
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
})
t.Run("modify with identity function", func(t *testing.T) {
ref := MakeIORef(42)()
result := Modify(func(x int) int { return x })(ref)()
assert.Equal(t, 42, result)
assert.Equal(t, 42, Read(ref)())
})
t.Run("modify returns new value", func(t *testing.T) {
ref := MakeIORef(100)()
result := Modify(func(x int) int { return x - 50 })(ref)()
assert.Equal(t, 50, result)
})
}
func TestModifyWithResult(t *testing.T) {
t.Run("modifies and returns old value", func(t *testing.T) {
ref := MakeIORef(42)()
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)()
assert.Equal(t, 42, oldValue)
assert.Equal(t, 43, Read(ref)())
})
t.Run("swaps value and returns old", func(t *testing.T) {
ref := MakeIORef(100)()
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(200, x)
})(ref)()
assert.Equal(t, 100, oldValue)
assert.Equal(t, 200, Read(ref)())
})
t.Run("returns different type", func(t *testing.T) {
ref := MakeIORef(42)()
message := ModifyWithResult(func(x int) pair.Pair[int, string] {
return pair.MakePair(x*2, fmt.Sprintf("doubled from %d", x))
})(ref)()
assert.Equal(t, "doubled from 42", message)
assert.Equal(t, 84, Read(ref)())
})
t.Run("computes result based on old value", func(t *testing.T) {
ref := MakeIORef(10)()
wasPositive := ModifyWithResult(func(x int) pair.Pair[int, bool] {
return pair.MakePair(x+5, x > 0)
})(ref)()
assert.True(t, wasPositive)
assert.Equal(t, 15, Read(ref)())
})
t.Run("chained modifications with results", func(t *testing.T) {
ref := MakeIORef(5)()
result1 := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x*2, x)
})(ref)()
result2 := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+10, x)
})(ref)()
assert.Equal(t, 5, result1)
assert.Equal(t, 10, result2)
assert.Equal(t, 20, Read(ref)())
})
t.Run("concurrent modifications with results are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
results := make([]int, iterations)
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)()
results[idx] = oldValue
}(i)
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
// All old values should be unique
seen := make(map[int]bool)
for _, v := range results {
assert.False(t, seen[v])
seen[v] = true
}
})
t.Run("extract and replace pattern", func(t *testing.T) {
ref := MakeIORef([]int{1, 2, 3})()
first := ModifyWithResult(func(xs []int) pair.Pair[[]int, int] {
if len(xs) == 0 {
return pair.MakePair(xs, 0)
}
return pair.MakePair(xs[1:], xs[0])
})(ref)()
assert.Equal(t, 1, first)
assert.Equal(t, []int{2, 3}, Read(ref)())
})
}
func TestModifyReaderIOK(t *testing.T) {
type Config struct {
multiplier int
}
t.Run("modifies with environment", func(t *testing.T) {
ref := MakeIORef(10)()
config := Config{multiplier: 5}
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x * cfg.multiplier)
}
})(ref)(config)()
assert.Equal(t, 50, result)
assert.Equal(t, 50, Read(ref)())
})
t.Run("uses environment for computation", func(t *testing.T) {
ref := MakeIORef(100)()
config := Config{multiplier: 2}
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return func() int {
return x / cfg.multiplier
}
}
})(ref)(config)()
assert.Equal(t, 50, result)
assert.Equal(t, 50, Read(ref)())
})
t.Run("chained modifications with different configs", func(t *testing.T) {
ref := MakeIORef(10)()
config1 := Config{multiplier: 2}
config2 := Config{multiplier: 3}
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x * cfg.multiplier)
}
})(ref)(config1)()
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x + cfg.multiplier)
}
})(ref)(config2)()
assert.Equal(t, 23, result) // (10 * 2) + 3
assert.Equal(t, 23, Read(ref)())
})
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
config := Config{multiplier: 1}
var wg sync.WaitGroup
iterations := 100
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x + cfg.multiplier)
}
})(ref)(config)()
}()
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
})
t.Run("environment provides configuration", func(t *testing.T) {
type Settings struct {
prefix string
}
ref := MakeIORef("world")()
settings := Settings{prefix: "hello "}
result := ModifyReaderIOK(func(s string) readerio.ReaderIO[Settings, string] {
return func(cfg Settings) io.IO[string] {
return io.Of(cfg.prefix + s)
}
})(ref)(settings)()
assert.Equal(t, "hello world", result)
assert.Equal(t, "hello world", Read(ref)())
})
}
func TestModifyReaderIOKWithResult(t *testing.T) {
type Config struct {
logEnabled bool
multiplier int
}
t.Run("modifies with environment and returns result", func(t *testing.T) {
ref := MakeIORef(42)()
config := Config{logEnabled: false, multiplier: 2}
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x*cfg.multiplier, x))
}
})(ref)(config)()
assert.Equal(t, 42, oldValue)
assert.Equal(t, 84, Read(ref)())
})
t.Run("returns different type based on environment", func(t *testing.T) {
ref := MakeIORef(10)()
config := Config{logEnabled: true, multiplier: 3}
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
return func(cfg Config) io.IO[pair.Pair[int, string]] {
return func() pair.Pair[int, string] {
newVal := x * cfg.multiplier
msg := fmt.Sprintf("multiplied %d by %d", x, cfg.multiplier)
return pair.MakePair(newVal, msg)
}
}
})(ref)(config)()
assert.Equal(t, "multiplied 10 by 3", message)
assert.Equal(t, 30, Read(ref)())
})
t.Run("conditional logic based on environment", func(t *testing.T) {
ref := MakeIORef(-10)()
config := Config{logEnabled: true, multiplier: 2}
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
return func(cfg Config) io.IO[pair.Pair[int, string]] {
return func() pair.Pair[int, string] {
if x < 0 {
return pair.MakePair(0, "reset negative value")
}
return pair.MakePair(x*cfg.multiplier, "multiplied positive value")
}
}
})(ref)(config)()
assert.Equal(t, "reset negative value", message)
assert.Equal(t, 0, Read(ref)())
})
t.Run("chained modifications with results", func(t *testing.T) {
ref := MakeIORef(5)()
config := Config{logEnabled: false, multiplier: 2}
result1 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x*cfg.multiplier, x))
}
})(ref)(config)()
result2 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x+cfg.multiplier, x))
}
})(ref)(config)()
assert.Equal(t, 5, result1)
assert.Equal(t, 10, result2)
assert.Equal(t, 12, Read(ref)())
})
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
config := Config{logEnabled: false, multiplier: 1}
var wg sync.WaitGroup
iterations := 100
results := make([]int, iterations)
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x+cfg.multiplier, x))
}
})(ref)(config)()
results[idx] = oldValue
}(i)
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
// All old values should be unique
seen := make(map[int]bool)
for _, v := range results {
assert.False(t, seen[v])
seen[v] = true
}
})
t.Run("environment provides validation rules", func(t *testing.T) {
type ValidationConfig struct {
maxValue int
}
ref := MakeIORef(100)()
config := ValidationConfig{maxValue: 50}
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[ValidationConfig, pair.Pair[int, string]] {
return func(cfg ValidationConfig) io.IO[pair.Pair[int, string]] {
return func() pair.Pair[int, string] {
if x > cfg.maxValue {
return pair.MakePair(cfg.maxValue, fmt.Sprintf("capped at %d", cfg.maxValue))
}
return pair.MakePair(x, "value within limits")
}
}
})(ref)(config)()
assert.Equal(t, "capped at 50", message)
assert.Equal(t, 50, Read(ref)())
})
}
func TestModifyIOK(t *testing.T) {
t.Run("basic modification with IO effect", func(t *testing.T) {
ref := MakeIORef(42)()
// Double the value using ModifyIOK
newValue := ModifyIOK(func(x int) io.IO[int] {
return io.Of(x * 2)
})(ref)()
assert.Equal(t, 84, newValue)
assert.Equal(t, 84, Read(ref)())
})
t.Run("modification with side effects", func(t *testing.T) {
ref := MakeIORef(10)()
var sideEffect int
// Modify with a side effect
newValue := ModifyIOK(func(x int) io.IO[int] {
return func() int {
sideEffect = x // Capture old value
return x + 5
}
})(ref)()
assert.Equal(t, 15, newValue)
assert.Equal(t, 10, sideEffect)
assert.Equal(t, 15, Read(ref)())
})
t.Run("chained modifications", func(t *testing.T) {
ref := MakeIORef(5)()
// First modification: add 10
ModifyIOK(func(x int) io.IO[int] {
return io.Of(x + 10)
})(ref)()
// Second modification: multiply by 2
result := ModifyIOK(func(x int) io.IO[int] {
return io.Of(x * 2)
})(ref)()
assert.Equal(t, 30, result)
assert.Equal(t, 30, Read(ref)())
})
t.Run("concurrent modifications are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
// Increment concurrently
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ModifyIOK(func(x int) io.IO[int] {
return io.Of(x + 1)
})(ref)()
}()
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
})
t.Run("modification with string type", func(t *testing.T) {
ref := MakeIORef("hello")()
newValue := ModifyIOK(func(s string) io.IO[string] {
return io.Of(s + " world")
})(ref)()
assert.Equal(t, "hello world", newValue)
assert.Equal(t, "hello world", Read(ref)())
})
t.Run("modification returns new value", func(t *testing.T) {
ref := MakeIORef(100)()
result := ModifyIOK(func(x int) io.IO[int] {
return io.Of(x / 2)
})(ref)()
// ModifyIOK returns the new value
assert.Equal(t, 50, result)
assert.Equal(t, 50, Read(ref)())
})
t.Run("modification with complex IO computation", func(t *testing.T) {
ref := MakeIORef(3)()
// Use a more complex IO computation
newValue := ModifyIOK(func(x int) io.IO[int] {
return F.Pipe1(
io.Of(x),
io.Map(func(n int) int { return n * n }),
)
})(ref)()
assert.Equal(t, 9, newValue)
assert.Equal(t, 9, Read(ref)())
})
}
func TestModifyIOKWithResult(t *testing.T) {
t.Run("basic modification with result", func(t *testing.T) {
ref := MakeIORef(42)()
// Increment and return old value
oldValue := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x+1, x))
})(ref)()
assert.Equal(t, 42, oldValue)
assert.Equal(t, 43, Read(ref)())
})
t.Run("swap and return old value", func(t *testing.T) {
ref := MakeIORef(100)()
oldValue := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(200, x))
})(ref)()
assert.Equal(t, 100, oldValue)
assert.Equal(t, 200, Read(ref)())
})
t.Run("modification with different result type", func(t *testing.T) {
ref := MakeIORef(42)()
// Double the value and return a message
message := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
return io.Of(pair.MakePair(x*2, fmt.Sprintf("doubled from %d", x)))
})(ref)()
assert.Equal(t, "doubled from 42", message)
assert.Equal(t, 84, Read(ref)())
})
t.Run("modification with side effects in IO", func(t *testing.T) {
ref := MakeIORef(10)()
var sideEffect string
result := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, bool]] {
return func() pair.Pair[int, bool] {
sideEffect = fmt.Sprintf("processing %d", x)
return pair.MakePair(x+5, x > 5)
}
})(ref)()
assert.True(t, result)
assert.Equal(t, "processing 10", sideEffect)
assert.Equal(t, 15, Read(ref)())
})
t.Run("chained modifications with results", func(t *testing.T) {
ref := MakeIORef(5)()
// First modification
result1 := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x*2, x))
})(ref)()
// Second modification
result2 := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x+10, x))
})(ref)()
assert.Equal(t, 5, result1) // Original value
assert.Equal(t, 10, result2) // After first modification
assert.Equal(t, 20, Read(ref)()) // After both modifications
})
t.Run("concurrent modifications with results are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
results := make([]int, iterations)
// Increment concurrently and collect old values
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
oldValue := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x+1, x))
})(ref)()
results[idx] = oldValue
}(i)
}
wg.Wait()
// Final value should be iterations
assert.Equal(t, iterations, Read(ref)())
// All old values should be unique and in range [0, iterations)
seen := make(map[int]bool)
for _, v := range results {
assert.False(t, seen[v], "duplicate old value: %d", v)
assert.GreaterOrEqual(t, v, 0)
assert.Less(t, v, iterations)
seen[v] = true
}
})
t.Run("modification with string types", func(t *testing.T) {
ref := MakeIORef("hello")()
length := ModifyIOKWithResult(func(s string) io.IO[pair.Pair[string, int]] {
return io.Of(pair.MakePair(s+" world", len(s)))
})(ref)()
assert.Equal(t, 5, length)
assert.Equal(t, "hello world", Read(ref)())
})
t.Run("modification with validation logic", func(t *testing.T) {
ref := MakeIORef(-10)()
message := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
return func() pair.Pair[int, string] {
if x < 0 {
return pair.MakePair(0, "reset negative value")
}
return pair.MakePair(x*2, "doubled positive value")
}
})(ref)()
assert.Equal(t, "reset negative value", message)
assert.Equal(t, 0, Read(ref)())
})
t.Run("modification with complex IO computation", func(t *testing.T) {
ref := MakeIORef(5)()
result := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
return F.Pipe1(
io.Of(x),
io.Map(func(n int) pair.Pair[int, string] {
squared := n * n
return pair.MakePair(squared, fmt.Sprintf("%d squared is %d", n, squared))
}),
)
})(ref)()
assert.Equal(t, "5 squared is 25", result)
assert.Equal(t, 25, Read(ref)())
})
t.Run("extract and replace pattern", func(t *testing.T) {
ref := MakeIORef([]int{1, 2, 3})()
// Extract first element and remove it from the slice
first := ModifyIOKWithResult(func(xs []int) io.IO[pair.Pair[[]int, int]] {
return func() pair.Pair[[]int, int] {
if len(xs) == 0 {
return pair.MakePair(xs, 0)
}
return pair.MakePair(xs[1:], xs[0])
}
})(ref)()
assert.Equal(t, 1, first)
assert.Equal(t, []int{2, 3}, Read(ref)())
})
}
func TestModifyIOKIntegration(t *testing.T) {
t.Run("ModifyIOK integrates with Modify", func(t *testing.T) {
ref := MakeIORef(10)()
// Use Modify (which internally uses ModifyIOK)
result1 := Modify(N.Mul(2))(ref)()
assert.Equal(t, 20, result1)
// Use ModifyIOK directly
result2 := ModifyIOK(func(x int) io.IO[int] {
return io.Of(x + 5)
})(ref)()
assert.Equal(t, 25, result2)
assert.Equal(t, 25, Read(ref)())
})
}
func TestModifyIOKWithResultIntegration(t *testing.T) {
t.Run("ModifyIOKWithResult integrates with ModifyWithResult", func(t *testing.T) {
ref := MakeIORef(10)()
// Use ModifyWithResult (which internally uses ModifyIOKWithResult)
result1 := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x*2, x)
})(ref)()
assert.Equal(t, 10, result1)
assert.Equal(t, 20, Read(ref)())
// Use ModifyIOKWithResult directly
result2 := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
return io.Of(pair.MakePair(x+5, fmt.Sprintf("was %d", x)))
})(ref)()
assert.Equal(t, "was 20", result2)
assert.Equal(t, 25, Read(ref)())
})
}

View File

@@ -45,30 +45,134 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/readerio"
)
type (
// ioRef is the internal implementation of a mutable reference.
// It uses a read-write mutex to ensure thread-safe access.
// It uses a read-write mutex to ensure thread-safe access to the stored value.
//
// The mutex allows multiple concurrent readers (using RLock) but ensures
// exclusive access for writers (using Lock), preventing race conditions
// when reading or modifying the stored value.
//
// This type is not exported; users interact with it through the IORef type alias.
ioRef[A any] struct {
mu sync.RWMutex
a A
mu sync.RWMutex // Protects concurrent access to the stored value
a A // The stored value
}
// IO represents a synchronous computation that may have side effects.
// It's a function that takes no arguments and returns a value of type A.
//
// IO computations are lazy - they don't execute until explicitly invoked
// by calling the function. This allows for composing and chaining effects
// before execution.
//
// Example:
//
// // Define an IO computation
// computation := func() int {
// fmt.Println("Computing...")
// return 42
// }
//
// // Nothing happens yet - the computation is lazy
// result := computation() // Now it executes and prints "Computing..."
IO[A any] = io.IO[A]
// ReaderIO represents a computation that requires an environment of type R
// and produces an IO effect that yields a value of type A.
//
// This combines the Reader pattern (dependency injection) with IO effects,
// allowing computations to access shared configuration or context while
// performing side effects.
//
// Example:
//
// type Config struct {
// multiplier int
// }
//
// // A ReaderIO that uses config to compute a value
// computation := func(cfg Config) io.IO[int] {
// return func() int {
// return 42 * cfg.multiplier
// }
// }
//
// // Execute with specific config
// result := computation(Config{multiplier: 2})() // Returns 84
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// IORef represents a mutable reference to a value of type A.
// Operations on IORef are thread-safe and performed within the IO monad.
//
// IORef provides a way to work with mutable state in a functional style,
// where mutations are explicit and contained within IO computations.
// This makes side effects visible in the type system and allows for
// better reasoning about code that uses mutable state.
//
// All operations on IORef (Read, Write, Modify, etc.) are atomic and
// thread-safe, making it safe to share IORefs across goroutines.
//
// Example:
//
// // Create a new IORef
// ref := ioref.MakeIORef(42)()
//
// // Read the current value
// value := ioref.Read(ref)() // 42
//
// // Write a new value
// ioref.Write(100)(ref)()
//
// // Modify the value atomically
// ioref.Modify(func(x int) int { return x * 2 })(ref)()
IORef[A any] = *ioRef[A]
// Endomorphism represents a function from A to A.
// It's commonly used with Modify to transform the value in an IORef.
//
// An endomorphism is a morphism (structure-preserving map) from a
// mathematical object to itself. In programming terms, it's simply
// a function that takes a value and returns a value of the same type.
//
// Example:
//
// // An endomorphism that doubles an integer
// double := func(x int) int { return x * 2 }
//
// // An endomorphism that uppercases a string
// upper := func(s string) string { return strings.ToUpper(s) }
//
// // Use with IORef
// ref := ioref.MakeIORef(21)()
// ioref.Modify(double)(ref)() // ref now contains 42
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Pair represents a tuple of two values of types A and B.
// It's used with ModifyWithResult and ModifyIOKWithResult to return both
// a new value for the IORef (head) and a computed result (tail).
//
// The head of the pair contains the new value to store in the IORef,
// while the tail contains the result to return from the operation.
// This allows atomic operations that both update the reference and
// compute a result based on the old value.
//
// Example:
//
// // Create a pair where head is the new value and tail is the old value
// p := pair.MakePair(newValue, oldValue)
//
// // Extract values
// newVal := pair.Head(p) // Gets the head (new value)
// oldVal := pair.Tail(p) // Gets the tail (old value)
//
// // Use with ModifyWithResult to swap and return old value
// ref := ioref.MakeIORef(42)()
// oldValue := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
// return pair.MakePair(100, x) // Store 100, return old value
// })(ref)() // oldValue is 42, ref now contains 100
Pair[A, B any] = pair.Pair[A, B]
)

View File

@@ -29,7 +29,7 @@ func ExampleIOEither_do() {
bar := Of(1)
// quux consumes the state of three bindings and returns an [IO] instead of an [IOEither]
quux := func(t T.Tuple3[string, int, string]) IO[any] {
quux := func(t T.Tuple3[string, int, string]) IO[Void] {
return io.FromImpure(func() {
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
})

View File

@@ -45,7 +45,7 @@ func TestBuilderWithQuery(t *testing.T) {
ioresult.Map(func(r *http.Request) *url.URL {
return r.URL
}),
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[any] {
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[Void] {
return io.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))

View File

@@ -15,10 +15,14 @@
package builder
import "github.com/IBM/fp-go/v2/ioresult"
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioresult"
)
type (
IOResult[T any] = ioresult.IOResult[T]
Kleisli[A, B any] = ioresult.Kleisli[A, B]
Operator[A, B any] = ioresult.Operator[A, B]
Void = function.Void
)

View File

@@ -52,27 +52,27 @@ func MakeClient(httpClient *http.Client) Client {
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
//
//go:inline
func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
func ReadFullResponse(client Client) Operator[*http.Request, H.FullResponse] {
return IOEH.ReadFullResponse(client)
}
// ReadAll sends a request and reads the response as bytes
//
//go:inline
func ReadAll(client Client) Kleisli[Requester, []byte] {
func ReadAll(client Client) Operator[*http.Request, []byte] {
return IOEH.ReadAll(client)
}
// ReadText sends a request, reads the response and represents the response as a text string
//
//go:inline
func ReadText(client Client) Kleisli[Requester, string] {
func ReadText(client Client) Operator[*http.Request, string] {
return IOEH.ReadText(client)
}
// ReadJSON sends a request, reads the response and parses the response as JSON
//
//go:inline
func ReadJSON[A any](client Client) Kleisli[Requester, A] {
func ReadJSON[A any](client Client) Operator[*http.Request, A] {
return IOEH.ReadJSON[A](client)
}

View File

@@ -404,7 +404,7 @@ func Swap[A any](val IOResult[A]) ioeither.IOEither[A, error] {
// FromImpure converts a side effect without a return value into a side effect that returns any
//
//go:inline
func FromImpure[E any](f func()) IOResult[any] {
func FromImpure[E any](f func()) IOResult[Void] {
return ioeither.FromImpure[error](f)
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
@@ -56,4 +57,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
// It's commonly used for filtering and conditional operations.
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
)

View File

@@ -466,6 +466,11 @@ func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
return F.Bind2nd(MonadChain[A, B], f)
}
//go:inline
func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
return Chain(f)
}
// Flatten flattens a sequence of sequences into a single sequence.
//
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll

54
v2/iterator/iter/last.go Normal file
View File

@@ -0,0 +1,54 @@
package iter
import (
"github.com/IBM/fp-go/v2/option"
)
// Last returns the last element from an [Iterator] wrapped in an [Option].
//
// This function retrieves the last element from the iterator by consuming the entire
// sequence. If the iterator contains at least one element, it returns Some(element).
// If the iterator is empty, it returns None.
//
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
//
// Type Parameters:
// - U: The type of elements in the iterator
//
// Parameters:
// - it: The input iterator to get the last element from
//
// Returns:
// - Option[U]: Some(last element) if the iterator is non-empty, None otherwise
//
// Example with non-empty sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// last := iter.Last(seq)
// // Returns: Some(5)
//
// Example with empty sequence:
//
// seq := iter.Empty[int]()
// last := iter.Last(seq)
// // Returns: None
//
// Example with filtered sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// filtered := iter.Filter(func(x int) bool { return x < 4 })(seq)
// last := iter.Last(filtered)
// // Returns: Some(3)
func Last[U any](it Seq[U]) Option[U] {
var last U
found := false
for last = range it {
found = true
}
if !found {
return option.None[U]()
}
return option.Some(last)
}

View File

@@ -0,0 +1,305 @@
package iter
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestLast test getting the last element from a non-empty sequence
func TestLastSimple(t *testing.T) {
t.Run("returns last element from integer sequence", func(t *testing.T) {
seq := From(1, 2, 3)
last := Last(seq)
assert.Equal(t, O.Of(3), last)
})
t.Run("returns last element from string sequence", func(t *testing.T) {
seq := From("a", "b", "c")
last := Last(seq)
assert.Equal(t, O.Of("c"), last)
})
t.Run("returns last element from single element sequence", func(t *testing.T) {
seq := From(42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns last element from large sequence", func(t *testing.T) {
seq := From(100, 200, 300, 400, 500)
last := Last(seq)
assert.Equal(t, O.Of(500), last)
})
}
// TestLastEmpty tests getting the last element from an empty sequence
func TestLastEmpty(t *testing.T) {
t.Run("returns None for empty integer sequence", func(t *testing.T) {
seq := Empty[int]()
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
t.Run("returns None for empty string sequence", func(t *testing.T) {
seq := Empty[string]()
last := Last(seq)
assert.Equal(t, O.None[string](), last)
})
t.Run("returns None for empty struct sequence", func(t *testing.T) {
type TestStruct struct {
Value int
}
seq := Empty[TestStruct]()
last := Last(seq)
assert.Equal(t, O.None[TestStruct](), last)
})
t.Run("returns None for empty sequence of functions", func(t *testing.T) {
type TestFunc func(int)
seq := Empty[TestFunc]()
last := Last(seq)
assert.Equal(t, O.None[TestFunc](), last)
})
}
// TestLastWithComplex tests Last with complex types
func TestLastWithComplex(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("returns last person", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
)
last := Last(seq)
expected := O.Of(Person{"Charlie", 35})
assert.Equal(t, expected, last)
})
t.Run("returns last pointer", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
seq := From(p1, p2)
last := Last(seq)
assert.Equal(t, O.Of(p2), last)
})
}
func TestLastWithFunctions(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := "last"
f1 := function.Constant("first")
f2 := function.Constant("last")
seq := From(f1, f2)
getLast := function.Flow2(
Last,
O.Map(funcReader),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func funcReader(f func() string) string {
return f()
}
// TestLastWithChan tests Last with channels
func TestLastWithChan(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := 30
seq := From(intChan(10),
intChan(20),
intChan(want))
getLast := function.Flow2(
Last,
O.Map(chanReader[int]),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func chanReader[T any](c <-chan T) T {
return <-c
}
func intChan(val int) <-chan int {
ch := make(chan int, 1)
ch <- val
close(ch)
return ch
}
// TestLastWithChainedOperations tests Last with multiple chained operations
func TestLastWithChainedOperations(t *testing.T) {
t.Run("chains filter, map, and last", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, N.MoreThan(5))
mapped := MonadMap(filtered, N.Mul(10))
result := Last(mapped)
assert.Equal(t, O.Of(100), result)
})
t.Run("chains map and filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
filtered := MonadFilter(mapped, N.MoreThan(5))
result := Last(filtered)
assert.Equal(t, O.Of(10), result)
})
}
// TestLastWithReplicate tests Last with replicated values
func TestLastWithReplicate(t *testing.T) {
t.Run("returns last from replicated sequence", func(t *testing.T) {
seq := Replicate(5, 42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns None from zero replications", func(t *testing.T) {
seq := Replicate(0, 42)
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithMakeBy tests Last with MakeBy
func TestLastWithMakeBy(t *testing.T) {
t.Run("returns last generated element", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i * i })
last := Last(seq)
assert.Equal(t, O.Of(16), last)
})
t.Run("returns None for zero elements", func(t *testing.T) {
seq := MakeBy(0, F.Identity[int])
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithPrepend tests Last with Prepend
func TestLastWithPrepend(t *testing.T) {
t.Run("returns last element, not prepended", func(t *testing.T) {
seq := From(2, 3, 4)
prepended := Prepend(1)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
prepended := Prepend(42)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithAppend tests Last with Append
func TestLastWithAppend(t *testing.T) {
t.Run("returns appended element", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
last := Last(appended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns appended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
appended := Append(42)(seq)
last := Last(appended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithChain tests Last with Chain (flatMap)
func TestLastWithChain(t *testing.T) {
t.Run("returns last from chained sequence", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
last := Last(chained)
assert.Equal(t, O.Of(30), last)
})
t.Run("returns None when chain produces empty", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return Empty[int]()
})
last := Last(chained)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithFlatten tests Last with Flatten
func TestLastWithFlatten(t *testing.T) {
t.Run("returns last from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5))
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.Of(5), last)
})
t.Run("returns None from empty nested sequence", func(t *testing.T) {
nested := Empty[Seq[int]]()
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.None[int](), last)
})
}
// Example tests for documentation
func ExampleLast() {
seq := From(1, 2, 3, 4, 5)
last := Last(seq)
if value, ok := O.Unwrap(last); ok {
fmt.Printf("Last element: %d\n", value)
}
// Output: Last element: 5
}
func ExampleLast_empty() {
seq := Empty[int]()
last := Last(seq)
if _, ok := O.Unwrap(last); !ok {
fmt.Println("Sequence is empty")
}
// Output: Sequence is empty
}
func ExampleLast_functions() {
f1 := function.Constant("first")
f2 := function.Constant("middle")
f3 := function.Constant("last")
seq := From(f1, f2, f3)
last := Last(seq)
if fn, ok := O.Unwrap(last); ok {
result := fn()
fmt.Printf("Last function result: %s\n", result)
}
// Output: Last function result: last
}

158
v2/iterator/iter/option.go Normal file
View File

@@ -0,0 +1,158 @@
// 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 iter
import (
"github.com/IBM/fp-go/v2/option"
)
// MonadChainOptionK chains a function that returns an Option into a sequence,
// filtering out None values and unwrapping Some values.
//
// This is useful for operations that may or may not produce a value for each element
// in the sequence. Only the successful (Some) results are included in the output sequence,
// while None values are filtered out.
//
// This is the monadic form that takes the sequence as the first parameter.
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - as: The input sequence to transform
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// A new sequence containing only the unwrapped Some values
//
// Example:
//
// import (
// "strconv"
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Parse strings to integers, filtering out invalid ones
// parseNum := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
//
// seq := I.From("1", "invalid", "2", "3", "bad")
// result := I.MonadChainOptionK(seq, parseNum)
// // yields: 1, 2, 3 (invalid strings are filtered out)
func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
return MonadFilterMap(as, f)
}
// ChainOptionK returns an operator that chains a function returning an Option into a sequence,
// filtering out None values and unwrapping Some values.
//
// This is the curried version of [MonadChainOptionK], useful for function composition
// and creating reusable transformations.
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
//
// Example:
//
// import (
// "strconv"
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Create a reusable parser operator
// parsePositive := I.ChainOptionK(func(x int) O.Option[int] {
// if x > 0 {
// return O.Some(x)
// }
// return O.None[int]()
// })
//
// result := F.Pipe1(
// I.From(-1, 2, -3, 4, 5),
// parsePositive,
// )
// // yields: 2, 4, 5 (negative numbers are filtered out)
//
//go:inline
func ChainOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return FilterMap(f)
}
// FlatMapOptionK is an alias for [ChainOptionK].
//
// This provides a more familiar name for developers coming from other functional
// programming languages or libraries where "flatMap" is the standard terminology
// for the monadic bind operation.
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Validate and transform data
// validateAge := I.FlatMapOptionK(func(age int) O.Option[string] {
// if age >= 18 && age <= 120 {
// return O.Some(fmt.Sprintf("Valid age: %d", age))
// }
// return O.None[string]()
// })
//
// result := F.Pipe1(
// I.From(15, 25, 150, 30),
// validateAge,
// )
// // yields: "Valid age: 25", "Valid age: 30"
//
//go:inline
func FlatMapOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return ChainOptionK(f)
}

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