1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-19 23:42:05 +02:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Dr. Carsten Leue
20398e67a9 fix: better doc and implementation of retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-17 15:58:11 +01:00
Dr. Carsten Leue
fceda15701 doc: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-17 10:11:58 +01:00
Dr. Carsten Leue
4ebfcadabe fix: add better tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-16 14:03:01 +01:00
Dr. Carsten Leue
acb601fc01 fix: reuse some more code
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-15 16:30:40 +01:00
Dr. Carsten Leue
d17663f016 fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-15 11:16:09 +01:00
Dr. Carsten Leue
829365fc24 doc: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 13:30:10 +01:00
Dr. Carsten Leue
64b5660b4e doc: remove some comments
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 12:35:53 +01:00
Dr. Carsten Leue
16e82d6a65 fix: better cancellation support
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 11:52:43 +01:00
Dr. Carsten Leue
0d40fdcebb fix: implement tail recursion
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 11:18:32 +01:00
Dr. Carsten Leue
6a4dfa2c93 fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-11 16:18:55 +01:00
Dr. Carsten Leue
a37f379a3c fix: semantic of MapTo and ChainTo and update tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-11 09:09:44 +01:00
Dr. Carsten Leue
ece0cd135d fix: add more tests and logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-10 18:23:19 +01:00
Dr. Carsten Leue
739b6a284c fix: better slog based logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 17:52:57 +01:00
Dr. Carsten Leue
ba10d8d314 doc: fix docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 13:00:03 +01:00
Dr. Carsten Leue
3d6c419185 fix: add better logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 12:49:44 +01:00
Dr. Carsten Leue
3f4b6292e4 fix: optimize Traverse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 21:35:05 +01:00
274 changed files with 39315 additions and 4663 deletions

View File

@@ -0,0 +1,212 @@
# Example Tests Progress
This document tracks the progress of converting documentation examples into executable example test files.
## Overview
The codebase has 300+ documentation examples across many packages. This document tracks which packages have been completed and which still need work.
## Completed Packages
### Core Packages
- [x] **result** - Created `examples_bind_test.go`, `examples_curry_test.go`, `examples_apply_test.go`
- Files: `bind.go` (10 examples), `curry.go` (5 examples), `apply.go` (2 examples)
- Status: ✅ 17 tests passing
### Utility Packages
- [x] **pair** - Created `examples_test.go`
- Files: `pair.go` (14 examples)
- Status: ✅ 14 tests passing
- [x] **tuple** - Created `examples_test.go`
- Files: `tuple.go` (6 examples)
- Status: ✅ 6 tests passing
### Type Class Packages
- [x] **semigroup** - Created `examples_test.go`
- Files: `semigroup.go` (7 examples)
- Status: ✅ 7 tests passing
### Utility Packages (continued)
- [x] **predicate** - Created `examples_test.go`
- Files: `bool.go` (3 examples), `contramap.go` (1 example)
- Status: ✅ 4 tests passing
### Context Reader Packages
- [x] **idiomatic/context/readerresult** - Created `examples_reader_test.go`, `examples_bind_test.go`
- Files: `reader.go` (8 examples), `bind.go` (14 examples)
- Status: ✅ 22 tests passing
## Summary Statistics
- **Total Example Tests Created**: 74
- **Total Packages Completed**: 7 (result, pair, tuple, semigroup, predicate, idiomatic/context/readerresult)
- **All Tests Status**: ✅ PASSING
### Breakdown by Package
- **result**: 21 tests (bind: 10, curry: 5, apply: 2, array: 4)
- **pair**: 14 tests
- **tuple**: 6 tests
- **semigroup**: 7 tests
- **predicate**: 4 tests
- **idiomatic/context/readerresult**: 22 tests (reader: 8, bind: 14)
## Packages with Existing Examples
These packages already have some example test files:
- result (has `examples_create_test.go`, `examples_extract_test.go`)
- option (has `examples_create_test.go`, `examples_extract_test.go`)
- either (has `examples_create_test.go`, `examples_extract_test.go`)
- ioeither (has `examples_create_test.go`, `examples_do_test.go`, `examples_extract_test.go`)
- ioresult (has `examples_create_test.go`, `examples_do_test.go`, `examples_extract_test.go`)
- lazy (has `example_lazy_test.go`)
- array (has `examples_basic_test.go`, `examples_sort_test.go`, `example_any_test.go`, `example_find_test.go`)
- readerioeither (has `traverse_example_test.go`)
- context/readerioresult (has `flip_example_test.go`)
## Packages Needing Example Tests
### Core Packages (High Priority)
- [ ] **result** - Additional files need examples:
- `apply.go` (2 examples)
- `array.go` (7 examples)
- `core.go` (6 examples)
- `either.go` (26 examples)
- `eq.go` (2 examples)
- `functor.go` (1 example)
- [ ] **option** - Additional files need examples
- [ ] **either** - Additional files need examples
### Reader Packages (High Priority)
- [ ] **reader** - Many examples in:
- `array.go` (12 examples)
- `bind.go` (10 examples)
- `curry.go` (8 examples)
- `flip.go` (2 examples)
- `reader.go` (21 examples)
- [ ] **readeroption** - Examples in:
- `array.go` (3 examples)
- `bind.go` (7 examples)
- `curry.go` (5 examples)
- `flip.go` (2 examples)
- `from.go` (4 examples)
- `reader.go` (18 examples)
- `sequence.go` (4 examples)
- [ ] **readerresult** - Examples in:
- `array.go` (3 examples)
- `bind.go` (24 examples)
- `curry.go` (7 examples)
- `flip.go` (2 examples)
- `from.go` (4 examples)
- `monoid.go` (3 examples)
- [ ] **readereither** - Examples in:
- `array.go` (3 examples)
- `bind.go` (7 examples)
- `flip.go` (3 examples)
- [ ] **readerio** - Examples in:
- `array.go` (3 examples)
- `bind.go` (7 examples)
- `flip.go` (2 examples)
- `logging.go` (4 examples)
- `reader.go` (30 examples)
- [ ] **readerioeither** - Examples in:
- `bind.go` (7 examples)
- `flip.go` (1 example)
- [ ] **readerioresult** - Examples in:
- `array.go` (8 examples)
- `bind.go` (24 examples)
### State Packages
- [ ] **statereaderioeither** - Examples in:
- `bind.go` (5 examples)
- `resource.go` (1 example)
- `state.go` (13 examples)
### Utility Packages
- [ ] **lazy** - Additional examples in:
- `apply.go` (2 examples)
- `bind.go` (7 examples)
- `lazy.go` (10 examples)
- `sequence.go` (4 examples)
- `traverse.go` (2 examples)
- [ ] **pair** - Additional examples in:
- `monad.go` (12 examples)
- `pair.go` (remaining ~20 examples)
- [ ] **tuple** - Examples in:
- `tuple.go` (6 examples)
- [ ] **predicate** - Examples in:
- `bool.go` (3 examples)
- `contramap.go` (1 example)
- `monoid.go` (4 examples)
- [ ] **retry** - Examples in:
- `retry.go` (7 examples)
- [ ] **logging** - Examples in:
- `logger.go` (5 examples)
### Collection Packages
- [ ] **record** - Examples in:
- `bind.go` (3 examples)
### Type Class Packages
- [ ] **semigroup** - Examples in:
- `alt.go` (1 example)
- `apply.go` (1 example)
- `array.go` (4 examples)
- `semigroup.go` (7 examples)
- [ ] **ord** - Examples in:
- `ord.go` (1 example)
## Strategy for Completion
1. **Prioritize by usage**: Focus on core packages (result, option, either) first
2. **Group by package**: Complete all examples for one package before moving to next
3. **Test incrementally**: Run tests after each file to catch errors early
4. **Follow patterns**: Use existing example test files as templates
5. **Document as you go**: Update this file with progress
## Example Test File Template
```go
// Copyright header...
package packagename_test
import (
"fmt"
PKG "github.com/IBM/fp-go/v2/packagename"
)
func ExampleFunctionName() {
// Copy example from doc comment
// Ensure it compiles and produces correct output
fmt.Println(result)
// Output:
// expected output
}
```
## Notes
- Use `F.Constant1[error](defaultValue)` for GetOrElse in result package
- Use `F.Pipe1` instead of `F.Pipe2` when only one transformation
- Check function signatures carefully for type parameters
- Some functions like `BiMap` are capitalized differently than in docs
- **Prefer `R.Eitherize1(func)` over manual error handling** - converts `func(T) (R, error)` to `func(T) Result[R]`
- Example: Use `R.Eitherize1(strconv.Atoi)` instead of manual if/else error checking
- **Add Go documentation comments to all example functions** - Each example should have a comment explaining what it demonstrates
- **Idiomatic vs Non-Idiomatic packages**:
- Non-idiomatic (e.g., `result`): Uses `Result[A]` type (Either monad)
- Idiomatic (e.g., `idiomatic/result`): Uses `(A, error)` tuples (Go-style)
- Context readers use non-idiomatic `Result[A]` internally

View File

@@ -61,6 +61,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
N "github.com/IBM/fp-go/v2/number"
) )
func main() { func main() {
@@ -145,6 +146,8 @@ func main() {
} }
``` ```
## ⚠️ Breaking Changes
### From V1 to V2 ### From V1 to V2
#### 1. Generic Type Aliases #### 1. Generic Type Aliases

View File

@@ -536,3 +536,89 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
func Prepend[A any](head A) Operator[A, A] { func Prepend[A any](head A) Operator[A, A] {
return G.Prepend[Operator[A, A]](head) return G.Prepend[Operator[A, A]](head)
} }
// Reverse returns a new slice with elements in reverse order.
// This function creates a new slice containing all elements from the input slice
// in reverse order, without modifying the original slice.
//
// Type Parameters:
// - A: The type of elements in the slice
//
// Parameters:
// - as: The input slice to reverse
//
// Returns:
// - A new slice with elements in reverse order
//
// Behavior:
// - Creates a new slice with the same length as the input
// - Copies elements from the input slice in reverse order
// - Does not modify the original slice
// - Returns an empty slice if the input is empty
// - Returns a single-element slice unchanged if input has one element
//
// Example:
//
// numbers := []int{1, 2, 3, 4, 5}
// reversed := array.Reverse(numbers)
// // reversed: []int{5, 4, 3, 2, 1}
// // numbers: []int{1, 2, 3, 4, 5} (unchanged)
//
// Example with strings:
//
// words := []string{"hello", "world", "foo", "bar"}
// reversed := array.Reverse(words)
// // reversed: []string{"bar", "foo", "world", "hello"}
//
// Example with empty slice:
//
// empty := []int{}
// reversed := array.Reverse(empty)
// // reversed: []int{} (empty slice)
//
// Example with single element:
//
// single := []string{"only"}
// reversed := array.Reverse(single)
// // reversed: []string{"only"}
//
// Use cases:
// - Reversing the order of elements for display or processing
// - Implementing stack-like behavior (LIFO)
// - Processing data in reverse chronological order
// - Reversing transformation pipelines
// - Creating palindrome checks
// - Implementing undo/redo functionality
//
// Example with processing in reverse:
//
// events := []string{"start", "middle", "end"}
// reversed := array.Reverse(events)
// // Process events in reverse order
// for _, event := range reversed {
// fmt.Println(event) // Prints: "end", "middle", "start"
// }
//
// Example with functional composition:
//
// numbers := []int{1, 2, 3, 4, 5}
// result := F.Pipe2(
// numbers,
// array.Map(N.Mul(2)),
// array.Reverse,
// )
// // result: []int{10, 8, 6, 4, 2}
//
// Performance:
// - Time complexity: O(n) where n is the length of the slice
// - Space complexity: O(n) for the new slice
// - Does not allocate if the input slice is empty
//
// Note: This function is immutable - it does not modify the original slice.
// If you need to reverse a slice in-place, consider using a different approach
// or modifying the slice directly.
//
//go:inline
func Reverse[A any](as []A) []A {
return G.Reverse(as)
}

View File

@@ -22,6 +22,7 @@ import (
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils" "github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string" S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple" T "github.com/IBM/fp-go/v2/tuple"
@@ -214,3 +215,262 @@ func ExampleFoldMap() {
// Output: ABC // Output: ABC
} }
// TestReverse tests the Reverse function
func TestReverse(t *testing.T) {
t.Run("Reverse integers", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Reverse(input)
expected := []int{5, 4, 3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Reverse strings", func(t *testing.T) {
input := []string{"hello", "world", "foo", "bar"}
result := Reverse(input)
expected := []string{"bar", "foo", "world", "hello"}
assert.Equal(t, expected, result)
})
t.Run("Reverse empty slice", func(t *testing.T) {
input := []int{}
result := Reverse(input)
assert.Equal(t, []int{}, result)
})
t.Run("Reverse single element", func(t *testing.T) {
input := []string{"only"}
result := Reverse(input)
assert.Equal(t, []string{"only"}, result)
})
t.Run("Reverse two elements", func(t *testing.T) {
input := []int{1, 2}
result := Reverse(input)
assert.Equal(t, []int{2, 1}, result)
})
t.Run("Does not modify original slice", func(t *testing.T) {
original := []int{1, 2, 3, 4, 5}
originalCopy := []int{1, 2, 3, 4, 5}
_ = Reverse(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Reverse with floats", func(t *testing.T) {
input := []float64{1.1, 2.2, 3.3}
result := Reverse(input)
expected := []float64{3.3, 2.2, 1.1}
assert.Equal(t, expected, result)
})
t.Run("Reverse with structs", func(t *testing.T) {
type Person struct {
Name string
Age int
}
input := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
result := Reverse(input)
expected := []Person{
{"Charlie", 35},
{"Bob", 25},
{"Alice", 30},
}
assert.Equal(t, expected, result)
})
t.Run("Reverse with pointers", func(t *testing.T) {
a, b, c := 1, 2, 3
input := []*int{&a, &b, &c}
result := Reverse(input)
assert.Equal(t, []*int{&c, &b, &a}, result)
})
t.Run("Double reverse returns original order", func(t *testing.T) {
original := []int{1, 2, 3, 4, 5}
reversed := Reverse(original)
doubleReversed := Reverse(reversed)
assert.Equal(t, original, doubleReversed)
})
t.Run("Reverse with large slice", func(t *testing.T) {
input := MakeBy(1000, F.Identity[int])
result := Reverse(input)
// Check first and last elements
assert.Equal(t, 999, result[0])
assert.Equal(t, 0, result[999])
// Check length
assert.Equal(t, 1000, len(result))
})
t.Run("Reverse palindrome", func(t *testing.T) {
input := []int{1, 2, 3, 2, 1}
result := Reverse(input)
assert.Equal(t, input, result)
})
}
// TestReverseComposition tests Reverse with other array operations
func TestReverseComposition(t *testing.T) {
t.Run("Reverse after Map", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := F.Pipe2(
input,
Map(N.Mul(2)),
Reverse[int],
)
expected := []int{10, 8, 6, 4, 2}
assert.Equal(t, expected, result)
})
t.Run("Map after Reverse", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := F.Pipe2(
input,
Reverse[int],
Map(N.Mul(2)),
)
expected := []int{10, 8, 6, 4, 2}
assert.Equal(t, expected, result)
})
t.Run("Reverse with Filter", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6}
result := F.Pipe2(
input,
Filter(func(n int) bool { return n%2 == 0 }),
Reverse[int],
)
expected := []int{6, 4, 2}
assert.Equal(t, expected, result)
})
t.Run("Reverse with Reduce", func(t *testing.T) {
input := []string{"a", "b", "c"}
reversed := Reverse(input)
result := Reduce(func(acc, val string) string {
return acc + val
}, "")(reversed)
assert.Equal(t, "cba", result)
})
t.Run("Reverse with Flatten", func(t *testing.T) {
input := [][]int{{1, 2}, {3, 4}, {5, 6}}
result := F.Pipe2(
input,
Reverse[[]int],
Flatten[int],
)
expected := []int{5, 6, 3, 4, 1, 2}
assert.Equal(t, expected, result)
})
}
// TestReverseUseCases demonstrates practical use cases for Reverse
func TestReverseUseCases(t *testing.T) {
t.Run("Process events in reverse chronological order", func(t *testing.T) {
events := []string{"2024-01-01", "2024-01-02", "2024-01-03"}
reversed := Reverse(events)
// Most recent first
assert.Equal(t, "2024-01-03", reversed[0])
assert.Equal(t, "2024-01-01", reversed[2])
})
t.Run("Implement stack behavior (LIFO)", func(t *testing.T) {
stack := []int{1, 2, 3, 4, 5}
reversed := Reverse(stack)
// Pop from reversed (LIFO)
assert.Equal(t, 5, reversed[0])
assert.Equal(t, 4, reversed[1])
})
t.Run("Reverse string characters", func(t *testing.T) {
chars := []rune("hello")
reversed := Reverse(chars)
result := string(reversed)
assert.Equal(t, "olleh", result)
})
t.Run("Check palindrome", func(t *testing.T) {
word := []rune("racecar")
reversed := Reverse(word)
assert.Equal(t, word, reversed)
notPalindrome := []rune("hello")
reversedNot := Reverse(notPalindrome)
assert.NotEqual(t, notPalindrome, reversedNot)
})
t.Run("Reverse transformation pipeline", func(t *testing.T) {
// Apply transformations in reverse order
numbers := []int{1, 2, 3}
// Normal: add 10, then multiply by 2
normal := F.Pipe2(
numbers,
Map(N.Add(10)),
Map(N.Mul(2)),
)
// Reversed order of operations
reversed := F.Pipe2(
numbers,
Map(N.Mul(2)),
Map(N.Add(10)),
)
assert.NotEqual(t, normal, reversed)
assert.Equal(t, []int{22, 24, 26}, normal)
assert.Equal(t, []int{12, 14, 16}, reversed)
})
}
// TestReverseProperties tests mathematical properties of Reverse
func TestReverseProperties(t *testing.T) {
t.Run("Involution property: Reverse(Reverse(x)) == x", func(t *testing.T) {
testCases := [][]int{
{1, 2, 3, 4, 5},
{1},
{},
{1, 2},
{5, 4, 3, 2, 1},
}
for _, original := range testCases {
result := Reverse(Reverse(original))
assert.Equal(t, original, result)
}
})
t.Run("Length preservation: len(Reverse(x)) == len(x)", func(t *testing.T) {
testCases := [][]int{
{1, 2, 3, 4, 5},
{1},
{},
MakeBy(100, F.Identity[int]),
}
for _, input := range testCases {
result := Reverse(input)
assert.Equal(t, len(input), len(result))
}
})
t.Run("First element becomes last", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Reverse(input)
if len(input) > 0 {
assert.Equal(t, input[0], result[len(result)-1])
assert.Equal(t, input[len(input)-1], result[0])
}
})
}

View File

@@ -19,7 +19,7 @@ import (
E "github.com/IBM/fp-go/v2/eq" E "github.com/IBM/fp-go/v2/eq"
) )
func equals[T any](left []T, right []T, eq func(T, T) bool) bool { func equals[T any](left, right []T, eq func(T, T) bool) bool {
if len(left) != len(right) { if len(left) != len(right) {
return false return false
} }

View File

@@ -140,22 +140,27 @@ func Empty[GA ~[]A, A any]() GA {
return array.Empty[GA]() return array.Empty[GA]()
} }
//go:inline
func UpsertAt[GA ~[]A, A any](a A) func(GA) GA { func UpsertAt[GA ~[]A, A any](a A) func(GA) GA {
return array.UpsertAt[GA](a) return array.UpsertAt[GA](a)
} }
//go:inline
func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB { func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB {
return array.MonadMap[GA, GB](as, f) return array.MonadMap[GA, GB](as, f)
} }
//go:inline
func Map[GA ~[]A, GB ~[]B, A, B any](f func(a A) B) func(GA) GB { func Map[GA ~[]A, GB ~[]B, A, B any](f func(a A) B) func(GA) GB {
return array.Map[GA, GB](f) return array.Map[GA, GB](f)
} }
//go:inline
func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(int, A) B) GB { func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(int, A) B) GB {
return array.MonadMapWithIndex[GA, GB](as, f) return array.MonadMapWithIndex[GA, GB](as, f)
} }
//go:inline
func MapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) B) func(GA) GB { func MapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
return F.Bind2nd(MonadMapWithIndex[GA, GB, A, B], f) return F.Bind2nd(MonadMapWithIndex[GA, GB, A, B], f)
} }
@@ -297,7 +302,7 @@ func MatchLeft[AS ~[]A, A, B any](onEmpty func() B, onNonEmpty func(A, AS) B) fu
} }
//go:inline //go:inline
func Slice[AS ~[]A, A any](start int, end int) func(AS) AS { func Slice[AS ~[]A, A any](start, end int) func(AS) AS {
return array.Slice[AS](start, end) return array.Slice[AS](start, end)
} }
@@ -361,6 +366,12 @@ func Flap[FAB ~func(A) B, GFAB ~[]FAB, GB ~[]B, A, B any](a A) func(GFAB) GB {
return FC.Flap(Map[GFAB, GB], a) return FC.Flap(Map[GFAB, GB], a)
} }
//go:inline
func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO { func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
return array.Prepend[ENDO](head) return array.Prepend[ENDO](head)
} }
//go:inline
func Reverse[GT ~[]T, T any](as GT) GT {
return array.Reverse(as)
}

View File

@@ -18,14 +18,11 @@ package nonempty
import ( import (
G "github.com/IBM/fp-go/v2/array/generic" G "github.com/IBM/fp-go/v2/array/generic"
EM "github.com/IBM/fp-go/v2/endomorphism" EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array" "github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/semigroup" S "github.com/IBM/fp-go/v2/semigroup"
) )
// NonEmptyArray represents an array with at least one element
type NonEmptyArray[A any] []A
// Of constructs a single element array // Of constructs a single element array
func Of[A any](first A) NonEmptyArray[A] { func Of[A any](first A) NonEmptyArray[A] {
return G.Of[NonEmptyArray[A]](first) return G.Of[NonEmptyArray[A]](first)
@@ -44,20 +41,24 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
return buffer return buffer
} }
//go:inline
func IsEmpty[A any](_ NonEmptyArray[A]) bool { func IsEmpty[A any](_ NonEmptyArray[A]) bool {
return false return false
} }
//go:inline
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool { func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
return true return true
} }
//go:inline
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] { func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f) return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
} }
func Map[A, B any](f func(a A) B) func(NonEmptyArray[A]) NonEmptyArray[B] { //go:inline
return F.Bind2nd(MonadMap[A, B], f) func Map[A, B any](f func(a A) B) Operator[A, B] {
return G.Map[NonEmptyArray[A], NonEmptyArray[B]](f)
} }
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B { func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
@@ -72,22 +73,27 @@ func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
} }
} }
//go:inline
func Tail[A any](as NonEmptyArray[A]) []A { func Tail[A any](as NonEmptyArray[A]) []A {
return as[1:] return as[1:]
} }
//go:inline
func Head[A any](as NonEmptyArray[A]) A { func Head[A any](as NonEmptyArray[A]) A {
return as[0] return as[0]
} }
//go:inline
func First[A any](as NonEmptyArray[A]) A { func First[A any](as NonEmptyArray[A]) A {
return as[0] return as[0]
} }
//go:inline
func Last[A any](as NonEmptyArray[A]) A { func Last[A any](as NonEmptyArray[A]) A {
return as[len(as)-1] return as[len(as)-1]
} }
//go:inline
func Size[A any](as NonEmptyArray[A]) int { func Size[A any](as NonEmptyArray[A]) int {
return G.Size(as) return G.Size(as)
} }
@@ -96,11 +102,11 @@ func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
return G.Flatten(mma) return G.Flatten(mma)
} }
func MonadChain[A, B any](fa NonEmptyArray[A], f func(a A) NonEmptyArray[B]) NonEmptyArray[B] { func MonadChain[A, B any](fa NonEmptyArray[A], f Kleisli[A, B]) NonEmptyArray[B] {
return G.MonadChain(fa, f) return G.MonadChain(fa, f)
} }
func Chain[A, B any](f func(A) NonEmptyArray[B]) func(NonEmptyArray[A]) NonEmptyArray[B] { func Chain[A, B any](f func(A) NonEmptyArray[B]) Operator[A, B] {
return G.Chain[NonEmptyArray[A]](f) return G.Chain[NonEmptyArray[A]](f)
} }
@@ -134,3 +140,89 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] { func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head) return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
} }
// ToNonEmptyArray attempts to convert a regular slice into a NonEmptyArray.
// This function provides a safe way to create a NonEmptyArray from a slice that might be empty,
// returning an Option type to handle the case where the input slice is empty.
//
// Type Parameters:
// - A: The element type of the array
//
// Parameters:
// - as: A regular slice that may or may not be empty
//
// Returns:
// - Option[NonEmptyArray[A]]: Some(NonEmptyArray) if the input slice is non-empty, None if empty
//
// Behavior:
// - If the input slice is empty, returns None
// - If the input slice has at least one element, wraps it in Some and returns it as a NonEmptyArray
// - The conversion is a type cast, so no data is copied
//
// Example:
//
// // Convert non-empty slice
// numbers := []int{1, 2, 3}
// result := ToNonEmptyArray(numbers) // Some(NonEmptyArray[1, 2, 3])
//
// // Convert empty slice
// empty := []int{}
// result := ToNonEmptyArray(empty) // None
//
// // Use with Option methods
// numbers := []int{1, 2, 3}
// result := ToNonEmptyArray(numbers)
// if O.IsSome(result) {
// nea := O.GetOrElse(F.Constant(From(0)))(result)
// head := Head(nea) // 1
// }
//
// Use cases:
// - Safely converting user input or external data to NonEmptyArray
// - Validating that a collection has at least one element before processing
// - Converting results from functions that return regular slices
// - Ensuring type safety when working with collections that must not be empty
//
// Example with validation:
//
// func processItems(items []string) Option[string] {
// return F.Pipe2(
// items,
// ToNonEmptyArray[string],
// O.Map(func(nea NonEmptyArray[string]) string {
// return Head(nea) // Safe to get head since we know it's non-empty
// }),
// )
// }
//
// Example with error handling:
//
// items := []int{1, 2, 3}
// result := ToNonEmptyArray(items)
// switch {
// case O.IsSome(result):
// nea := O.GetOrElse(F.Constant(From(0)))(result)
// fmt.Println("First item:", Head(nea))
// case O.IsNone(result):
// fmt.Println("Array is empty")
// }
//
// Example with chaining:
//
// // Process only if non-empty
// result := F.Pipe3(
// []int{1, 2, 3},
// ToNonEmptyArray[int],
// O.Map(Map(func(x int) int { return x * 2 })),
// O.Map(Head[int]),
// ) // Some(2)
//
// Note: This function is particularly useful when working with APIs or functions
// that return regular slices but you need the type-level guarantee that the
// collection is non-empty for subsequent operations.
func ToNonEmptyArray[A any](as []A) Option[NonEmptyArray[A]] {
if G.IsEmpty(as) {
return option.None[NonEmptyArray[A]]()
}
return option.Some(NonEmptyArray[A](as))
}

View File

@@ -0,0 +1,370 @@
// 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 nonempty
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestToNonEmptyArray tests the ToNonEmptyArray function
func TestToNonEmptyArray(t *testing.T) {
t.Run("Convert non-empty slice of integers", func(t *testing.T) {
input := []int{1, 2, 3}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(0)))(result)
assert.Equal(t, 3, Size(nea))
assert.Equal(t, 1, Head(nea))
assert.Equal(t, 3, Last(nea))
})
t.Run("Convert empty slice returns None", func(t *testing.T) {
input := []int{}
result := ToNonEmptyArray(input)
assert.True(t, O.IsNone(result))
})
t.Run("Convert single element slice", func(t *testing.T) {
input := []string{"hello"}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From("")))(result)
assert.Equal(t, 1, Size(nea))
assert.Equal(t, "hello", Head(nea))
})
t.Run("Convert non-empty slice of strings", func(t *testing.T) {
input := []string{"a", "b", "c", "d"}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From("")))(result)
assert.Equal(t, 4, Size(nea))
assert.Equal(t, "a", Head(nea))
assert.Equal(t, "d", Last(nea))
})
t.Run("Convert nil slice returns None", func(t *testing.T) {
var input []int
result := ToNonEmptyArray(input)
assert.True(t, O.IsNone(result))
})
t.Run("Convert slice with struct elements", func(t *testing.T) {
type Person struct {
Name string
Age int
}
input := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(Person{})))(result)
assert.Equal(t, 2, Size(nea))
assert.Equal(t, "Alice", Head(nea).Name)
})
t.Run("Convert slice with pointer elements", func(t *testing.T) {
val1, val2 := 10, 20
input := []*int{&val1, &val2}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From[*int](nil)))(result)
assert.Equal(t, 2, Size(nea))
assert.Equal(t, 10, *Head(nea))
})
t.Run("Convert large slice", func(t *testing.T) {
input := make([]int, 1000)
for i := range input {
input[i] = i
}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(0)))(result)
assert.Equal(t, 1000, Size(nea))
assert.Equal(t, 0, Head(nea))
assert.Equal(t, 999, Last(nea))
})
t.Run("Convert slice with float64 elements", func(t *testing.T) {
input := []float64{1.5, 2.5, 3.5}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(0.0)))(result)
assert.Equal(t, 3, Size(nea))
assert.Equal(t, 1.5, Head(nea))
})
t.Run("Convert slice with boolean elements", func(t *testing.T) {
input := []bool{true, false, true}
result := ToNonEmptyArray(input)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(false)))(result)
assert.Equal(t, 3, Size(nea))
assert.True(t, Head(nea))
})
}
// TestToNonEmptyArrayWithOption tests ToNonEmptyArray with Option operations
func TestToNonEmptyArrayWithOption(t *testing.T) {
t.Run("Chain with Map to process elements", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
ToNonEmptyArray[int],
O.Map(Map(func(x int) int { return x * 2 })),
)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(0)))(result)
assert.Equal(t, 2, Head(nea))
assert.Equal(t, 6, Last(nea))
})
t.Run("Chain with Map to get head", func(t *testing.T) {
input := []string{"first", "second", "third"}
result := F.Pipe2(
input,
ToNonEmptyArray[string],
O.Map(Head[string]),
)
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, "first", value)
})
t.Run("GetOrElse with default value for empty slice", func(t *testing.T) {
input := []int{}
defaultValue := From(42)
result := F.Pipe2(
input,
ToNonEmptyArray[int],
O.GetOrElse(F.Constant(defaultValue)),
)
assert.Equal(t, 1, Size(result))
assert.Equal(t, 42, Head(result))
})
t.Run("GetOrElse with default value for non-empty slice", func(t *testing.T) {
input := []int{1, 2, 3}
defaultValue := From(42)
result := F.Pipe2(
input,
ToNonEmptyArray[int],
O.GetOrElse(F.Constant(defaultValue)),
)
assert.Equal(t, 3, Size(result))
assert.Equal(t, 1, Head(result))
})
t.Run("Fold with Some case", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
ToNonEmptyArray[int],
O.Fold(
F.Constant(0),
func(nea NonEmptyArray[int]) int { return Head(nea) },
),
)
assert.Equal(t, 1, result)
})
t.Run("Fold with None case", func(t *testing.T) {
input := []int{}
result := F.Pipe2(
input,
ToNonEmptyArray[int],
O.Fold(
F.Constant(-1),
func(nea NonEmptyArray[int]) int { return Head(nea) },
),
)
assert.Equal(t, -1, result)
})
}
// TestToNonEmptyArrayComposition tests composing ToNonEmptyArray with other operations
func TestToNonEmptyArrayComposition(t *testing.T) {
t.Run("Compose with filter-like operation", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
// Filter even numbers then convert
filtered := []int{}
for _, x := range input {
if x%2 == 0 {
filtered = append(filtered, x)
}
}
result := ToNonEmptyArray(filtered)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(0)))(result)
assert.Equal(t, 2, Size(nea))
assert.Equal(t, 2, Head(nea))
})
t.Run("Compose with map operation before conversion", func(t *testing.T) {
input := []int{1, 2, 3}
// Map then convert
mapped := make([]int, len(input))
for i, x := range input {
mapped[i] = x * 10
}
result := ToNonEmptyArray(mapped)
assert.True(t, O.IsSome(result))
nea := O.GetOrElse(F.Constant(From(0)))(result)
assert.Equal(t, 10, Head(nea))
assert.Equal(t, 30, Last(nea))
})
t.Run("Chain multiple Option operations", func(t *testing.T) {
input := []int{5, 10, 15}
result := F.Pipe3(
input,
ToNonEmptyArray[int],
O.Map(Map(func(x int) int { return x / 5 })),
O.Map(func(nea NonEmptyArray[int]) int {
return Head(nea) + Last(nea)
}),
)
assert.True(t, O.IsSome(result))
value := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 4, value) // 1 + 3
})
}
// TestToNonEmptyArrayUseCases demonstrates practical use cases
func TestToNonEmptyArrayUseCases(t *testing.T) {
t.Run("Validate user input has at least one item", func(t *testing.T) {
// Simulate user input
userInput := []string{"item1", "item2"}
result := ToNonEmptyArray(userInput)
if O.IsSome(result) {
nea := O.GetOrElse(F.Constant(From("")))(result)
firstItem := Head(nea)
assert.Equal(t, "item1", firstItem)
} else {
t.Fatal("Expected Some but got None")
}
})
t.Run("Process only non-empty collections", func(t *testing.T) {
processItems := func(items []int) Option[int] {
return F.Pipe2(
items,
ToNonEmptyArray[int],
O.Map(func(nea NonEmptyArray[int]) int {
// Safe to use Head since we know it's non-empty
return Head(nea) * 2
}),
)
}
result1 := processItems([]int{5, 10, 15})
assert.True(t, O.IsSome(result1))
assert.Equal(t, 10, O.GetOrElse(F.Constant(0))(result1))
result2 := processItems([]int{})
assert.True(t, O.IsNone(result2))
})
t.Run("Convert API response to NonEmptyArray", func(t *testing.T) {
// Simulate API response
type APIResponse struct {
Items []string
}
response := APIResponse{Items: []string{"data1", "data2", "data3"}}
result := F.Pipe2(
response.Items,
ToNonEmptyArray[string],
O.Map(func(nea NonEmptyArray[string]) string {
return "First item: " + Head(nea)
}),
)
assert.True(t, O.IsSome(result))
message := O.GetOrElse(F.Constant("No items"))(result)
assert.Equal(t, "First item: data1", message)
})
t.Run("Ensure collection is non-empty before processing", func(t *testing.T) {
calculateAverage := func(numbers []float64) Option[float64] {
return F.Pipe2(
numbers,
ToNonEmptyArray[float64],
O.Map(func(nea NonEmptyArray[float64]) float64 {
sum := 0.0
for _, n := range nea {
sum += n
}
return sum / float64(Size(nea))
}),
)
}
result1 := calculateAverage([]float64{10.0, 20.0, 30.0})
assert.True(t, O.IsSome(result1))
assert.Equal(t, 20.0, O.GetOrElse(F.Constant(0.0))(result1))
result2 := calculateAverage([]float64{})
assert.True(t, O.IsNone(result2))
})
t.Run("Safe head extraction with type guarantee", func(t *testing.T) {
getFirstOrDefault := func(items []string, defaultValue string) string {
return F.Pipe2(
items,
ToNonEmptyArray[string],
O.Fold(
F.Constant(defaultValue),
Head[string],
),
)
}
result1 := getFirstOrDefault([]string{"a", "b", "c"}, "default")
assert.Equal(t, "a", result1)
result2 := getFirstOrDefault([]string{}, "default")
assert.Equal(t, "default", result2)
})
}

View File

@@ -0,0 +1,15 @@
package nonempty
import "github.com/IBM/fp-go/v2/option"
type (
// NonEmptyArray represents an array with at least one element
NonEmptyArray[A any] []A
Kleisli[A, B any] = func(A) NonEmptyArray[B]
Operator[A, B any] = Kleisli[NonEmptyArray[A], B]
Option[A any] = option.Option[A]
)

View File

@@ -22,6 +22,7 @@ import (
"github.com/IBM/fp-go/v2/optics/prism" "github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result" "github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
) )
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
@@ -334,7 +335,7 @@ func TestThat(t *testing.T) {
}) })
t.Run("should work with string predicates", func(t *testing.T) { t.Run("should work with string predicates", func(t *testing.T) {
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' } startsWithH := func(s string) bool { return S.IsNonEmpty(s) && s[0] == 'h' }
result := That(startsWithH)("hello")(t) result := That(startsWithH)("hello")(t)
if !result { if !result {
t.Error("Expected That to pass for string predicate") t.Error("Expected That to pass for string predicate")
@@ -484,7 +485,7 @@ func TestLocal(t *testing.T) {
t.Run("should compose with other assertions", func(t *testing.T) { t.Run("should compose with other assertions", func(t *testing.T) {
// Create multiple focused assertions // Create multiple focused assertions
nameNotEmpty := Local(func(u User) string { return u.Name })( nameNotEmpty := Local(func(u User) string { return u.Name })(
That(func(name string) bool { return len(name) > 0 }), That(S.IsNonEmpty),
) )
ageInRange := Local(func(u User) int { return u.Age })( ageInRange := Local(func(u User) int { return u.Age })(
That(func(age int) bool { return age >= 18 && age <= 100 }), That(func(age int) bool { return age >= 18 && age <= 100 }),

View File

@@ -27,13 +27,15 @@ import (
"strings" "strings"
"text/template" "text/template"
S "github.com/IBM/fp-go/v2/string"
C "github.com/urfave/cli/v2" C "github.com/urfave/cli/v2"
) )
const ( const (
keyLensDir = "dir" keyLensDir = "dir"
keyVerbose = "verbose" keyVerbose = "verbose"
lensAnnotation = "fp-go:Lens" keyIncludeTestFile = "include-test-files"
lensAnnotation = "fp-go:Lens"
) )
var ( var (
@@ -49,6 +51,13 @@ var (
Value: false, Value: false,
Usage: "Enable verbose output", Usage: "Enable verbose output",
} }
flagIncludeTestFiles = &C.BoolFlag{
Name: keyIncludeTestFile,
Aliases: []string{"t"},
Value: false,
Usage: "Include test files (*_test.go) when scanning for annotated types",
}
) )
// structInfo holds information about a struct that needs lens generation // structInfo holds information about a struct that needs lens generation
@@ -67,6 +76,7 @@ type fieldInfo struct {
BaseType string // TypeName without leading * for pointer types BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag IsOptional bool // true if field is a pointer or has json omitempty tag
IsComparable bool // true if the type is comparable (can use ==) IsComparable bool // true if the type is comparable (can use ==)
IsEmbedded bool // true if this field comes from an embedded struct
} }
// templateData holds data for template rendering // templateData holds data for template rendering
@@ -80,12 +90,12 @@ const lensStructTemplate = `
type {{.Name}}Lenses{{.TypeParams}} struct { type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields // mandatory fields
{{- range .Fields}} {{- range .Fields}}
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}] {{.Name}} __lens.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}} {{- end}}
// optional fields // optional fields
{{- range .Fields}} {{- range .Fields}}
{{- if .IsComparable}} {{- if .IsComparable}}
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}] {{.Name}}O __lens_option.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}} {{- end}}
{{- end}} {{- end}}
} }
@@ -94,13 +104,24 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
type {{.Name}}RefLenses{{.TypeParams}} struct { type {{.Name}}RefLenses{{.TypeParams}} struct {
// mandatory fields // mandatory fields
{{- range .Fields}} {{- range .Fields}}
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}] {{.Name}} __lens.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}} {{- end}}
// optional fields // optional fields
{{- range .Fields}} {{- range .Fields}}
{{- if .IsComparable}} {{- if .IsComparable}}
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}] {{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}} {{- end}}
{{- end}}
// prisms
{{- range .Fields}}
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
type {{.Name}}Prisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}} {{- end}}
} }
` `
@@ -110,15 +131,16 @@ const lensConstructorTemplate = `
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} { func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
// mandatory lenses // mandatory lenses
{{- range .Fields}} {{- range .Fields}}
lens{{.Name}} := L.MakeLens( lens{{.Name}} := __lens.MakeLensWithName(
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} }, func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s }, func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
) )
{{- end}} {{- end}}
// optional lenses // optional lenses
{{- range .Fields}} {{- range .Fields}}
{{- if .IsComparable}} {{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}}) lens{{.Name}}O := __lens_option.FromIso[{{$.Name}}{{$.TypeParamNames}}](__iso_option.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}} {{- end}}
{{- end}} {{- end}}
return {{.Name}}Lenses{{.TypeParamNames}}{ return {{.Name}}Lenses{{.TypeParamNames}}{
@@ -140,21 +162,23 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
// mandatory lenses // mandatory lenses
{{- range .Fields}} {{- range .Fields}}
{{- if .IsComparable}} {{- if .IsComparable}}
lens{{.Name}} := L.MakeLensStrict( lens{{.Name}} := __lens.MakeLensStrictWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} }, func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s }, func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
) )
{{- else}} {{- else}}
lens{{.Name}} := L.MakeLensRef( lens{{.Name}} := __lens.MakeLensRefWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} }, func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s }, func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
) )
{{- end}} {{- end}}
{{- end}} {{- end}}
// optional lenses // optional lenses
{{- range .Fields}} {{- range .Fields}}
{{- if .IsComparable}} {{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}}) lens{{.Name}}O := __lens_option.FromIso[*{{$.Name}}{{$.TypeParamNames}}](__iso_option.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}} {{- end}}
{{- end}} {{- end}}
return {{.Name}}RefLenses{{.TypeParamNames}}{ return {{.Name}}RefLenses{{.TypeParamNames}}{
@@ -170,6 +194,47 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
{{- end}} {{- end}}
} }
} }
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.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}}Prisms{{.TypeParamNames}} {
{{- range .Fields}}
{{.Name}}: _prism{{.Name}},
{{- end}}
}
}
` `
var ( var (
@@ -439,7 +504,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
return results return results
} }
if typeName == "" || typeIdent == nil { if S.IsEmpty(typeName) || typeIdent == nil {
return results return results
} }
@@ -494,6 +559,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
BaseType: baseType, BaseType: baseType,
IsOptional: isOptional, IsOptional: isOptional,
IsComparable: isComparable, IsComparable: isComparable,
IsEmbedded: true,
}, },
fieldType: field.Type, fieldType: field.Type,
}) })
@@ -695,7 +761,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
} }
// generateLensHelpers scans a directory for Go files and generates lens code // generateLensHelpers scans a directory for Go files and generates lens code
func generateLensHelpers(dir, filename string, verbose bool) error { func generateLensHelpers(dir, filename string, verbose, includeTestFiles bool) error {
// Get absolute path // Get absolute path
absDir, err := filepath.Abs(dir) absDir, err := filepath.Abs(dir)
if err != nil { if err != nil {
@@ -716,21 +782,34 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
log.Printf("Found %d Go files", len(files)) log.Printf("Found %d Go files", len(files))
} }
// Parse all files and collect structs // Parse all files and collect structs, separating test and non-test files
var allStructs []structInfo var regularStructs []structInfo
var testStructs []structInfo
var packageName string var packageName string
for _, file := range files { for _, file := range files {
// Skip generated files and test files baseName := filepath.Base(file)
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
// Skip generated lens files (both regular and test)
if strings.HasPrefix(baseName, "gen_lens") && strings.HasSuffix(baseName, ".go") {
if verbose { if verbose {
log.Printf("Skipping file: %s", filepath.Base(file)) log.Printf("Skipping generated lens file: %s", baseName)
}
continue
}
isTestFile := strings.HasSuffix(file, "_test.go")
// Skip test files unless includeTestFiles is true
if isTestFile && !includeTestFiles {
if verbose {
log.Printf("Skipping test file: %s", baseName)
} }
continue continue
} }
if verbose { if verbose {
log.Printf("Parsing file: %s", filepath.Base(file)) log.Printf("Parsing file: %s", baseName)
} }
structs, pkg, err := parseFile(file) structs, pkg, err := parseFile(file)
@@ -740,27 +819,52 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
} }
if verbose && len(structs) > 0 { if verbose && len(structs) > 0 {
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file)) log.Printf("Found %d annotated struct(s) in %s", len(structs), baseName)
for _, s := range structs { for _, s := range structs {
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields)) log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
} }
} }
if packageName == "" { if S.IsEmpty(packageName) {
packageName = pkg packageName = pkg
} }
allStructs = append(allStructs, structs...) // Separate structs based on source file type
if isTestFile {
testStructs = append(testStructs, structs...)
} else {
regularStructs = append(regularStructs, structs...)
}
} }
if len(allStructs) == 0 { if len(regularStructs) == 0 && len(testStructs) == 0 {
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir) log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
return nil return nil
} }
// Generate regular lens file if there are regular structs
if len(regularStructs) > 0 {
if err := generateLensFile(absDir, filename, packageName, regularStructs, verbose); err != nil {
return err
}
}
// Generate test lens file if there are test structs
if len(testStructs) > 0 {
testFilename := strings.TrimSuffix(filename, ".go") + "_test.go"
if err := generateLensFile(absDir, testFilename, packageName, testStructs, verbose); err != nil {
return err
}
}
return nil
}
// generateLensFile generates a lens file for the given structs
func generateLensFile(absDir, filename, packageName string, structs []structInfo, verbose bool) error {
// Collect all unique imports from all structs // Collect all unique imports from all structs
allImports := make(map[string]string) // import path -> alias allImports := make(map[string]string) // import path -> alias
for _, s := range allStructs { for _, s := range structs {
for importPath, alias := range s.Imports { for importPath, alias := range s.Imports {
allImports[importPath] = alias allImports[importPath] = alias
} }
@@ -774,7 +878,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
} }
defer f.Close() defer f.Close()
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs)) log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(structs))
// Write header // Write header
writePackage(f, packageName) writePackage(f, packageName)
@@ -782,10 +886,11 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
// Write imports // Write imports
f.WriteString("import (\n") f.WriteString("import (\n")
// Standard fp-go imports always needed // Standard fp-go imports always needed
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n") f.WriteString("\t__lens \"github.com/IBM/fp-go/v2/optics/lens\"\n")
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n") f.WriteString("\t__option \"github.com/IBM/fp-go/v2/option\"\n")
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n") f.WriteString("\t__prism \"github.com/IBM/fp-go/v2/optics/prism\"\n")
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n") f.WriteString("\t__lens_option \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\t__iso_option \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// Add additional imports collected from field types // Add additional imports collected from field types
for importPath, alias := range allImports { for importPath, alias := range allImports {
@@ -795,7 +900,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
f.WriteString(")\n") f.WriteString(")\n")
// Generate lens code for each struct using templates // Generate lens code for each struct using templates
for _, s := range allStructs { for _, s := range structs {
var buf bytes.Buffer var buf bytes.Buffer
// Generate struct type // Generate struct type
@@ -827,12 +932,14 @@ func LensCommand() *C.Command {
flagLensDir, flagLensDir,
flagFilename, flagFilename,
flagVerbose, flagVerbose,
flagIncludeTestFiles,
}, },
Action: func(ctx *C.Context) error { Action: func(ctx *C.Context) error {
return generateLensHelpers( return generateLensHelpers(
ctx.String(keyLensDir), ctx.String(keyLensDir),
ctx.String(keyFilename), ctx.String(keyFilename),
ctx.Bool(keyVerbose), ctx.Bool(keyVerbose),
ctx.Bool(keyIncludeTestFile),
) )
}, },
} }

View File

@@ -25,6 +25,7 @@ import (
"strings" "strings"
"testing" "testing"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -60,7 +61,7 @@ func TestHasLensAnnotation(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var doc *ast.CommentGroup var doc *ast.CommentGroup
if tt.comment != "" { if S.IsNonEmpty(tt.comment) {
doc = &ast.CommentGroup{ doc = &ast.CommentGroup{
List: []*ast.Comment{ List: []*ast.Comment{
{Text: tt.comment}, {Text: tt.comment},
@@ -289,7 +290,7 @@ func TestHasOmitEmpty(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
var tag *ast.BasicLit var tag *ast.BasicLit
if tt.tag != "" { if S.IsNonEmpty(tt.tag) {
tag = &ast.BasicLit{ tag = &ast.BasicLit{
Value: tt.tag, Value: tt.tag,
} }
@@ -326,7 +327,7 @@ type Other struct {
} }
` `
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Parse the file // Parse the file
@@ -380,7 +381,7 @@ type Config struct {
} }
` `
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Parse the file // Parse the file
@@ -440,7 +441,7 @@ type TypeTest struct {
} }
` `
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Parse the file // Parse the file
@@ -514,16 +515,16 @@ func TestLensRefTemplatesWithComparable(t *testing.T) {
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses") assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
// Name field - comparable, should use MakeLensStrict // Name field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(", assert.Contains(t, constructorStr, "lensName := __lens.MakeLensStrictWithName(",
"comparable field Name should use MakeLensStrict in RefLenses") "comparable field Name should use MakeLensStrictWithName in RefLenses")
// Age field - comparable, should use MakeLensStrict // Age field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(", assert.Contains(t, constructorStr, "lensAge := __lens.MakeLensStrictWithName(",
"comparable field Age should use MakeLensStrict in RefLenses") "comparable field Age should use MakeLensStrictWithName in RefLenses")
// Data field - not comparable, should use MakeLensRef // Data field - not comparable, should use MakeLensRef
assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(", assert.Contains(t, constructorStr, "lensData := __lens.MakeLensRefWithName(",
"non-comparable field Data should use MakeLensRef in RefLenses") "non-comparable field Data should use MakeLensRefWithName in RefLenses")
} }
@@ -542,12 +543,12 @@ type TestStruct struct {
` `
testFile := filepath.Join(tmpDir, "test.go") testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Generate lens code // Generate lens code
outputFile := "gen.go" outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false) err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err) require.NoError(t, err)
// Verify the generated file exists // Verify the generated file exists
@@ -564,23 +565,23 @@ type TestStruct struct {
// Check for expected content in RefLenses // Check for expected content in RefLenses
assert.Contains(t, contentStr, "MakeTestStructRefLenses") assert.Contains(t, contentStr, "MakeTestStructRefLenses")
// Name and Count are comparable, should use MakeLensStrict // Name and Count are comparable, should use MakeLensStrictWithName
assert.Contains(t, contentStr, "L.MakeLensStrict", assert.Contains(t, contentStr, "__lens.MakeLensStrictWithName",
"comparable fields should use MakeLensStrict in RefLenses") "comparable fields should use MakeLensStrictWithName in RefLenses")
// Data is not comparable (slice), should use MakeLensRef // Data is not comparable (slice), should use MakeLensRefWithName
assert.Contains(t, contentStr, "L.MakeLensRef", assert.Contains(t, contentStr, "__lens.MakeLensRefWithName",
"non-comparable fields should use MakeLensRef in RefLenses") "non-comparable fields should use MakeLensRefWithName in RefLenses")
// Verify the pattern appears for Name field (comparable) // Verify the pattern appears for Name field (comparable)
namePattern := "lensName := L.MakeLensStrict(" namePattern := "lensName := __lens.MakeLensStrictWithName("
assert.Contains(t, contentStr, namePattern, assert.Contains(t, contentStr, namePattern,
"Name field should use MakeLensStrict") "Name field should use MakeLensStrictWithName")
// Verify the pattern appears for Data field (not comparable) // Verify the pattern appears for Data field (not comparable)
dataPattern := "lensData := L.MakeLensRef(" dataPattern := "lensData := __lens.MakeLensRefWithName("
assert.Contains(t, contentStr, dataPattern, assert.Contains(t, contentStr, dataPattern,
"Data field should use MakeLensRef") "Data field should use MakeLensRefWithName")
} }
func TestGenerateLensHelpers(t *testing.T) { func TestGenerateLensHelpers(t *testing.T) {
@@ -597,12 +598,12 @@ type TestStruct struct {
` `
testFile := filepath.Join(tmpDir, "test.go") testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Generate lens code // Generate lens code
outputFile := "gen.go" outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false) err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err) require.NoError(t, err)
// Verify the generated file exists // Verify the generated file exists
@@ -621,9 +622,9 @@ type TestStruct struct {
assert.Contains(t, contentStr, "Code generated by go generate") assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLenses") assert.Contains(t, contentStr, "TestStructLenses")
assert.Contains(t, contentStr, "MakeTestStructLenses") assert.Contains(t, contentStr, "MakeTestStructLenses")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]") assert.Contains(t, contentStr, "__lens.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]") assert.Contains(t, contentStr, "__lens_option.LensO[TestStruct, *int]")
assert.Contains(t, contentStr, "IO.FromZero") assert.Contains(t, contentStr, "__iso_option.FromZero")
} }
func TestGenerateLensHelpersNoAnnotations(t *testing.T) { func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
@@ -639,12 +640,12 @@ type TestStruct struct {
` `
testFile := filepath.Join(tmpDir, "test.go") testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Generate lens code (should not create file) // Generate lens code (should not create file)
outputFile := "gen.go" outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false) err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err) require.NoError(t, err)
// Verify the generated file does not exist // Verify the generated file does not exist
@@ -669,10 +670,10 @@ func TestLensTemplates(t *testing.T) {
structStr := structBuf.String() structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct") assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]") assert.Contains(t, structStr, "Name __lens.Lens[TestStruct, string]")
assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]") assert.Contains(t, structStr, "NameO __lens_option.LensO[TestStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]") assert.Contains(t, structStr, "Value __lens.Lens[TestStruct, *int]")
assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]") assert.Contains(t, structStr, "ValueO __lens_option.LensO[TestStruct, *int]")
// Test constructor template // Test constructor template
var constructorBuf bytes.Buffer var constructorBuf bytes.Buffer
@@ -686,7 +687,7 @@ func TestLensTemplates(t *testing.T) {
assert.Contains(t, constructorStr, "NameO: lensNameO,") assert.Contains(t, constructorStr, "NameO: lensNameO,")
assert.Contains(t, constructorStr, "Value: lensValue,") assert.Contains(t, constructorStr, "Value: lensValue,")
assert.Contains(t, constructorStr, "ValueO: lensValueO,") assert.Contains(t, constructorStr, "ValueO: lensValueO,")
assert.Contains(t, constructorStr, "IO.FromZero") assert.Contains(t, constructorStr, "__iso_option.FromZero")
} }
func TestLensTemplatesWithOmitEmpty(t *testing.T) { func TestLensTemplatesWithOmitEmpty(t *testing.T) {
@@ -707,14 +708,14 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
structStr := structBuf.String() structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct") assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]") assert.Contains(t, structStr, "Name __lens.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]") assert.Contains(t, structStr, "NameO __lens_option.LensO[ConfigStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]") assert.Contains(t, structStr, "Value __lens.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens") assert.Contains(t, structStr, "ValueO __lens_option.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]") assert.Contains(t, structStr, "Count __lens.Lens[ConfigStruct, int]")
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens") assert.Contains(t, structStr, "CountO __lens_option.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]") assert.Contains(t, structStr, "Pointer __lens.Lens[ConfigStruct, *string]")
assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]") assert.Contains(t, structStr, "PointerO __lens_option.LensO[ConfigStruct, *string]")
// Test constructor template // Test constructor template
var constructorBuf bytes.Buffer var constructorBuf bytes.Buffer
@@ -723,9 +724,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
constructorStr := constructorBuf.String() constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses") assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "IO.FromZero[string]()") assert.Contains(t, constructorStr, "__iso_option.FromZero[string]()")
assert.Contains(t, constructorStr, "IO.FromZero[int]()") assert.Contains(t, constructorStr, "__iso_option.FromZero[int]()")
assert.Contains(t, constructorStr, "IO.FromZero[*string]()") assert.Contains(t, constructorStr, "__iso_option.FromZero[*string]()")
} }
func TestLensCommandFlags(t *testing.T) { func TestLensCommandFlags(t *testing.T) {
@@ -737,9 +738,9 @@ func TestLensCommandFlags(t *testing.T) {
assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses") assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")
// Check flags // Check flags
assert.Len(t, cmd.Flags, 3) assert.Len(t, cmd.Flags, 4)
var hasDir, hasFilename, hasVerbose bool var hasDir, hasFilename, hasVerbose, hasIncludeTestFiles bool
for _, flag := range cmd.Flags { for _, flag := range cmd.Flags {
switch flag.Names()[0] { switch flag.Names()[0] {
case "dir": case "dir":
@@ -748,12 +749,15 @@ func TestLensCommandFlags(t *testing.T) {
hasFilename = true hasFilename = true
case "verbose": case "verbose":
hasVerbose = true hasVerbose = true
case "include-test-files":
hasIncludeTestFiles = true
} }
} }
assert.True(t, hasDir, "should have dir flag") assert.True(t, hasDir, "should have dir flag")
assert.True(t, hasFilename, "should have filename flag") assert.True(t, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag") assert.True(t, hasVerbose, "should have verbose flag")
assert.True(t, hasIncludeTestFiles, "should have include-test-files flag")
} }
func TestParseFileWithEmbeddedStruct(t *testing.T) { func TestParseFileWithEmbeddedStruct(t *testing.T) {
@@ -776,7 +780,7 @@ type Extended struct {
} }
` `
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Parse the file // Parse the file
@@ -824,12 +828,12 @@ type Person struct {
` `
testFile := filepath.Join(tmpDir, "test.go") testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Generate lens code // Generate lens code
outputFile := "gen.go" outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false) err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err) require.NoError(t, err)
// Verify the generated file exists // Verify the generated file exists
@@ -849,14 +853,14 @@ type Person struct {
assert.Contains(t, contentStr, "MakePersonLenses") assert.Contains(t, contentStr, "MakePersonLenses")
// Check that embedded fields are included // Check that embedded fields are included
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field") assert.Contains(t, contentStr, "Street __lens.Lens[Person, string]", "Should have lens for embedded Street field")
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field") assert.Contains(t, contentStr, "City __lens.Lens[Person, string]", "Should have lens for embedded City field")
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field") assert.Contains(t, contentStr, "Name __lens.Lens[Person, string]", "Should have lens for Name field")
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field") assert.Contains(t, contentStr, "Age __lens.Lens[Person, int]", "Should have lens for Age field")
// Check that optional lenses are also generated for embedded fields // Check that optional lenses are also generated for embedded fields
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]") assert.Contains(t, contentStr, "StreetO __lens_option.LensO[Person, string]")
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]") assert.Contains(t, contentStr, "CityO __lens_option.LensO[Person, string]")
} }
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) { func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
@@ -880,7 +884,7 @@ type Document struct {
} }
` `
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Parse the file // Parse the file
@@ -922,7 +926,7 @@ type Container[T any] struct {
} }
` `
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Parse the file // Parse the file
@@ -960,7 +964,7 @@ type Pair[K comparable, V any] struct {
} }
` `
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Parse the file // Parse the file
@@ -998,12 +1002,12 @@ type Box[T any] struct {
` `
testFile := filepath.Join(tmpDir, "test.go") testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Generate lens code // Generate lens code
outputFile := "gen.go" outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false) err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err) require.NoError(t, err)
// Verify the generated file exists // Verify the generated file exists
@@ -1025,14 +1029,14 @@ type Box[T any] struct {
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor") assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
// Check that fields use the generic type parameter // Check that fields use the generic type parameter
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field") assert.Contains(t, contentStr, "Content __lens.Lens[Box[T], T]", "Should have lens for generic Content field")
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field") assert.Contains(t, contentStr, "Label __lens.Lens[Box[T], string]", "Should have lens for Label field")
// Check optional lenses - only for comparable types // Check optional lenses - only for comparable types
// T any is not comparable, so ContentO should NOT be generated // T any is not comparable, so ContentO should NOT be generated
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens") assert.NotContains(t, contentStr, "ContentO __lens_option.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
// string is comparable, so LabelO should be generated // string is comparable, so LabelO should be generated
assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens") assert.Contains(t, contentStr, "LabelO __lens_option.LensO[Box[T], string]", "string is comparable, should have optional lens")
} }
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) { func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
@@ -1049,12 +1053,12 @@ type ComparableBox[T comparable] struct {
` `
testFile := filepath.Join(tmpDir, "test.go") testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644) err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Generate lens code // Generate lens code
outputFile := "gen.go" outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false) err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err) require.NoError(t, err)
// Verify the generated file exists // Verify the generated file exists
@@ -1074,11 +1078,11 @@ type ComparableBox[T comparable] struct {
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type") assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses // Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict") assert.Contains(t, contentStr, "lensKey := __lens.MakeLensStrictWithName(", "Key field with comparable constraint should use MakeLensStrictWithName")
// Check that Value field (string, always comparable) also uses MakeLensStrict // Check that Value field (string, always comparable) also uses MakeLensStrict
assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict") assert.Contains(t, contentStr, "lensValue := __lens.MakeLensStrictWithName(", "Value field (string) should use MakeLensStrictWithName")
// Verify that MakeLensRef is NOT used (since both fields are comparable) // Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable") assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
} }

View File

@@ -19,6 +19,8 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
S "github.com/IBM/fp-go/v2/string"
) )
// Deprecated: // Deprecated:
@@ -176,7 +178,7 @@ func generateTraverseTuple1(
} }
fmt.Fprintf(f, "F%d ~func(A%d) %s", j+1, j+1, hkt(fmt.Sprintf("T%d", j+1))) fmt.Fprintf(f, "F%d ~func(A%d) %s", j+1, j+1, hkt(fmt.Sprintf("T%d", j+1)))
} }
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix) fmt.Fprintf(f, ", %s", infix)
} }
// types // types
@@ -209,7 +211,7 @@ func generateTraverseTuple1(
fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i) fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i)
// map // map
fmt.Fprintf(f, " Map[") fmt.Fprintf(f, " Map[")
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s, T1,", infix) fmt.Fprintf(f, "%s, T1,", infix)
} else { } else {
fmt.Fprintf(f, "T1,") fmt.Fprintf(f, "T1,")
@@ -231,7 +233,7 @@ func generateTraverseTuple1(
fmt.Fprintf(f, " ") fmt.Fprintf(f, " ")
} }
fmt.Fprintf(f, "%s", tuple) fmt.Fprintf(f, "%s", tuple)
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix) fmt.Fprintf(f, ", %s", infix)
} }
fmt.Fprintf(f, ", T%d],\n", j+1) fmt.Fprintf(f, ", T%d],\n", j+1)
@@ -256,11 +258,11 @@ func generateSequenceTuple1(
fmt.Fprintf(f, "\n// SequenceTuple%d converts a [Tuple%d] of [%s] into an [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i))) fmt.Fprintf(f, "\n// SequenceTuple%d converts a [Tuple%d] of [%s] into an [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
fmt.Fprintf(f, "func SequenceTuple%d[", i) fmt.Fprintf(f, "func SequenceTuple%d[", i)
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s", infix) fmt.Fprintf(f, "%s", infix)
} }
for j := 0; j < i; j++ { for j := 0; j < i; j++ {
if infix != "" || j > 0 { if S.IsNonEmpty(infix) || j > 0 {
fmt.Fprintf(f, ", ") fmt.Fprintf(f, ", ")
} }
fmt.Fprintf(f, "T%d", j+1) fmt.Fprintf(f, "T%d", j+1)
@@ -276,7 +278,7 @@ func generateSequenceTuple1(
fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i) fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i)
// map // map
fmt.Fprintf(f, " Map[") fmt.Fprintf(f, " Map[")
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s, T1,", infix) fmt.Fprintf(f, "%s, T1,", infix)
} else { } else {
fmt.Fprintf(f, "T1,") fmt.Fprintf(f, "T1,")
@@ -298,7 +300,7 @@ func generateSequenceTuple1(
fmt.Fprintf(f, " ") fmt.Fprintf(f, " ")
} }
fmt.Fprintf(f, "%s", tuple) fmt.Fprintf(f, "%s", tuple)
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix) fmt.Fprintf(f, ", %s", infix)
} }
fmt.Fprintf(f, ", T%d],\n", j+1) fmt.Fprintf(f, ", T%d],\n", j+1)
@@ -319,11 +321,11 @@ func generateSequenceT1(
fmt.Fprintf(f, "\n// SequenceT%d converts %d parameters of [%s] into a [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i))) fmt.Fprintf(f, "\n// SequenceT%d converts %d parameters of [%s] into a [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
fmt.Fprintf(f, "func SequenceT%d[", i) fmt.Fprintf(f, "func SequenceT%d[", i)
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s", infix) fmt.Fprintf(f, "%s", infix)
} }
for j := 0; j < i; j++ { for j := 0; j < i; j++ {
if infix != "" || j > 0 { if S.IsNonEmpty(infix) || j > 0 {
fmt.Fprintf(f, ", ") fmt.Fprintf(f, ", ")
} }
fmt.Fprintf(f, "T%d", j+1) fmt.Fprintf(f, "T%d", j+1)
@@ -339,7 +341,7 @@ func generateSequenceT1(
fmt.Fprintf(f, " return A.SequenceT%d(\n", i) fmt.Fprintf(f, " return A.SequenceT%d(\n", i)
// map // map
fmt.Fprintf(f, " Map[") fmt.Fprintf(f, " Map[")
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s, T1,", infix) fmt.Fprintf(f, "%s, T1,", infix)
} else { } else {
fmt.Fprintf(f, "T1,") fmt.Fprintf(f, "T1,")
@@ -361,7 +363,7 @@ func generateSequenceT1(
fmt.Fprintf(f, " ") fmt.Fprintf(f, " ")
} }
fmt.Fprintf(f, "%s", tuple) fmt.Fprintf(f, "%s", tuple)
if infix != "" { if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix) fmt.Fprintf(f, ", %s", infix)
} }
fmt.Fprintf(f, ", T%d],\n", j+1) fmt.Fprintf(f, ", T%d],\n", j+1)

177
v2/consumer/consumer.go Normal file
View File

@@ -0,0 +1,177 @@
// 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 consumer
// Local transforms a Consumer by preprocessing its input through a function.
// This is the contravariant map operation for Consumers, analogous to reader.Local
// but operating on the input side rather than the output side.
//
// Given a Consumer[R1] that consumes values of type R1, and a function f that
// converts R2 to R1, Local creates a new Consumer[R2] that:
// 1. Takes a value of type R2
// 2. Applies f to convert it to R1
// 3. Passes the result to the original Consumer[R1]
//
// This is particularly useful for adapting consumers to work with different input types,
// similar to how reader.Local adapts readers to work with different environment types.
//
// Comparison with reader.Local:
// - reader.Local: Transforms the environment BEFORE passing it to a Reader (preprocessing input)
// - consumer.Local: Transforms the value BEFORE passing it to a Consumer (preprocessing input)
// - Both are contravariant operations on the input type
// - Reader produces output, Consumer performs side effects
//
// Type Parameters:
// - R2: The input type of the new Consumer (what you have)
// - R1: The input type of the original Consumer (what it expects)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic type adaptation:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Adapt it to consume strings by parsing them first
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Local(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Extracting fields from structs:
//
// type User struct {
// Name string
// Age int
// }
//
// // Consumer that logs names
// logName := func(name string) {
// fmt.Printf("Name: %s\n", name)
// }
//
// // Adapt it to consume User structs
// extractName := func(u User) string {
// return u.Name
// }
//
// logUser := consumer.Local(extractName)(logName)
// logUser(User{Name: "Alice", Age: 30}) // Logs: "Name: Alice"
//
// Example - Simplifying complex types:
//
// type DetailedConfig struct {
// Host string
// Port int
// Timeout time.Duration
// MaxRetry int
// }
//
// type SimpleConfig struct {
// Host string
// Port int
// }
//
// // Consumer that logs simple configs
// logSimple := func(c SimpleConfig) {
// fmt.Printf("Server: %s:%d\n", c.Host, c.Port)
// }
//
// // Adapt it to consume detailed configs
// simplify := func(d DetailedConfig) SimpleConfig {
// return SimpleConfig{Host: d.Host, Port: d.Port}
// }
//
// logDetailed := consumer.Local(simplify)(logSimple)
// logDetailed(DetailedConfig{
// Host: "localhost",
// Port: 8080,
// Timeout: time.Second,
// MaxRetry: 3,
// }) // Logs: "Server: localhost:8080"
//
// Example - Composing multiple transformations:
//
// type Response struct {
// StatusCode int
// Body string
// }
//
// // Consumer that logs status codes
// logStatus := func(code int) {
// fmt.Printf("Status: %d\n", code)
// }
//
// // Extract status code from response
// getStatus := func(r Response) int {
// return r.StatusCode
// }
//
// // Adapt to consume responses
// logResponse := consumer.Local(getStatus)(logStatus)
// logResponse(Response{StatusCode: 200, Body: "OK"}) // Logs: "Status: 200"
//
// Example - Using with multiple consumers:
//
// type Event struct {
// Type string
// Timestamp time.Time
// Data map[string]any
// }
//
// // Consumers for different aspects
// logType := func(t string) { fmt.Printf("Type: %s\n", t) }
// logTime := func(t time.Time) { fmt.Printf("Time: %v\n", t) }
//
// // Adapt them to consume events
// logEventType := consumer.Local(func(e Event) string { return e.Type })(logType)
// logEventTime := consumer.Local(func(e Event) time.Time { return e.Timestamp })(logTime)
//
// event := Event{Type: "UserLogin", Timestamp: time.Now(), Data: nil}
// logEventType(event) // Logs: "Type: UserLogin"
// logEventTime(event) // Logs: "Time: ..."
//
// Use Cases:
// - Type adaptation: Convert between different input types
// - Field extraction: Extract specific fields from complex structures
// - Data transformation: Preprocess data before consumption
// - Interface adaptation: Adapt consumers to work with different interfaces
// - Logging pipelines: Transform data before logging
// - Event handling: Extract relevant data from events before processing
//
// Relationship to Reader:
// Consumer is the dual of Reader in category theory:
// - Reader[R, A] = R -> A (produces output from environment)
// - Consumer[A] = A -> () (consumes input, produces side effects)
// - reader.Local transforms the environment before reading
// - consumer.Local transforms the input before consuming
// - Both are contravariant functors on their input type
func Local[R2, R1 any](f func(R2) R1) Operator[R1, R2] {
return func(c Consumer[R1]) Consumer[R2] {
return func(r2 R2) {
c(f(r2))
}
}
}

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 consumer
import (
"strconv"
"testing"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestLocal(t *testing.T) {
t.Run("basic type transformation", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
// Transform string to int before consuming
stringToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Local(stringToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("field extraction from struct", func(t *testing.T) {
type User struct {
Name string
Age int
}
var capturedName string
consumeName := func(name string) {
capturedName = name
}
extractName := func(u User) string {
return u.Name
}
consumeUser := Local(extractName)(consumeName)
consumeUser(User{Name: "Alice", Age: 30})
assert.Equal(t, "Alice", capturedName)
})
t.Run("simplifying complex types", func(t *testing.T) {
type DetailedConfig struct {
Host string
Port int
Timeout time.Duration
MaxRetry int
}
type SimpleConfig struct {
Host string
Port int
}
var captured SimpleConfig
consumeSimple := func(c SimpleConfig) {
captured = c
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Host: d.Host, Port: d.Port}
}
consumeDetailed := Local(simplify)(consumeSimple)
consumeDetailed(DetailedConfig{
Host: "localhost",
Port: 8080,
Timeout: time.Second,
MaxRetry: 3,
})
assert.Equal(t, SimpleConfig{Host: "localhost", Port: 8080}, captured)
})
t.Run("multiple transformations", func(t *testing.T) {
type Response struct {
StatusCode int
Body string
}
var capturedStatus int
consumeStatus := func(code int) {
capturedStatus = code
}
getStatus := func(r Response) int {
return r.StatusCode
}
consumeResponse := Local(getStatus)(consumeStatus)
consumeResponse(Response{StatusCode: 200, Body: "OK"})
assert.Equal(t, 200, capturedStatus)
})
t.Run("chaining Local transformations", func(t *testing.T) {
type Level3 struct{ Value int }
type Level2 struct{ L3 Level3 }
type Level1 struct{ L2 Level2 }
var captured int
consumeInt := func(x int) {
captured = x
}
// Chain multiple Local transformations
extract3 := func(l3 Level3) int { return l3.Value }
extract2 := func(l2 Level2) Level3 { return l2.L3 }
extract1 := func(l1 Level1) Level2 { return l1.L2 }
// Compose the transformations
consumeLevel3 := Local(extract3)(consumeInt)
consumeLevel2 := Local(extract2)(consumeLevel3)
consumeLevel1 := Local(extract1)(consumeLevel2)
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
assert.Equal(t, 42, captured)
})
t.Run("identity transformation", func(t *testing.T) {
var captured string
consumeString := func(s string) {
captured = s
}
identity := function.Identity[string]
consumeIdentity := Local(identity)(consumeString)
consumeIdentity("test")
assert.Equal(t, "test", captured)
})
t.Run("transformation with calculation", func(t *testing.T) {
type Rectangle struct {
Width int
Height int
}
var capturedArea int
consumeArea := func(area int) {
capturedArea = area
}
calculateArea := func(r Rectangle) int {
return r.Width * r.Height
}
consumeRectangle := Local(calculateArea)(consumeArea)
consumeRectangle(Rectangle{Width: 5, Height: 10})
assert.Equal(t, 50, capturedArea)
})
t.Run("multiple consumers with same transformation", func(t *testing.T) {
type Event struct {
Type string
Timestamp time.Time
}
var capturedType string
var capturedTime time.Time
consumeType := func(t string) {
capturedType = t
}
consumeTime := func(t time.Time) {
capturedTime = t
}
extractType := func(e Event) string { return e.Type }
extractTime := func(e Event) time.Time { return e.Timestamp }
consumeEventType := Local(extractType)(consumeType)
consumeEventTime := Local(extractTime)(consumeTime)
now := time.Now()
event := Event{Type: "UserLogin", Timestamp: now}
consumeEventType(event)
consumeEventTime(event)
assert.Equal(t, "UserLogin", capturedType)
assert.Equal(t, now, capturedTime)
})
t.Run("transformation with slice", func(t *testing.T) {
var captured int
consumeLength := func(n int) {
captured = n
}
getLength := func(s []string) int {
return len(s)
}
consumeSlice := Local(getLength)(consumeLength)
consumeSlice([]string{"a", "b", "c"})
assert.Equal(t, 3, captured)
})
t.Run("transformation with map", func(t *testing.T) {
var captured int
consumeCount := func(n int) {
captured = n
}
getCount := func(m map[string]int) int {
return len(m)
}
consumeMap := Local(getCount)(consumeCount)
consumeMap(map[string]int{"a": 1, "b": 2, "c": 3})
assert.Equal(t, 3, captured)
})
t.Run("transformation with pointer", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
dereference := func(p *int) int {
if p == nil {
return 0
}
return *p
}
consumePointer := Local(dereference)(consumeInt)
value := 42
consumePointer(&value)
assert.Equal(t, 42, captured)
consumePointer(nil)
assert.Equal(t, 0, captured)
})
t.Run("transformation with custom type", func(t *testing.T) {
type MyType struct {
Value string
}
var captured string
consumeString := func(s string) {
captured = s
}
extractValue := func(m MyType) string {
return m.Value
}
consumeMyType := Local(extractValue)(consumeString)
consumeMyType(MyType{Value: "test"})
assert.Equal(t, "test", captured)
})
t.Run("accumulation through multiple calls", func(t *testing.T) {
var sum int
accumulate := func(x int) {
sum += x
}
double := func(x int) int {
return x * 2
}
accumulateDoubled := Local(double)(accumulate)
accumulateDoubled(1)
accumulateDoubled(2)
accumulateDoubled(3)
assert.Equal(t, 12, sum) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
})
t.Run("transformation with error handling", func(t *testing.T) {
type Result struct {
Value int
Error error
}
var captured int
consumeInt := func(x int) {
captured = x
}
extractValue := func(r Result) int {
if r.Error != nil {
return -1
}
return r.Value
}
consumeResult := Local(extractValue)(consumeInt)
consumeResult(Result{Value: 42, Error: nil})
assert.Equal(t, 42, captured)
consumeResult(Result{Value: 100, Error: assert.AnError})
assert.Equal(t, -1, captured)
})
t.Run("transformation preserves consumer behavior", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
transformedConsumer := Local(transform)(consumer)
transformedConsumer("1")
transformedConsumer("2")
transformedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("comparison with reader.Local behavior", func(t *testing.T) {
// This test demonstrates the dual nature of Consumer and Reader
// Consumer: transforms input before consumption (contravariant)
// Reader: transforms environment before reading (also contravariant on input)
type DetailedEnv struct {
Value int
Extra string
}
type SimpleEnv struct {
Value int
}
var captured int
consumeSimple := func(e SimpleEnv) {
captured = e.Value
}
simplify := func(d DetailedEnv) SimpleEnv {
return SimpleEnv{Value: d.Value}
}
consumeDetailed := Local(simplify)(consumeSimple)
consumeDetailed(DetailedEnv{Value: 42, Extra: "ignored"})
assert.Equal(t, 42, captured)
})
}

56
v2/consumer/types.go Normal file
View File

@@ -0,0 +1,56 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package consumer provides types and utilities for functions that consume values without returning results.
//
// A Consumer represents a side-effecting operation that accepts a value but produces no output.
// This is useful for operations like logging, printing, updating state, or any action where
// the return value is not needed.
package consumer
type (
// Consumer represents a function that accepts a value of type A and performs a side effect.
// It does not return any value, making it useful for operations where only the side effect matters,
// such as logging, printing, or updating external state.
//
// This is a fundamental concept in functional programming for handling side effects in a
// controlled manner. Consumers can be composed, chained, or used in higher-order functions
// to build complex side-effecting behaviors.
//
// Type Parameters:
// - A: The type of value consumed by the function
//
// Example:
//
// // A simple consumer that prints values
// var printInt Consumer[int] = func(x int) {
// fmt.Println(x)
// }
// printInt(42) // Prints: 42
//
// // A consumer that logs messages
// var logger Consumer[string] = func(msg string) {
// log.Println(msg)
// }
// logger("Hello, World!") // Logs: Hello, World!
//
// // Consumers can be used in functional pipelines
// var saveToDatabase Consumer[User] = func(user User) {
// db.Save(user)
// }
Consumer[A any] = func(A)
Operator[A, B any] = func(Consumer[A]) Consumer[B]
)

View File

@@ -24,8 +24,8 @@ import (
// withContext wraps an existing IOEither and performs a context check for cancellation before delegating // withContext wraps an existing IOEither and performs a context check for cancellation before delegating
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] { func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
return func() Result[A] { return func() Result[A] {
if err := context.Cause(ctx); err != nil { if ctx.Err() != nil {
return result.Left[A](err) return result.Left[A](context.Cause(ctx))
} }
return ma() return ma()
} }

View File

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

View File

@@ -0,0 +1,13 @@
package readerio
import "github.com/IBM/fp-go/v2/io"
//go:inline
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return ChainIOK(io.FromConsumerK(c))
}
//go:inline
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
return ChainFirstIOK(io.FromConsumerK(c))
}

View File

@@ -16,5 +16,5 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
func TraverseReader[R, A, B any]( func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B], f reader.Kleisli[R, A, B],
) func(ReaderIO[A]) Kleisli[R, B] { ) func(ReaderIO[A]) Kleisli[R, B] {
return RIO.TraverseReader[context.Context, R](f) return RIO.TraverseReader[context.Context](f)
} }

View File

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

View File

@@ -17,6 +17,7 @@ package readerio
import ( import (
"context" "context"
"time"
"github.com/IBM/fp-go/v2/function" "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
@@ -558,3 +559,211 @@ func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A
func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] { func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
return RIO.Read[A](r) return RIO.Read[A](r)
} }
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerio.FromReader(func(ctx context.Context) string {
// if user := ctx.Value(userKey); user != nil {
// return user.(string)
// }
// return "unknown"
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
return func() A {
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}
// WithTimeout adds a timeout to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderIO is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{} after 5s timeout
//
// Successful Example:
//
// quickFetch := readerio.Of(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{Value: "quick"}
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](deadline),
// )
// data := result(context.Background())() // Returns Data{} if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](laterDeadline),
// )
// data := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
})
}
// Delay creates an operation that passes in the value after some delay
//
//go:inline
func Delay[A any](delay time.Duration) Operator[A, A] {
return RIO.Delay[context.Context, A](delay)
}
// After creates an operation that passes after the given [time.Time]
//
//go:inline
func After[R, E, A any](timestamp time.Time) Operator[A, A] {
return RIO.After[context.Context, A](timestamp)
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerio
import (
"github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
return readerio.TailRec(f)
}

View File

@@ -0,0 +1,41 @@
// 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 readerio
import (
"github.com/IBM/fp-go/v2/retry"
RG "github.com/IBM/fp-go/v2/retry/generic"
)
//go:inline
func Retrying[A any](
policy retry.RetryPolicy,
action Kleisli[retry.RetryStatus, A],
check func(A) bool,
) ReaderIO[A] {
// get an implementation for the types
return RG.Retrying(
Chain[A, A],
Chain[retry.RetryStatus, A],
Of[A],
Of[retry.RetryStatus],
Delay[retry.RetryStatus],
policy,
action,
check,
)
}

View File

@@ -18,6 +18,8 @@ package readerio
import ( import (
"context" "context"
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy" "github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
@@ -66,4 +68,8 @@ type (
// //
// Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B // Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B
Operator[A, B any] = Kleisli[ReaderIO[A], B] Operator[A, B any] = Kleisli[ReaderIO[A], B]
Consumer[A any] = consumer.Consumer[A]
Either[E, A any] = either.Either[E, A]
) )

View File

@@ -8,9 +8,10 @@ This document explains how the `Sequence*` functions in the `context/readeriores
2. [The Problem: Nested Function Application](#the-problem-nested-function-application) 2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
3. [The Solution: Sequence Functions](#the-solution-sequence-functions) 3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style) 4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
5. [Practical Benefits](#practical-benefits) 5. [TraverseReader: Introducing Dependencies](#traversereader-introducing-dependencies)
6. [Examples](#examples) 6. [Practical Benefits](#practical-benefits)
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence) 7. [Examples](#examples)
8. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
## What is Point-Free Style? ## What is Point-Free Style?
@@ -25,10 +26,7 @@ func double(x int) int {
**Point-free style (without points):** **Point-free style (without points):**
```go ```go
var double = F.Flow2( var double = N.Mul(2)
N.Mul(2),
identity,
)
``` ```
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data. The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
@@ -99,7 +97,7 @@ The `Sequence*` functions solve this by "flipping" or "sequencing" the nested st
```go ```go
func SequenceReader[R, A any]( func SequenceReader[R, A any](
ma ReaderIOResult[Reader[R, A]] ma ReaderIOResult[Reader[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]] ) Kleisli[R, A]
``` ```
**Type transformation:** **Type transformation:**
@@ -115,7 +113,7 @@ Now `R` (the Reader's environment) comes **first**, before `context.Context`!
```go ```go
func SequenceReaderIO[R, A any]( func SequenceReaderIO[R, A any](
ma ReaderIOResult[ReaderIO[R, A]] ma ReaderIOResult[ReaderIO[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]] ) Kleisli[R, A]
``` ```
**Type transformation:** **Type transformation:**
@@ -129,7 +127,7 @@ To: func(R) func(context.Context) func() Either[error, A]
```go ```go
func SequenceReaderResult[R, A any]( func SequenceReaderResult[R, A any](
ma ReaderIOResult[ReaderResult[R, A]] ma ReaderIOResult[ReaderResult[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]] ) Kleisli[R, A]
``` ```
**Type transformation:** **Type transformation:**
@@ -222,6 +220,186 @@ authInfo := authService(ctx)()
userInfo := userService(ctx)() userInfo := userService(ctx)()
``` ```
## TraverseReader: Introducing Dependencies
While `SequenceReader` flips the parameter order of an existing nested structure, `TraverseReader` allows you to **introduce** a new Reader dependency into an existing computation.
### Function Signature
```go
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderIOResult[A]) Kleisli[R, B]
```
**Type transformation:**
```
Input: ReaderIOResult[A] = func(context.Context) func() Either[error, A]
With: reader.Kleisli[R, A, B] = func(A) func(R) B
Output: Kleisli[R, B] = func(R) func(context.Context) func() Either[error, B]
```
### What It Does
`TraverseReader` takes:
1. A Reader-based transformation `f: func(A) func(R) B` that depends on environment `R`
2. Returns a function that transforms `ReaderIOResult[A]` into `Kleisli[R, B]`
This allows you to:
- Add environment dependencies to computations that don't have them yet
- Transform values within a ReaderIOResult using environment-dependent logic
- Build composable pipelines where transformations depend on configuration
### Key Difference from SequenceReader
- **SequenceReader**: Works with computations that **already contain** a Reader (`ReaderIOResult[Reader[R, A]]`)
- Flips the order so `R` comes first
- No transformation of the value itself
- **TraverseReader**: Works with computations that **don't have** a Reader yet (`ReaderIOResult[A]`)
- Introduces a new Reader dependency via a transformation function
- Transforms `A` to `B` using environment `R`
### Example: Adding Configuration to a Computation
```go
type Config struct {
Multiplier int
Prefix string
}
// Original computation that just produces an int
getValue := func(ctx context.Context) func() Either[error, int] {
return func() Either[error, int] {
return Right[error](10)
}
}
// A Reader-based transformation that depends on Config
formatWithConfig := func(n int) func(Config) string {
return func(cfg Config) string {
result := n * cfg.Multiplier
return fmt.Sprintf("%s: %d", cfg.Prefix, result)
}
}
// Use TraverseReader to introduce Config dependency
traversed := TraverseReader[Config, int, string](formatWithConfig)
withConfig := traversed(getValue)
// Now we can provide Config to get the final result
cfg := Config{Multiplier: 5, Prefix: "Result"}
ctx := context.Background()
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
```
### Point-Free Composition with TraverseReader
```go
// Build a pipeline that introduces dependencies at each stage
var pipeline = F.Flow4(
loadValue, // ReaderIOResult[int]
TraverseReader(multiplyByConfig), // Kleisli[Config, int]
applyConfig(cfg), // ReaderIOResult[int]
Chain(TraverseReader(formatWithStyle)), // Introduce another dependency
)
```
### When to Use TraverseReader vs SequenceReader
**Use SequenceReader when:**
- Your computation already returns a Reader: `ReaderIOResult[Reader[R, A]]`
- You just want to flip the parameter order
- No transformation of the value is needed
```go
// Already have Reader[Config, int]
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
sequenced := SequenceReader[Config, int](computation)
result := sequenced(cfg)(ctx)()
```
**Use TraverseReader when:**
- Your computation doesn't have a Reader yet: `ReaderIOResult[A]`
- You want to transform the value using environment-dependent logic
- You're introducing a new dependency into the pipeline
```go
// Have ReaderIOResult[int], want to add Config dependency
computation := getValue() // ReaderIOResult[int]
traversed := TraverseReader[Config, int, string](formatWithConfig)
withDep := traversed(computation)
result := withDep(cfg)(ctx)()
```
### Practical Example: Multi-Stage Processing
```go
type DatabaseConfig struct {
ConnectionString string
Timeout time.Duration
}
type FormattingConfig struct {
DateFormat string
Timezone string
}
// Stage 1: Load raw data (no dependencies yet)
loadData := func(ctx context.Context) func() Either[error, RawData] {
// ... implementation
}
// Stage 2: Process with database config
processWithDB := func(raw RawData) func(DatabaseConfig) ProcessedData {
return func(cfg DatabaseConfig) ProcessedData {
// Use cfg.ConnectionString, cfg.Timeout
return ProcessedData{/* ... */}
}
}
// Stage 3: Format with formatting config
formatData := func(processed ProcessedData) func(FormattingConfig) string {
return func(cfg FormattingConfig) string {
// Use cfg.DateFormat, cfg.Timezone
return "formatted result"
}
}
// Build pipeline introducing dependencies at each stage
var pipeline = F.Flow3(
loadData,
TraverseReader[DatabaseConfig, RawData, ProcessedData](processWithDB),
// Now we have Kleisli[DatabaseConfig, ProcessedData]
applyConfig(dbConfig),
// Now we have ReaderIOResult[ProcessedData]
TraverseReader[FormattingConfig, ProcessedData, string](formatData),
// Now we have Kleisli[FormattingConfig, string]
)
// Execute with both configs
result := pipeline(fmtConfig)(ctx)()
```
### Combining TraverseReader and SequenceReader
You can combine both functions in complex pipelines:
```go
// Start with nested Reader
computation := getComputation() // ReaderIOResult[Reader[Config, User]]
var pipeline = F.Flow4(
computation,
SequenceReader[Config, User], // Flip to get Kleisli[Config, User]
applyConfig(cfg), // Apply config, get ReaderIOResult[User]
TraverseReader(enrichWithDatabase), // Add database dependency
// Now have Kleisli[Database, EnrichedUser]
)
result := pipeline(db)(ctx)()
```
## Practical Benefits ## Practical Benefits
### 1. **Improved Testability** ### 1. **Improved Testability**

View File

@@ -18,14 +18,13 @@ package readerioresult
import ( import (
"context" "context"
"github.com/IBM/fp-go/v2/context/readerio"
F "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/apply"
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither" "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult" "github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult" RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result" "github.com/IBM/fp-go/v2/result"
) )
@@ -96,7 +95,7 @@ func Bind[S1, S2, T any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
f Kleisli[S1, T], f Kleisli[S1, T],
) Operator[S1, S2] { ) Operator[S1, S2] {
return RIOR.Bind(setter, f) return RIOR.Bind(setter, WithContextK(f))
} }
// 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]
@@ -128,6 +127,13 @@ func BindTo[S1, T any](
return RIOR.BindTo[context.Context](setter) return RIOR.BindTo[context.Context](setter)
} }
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering // ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad). // the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other. // This allows independent computations to be combined without one depending on the result of the other.
@@ -214,7 +220,7 @@ func ApS[S1, S2, T any](
// //
//go:inline //go:inline
func ApSL[S, T any]( func ApSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa ReaderIOResult[T], fa ReaderIOResult[T],
) Operator[S, S] { ) Operator[S, S] {
return ApS(lens.Set, fa) return ApS(lens.Set, fa)
@@ -253,10 +259,10 @@ func ApSL[S, T any](
// //
//go:inline //go:inline
func BindL[S, T any]( func BindL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f Kleisli[T, T], f Kleisli[T, T],
) Operator[S, S] { ) Operator[S, S] {
return RIOR.BindL(lens, f) return RIOR.BindL(lens, WithContextK(f))
} }
// LetL is a variant of Let that uses a lens to focus on a specific part of the context. // LetL is a variant of Let that uses a lens to focus on a specific part of the context.
@@ -289,8 +295,8 @@ func BindL[S, T any](
// //
//go:inline //go:inline
func LetL[S, T any]( func LetL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f func(T) T, f Endomorphism[T],
) Operator[S, S] { ) Operator[S, S] {
return RIOR.LetL[context.Context](lens, f) return RIOR.LetL[context.Context](lens, f)
} }
@@ -322,7 +328,7 @@ func LetL[S, T any](
// //
//go:inline //go:inline
func LetToL[S, T any]( func LetToL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
b T, b T,
) Operator[S, S] { ) Operator[S, S] {
return RIOR.LetToL[context.Context](lens, b) return RIOR.LetToL[context.Context](lens, b)
@@ -398,7 +404,7 @@ func BindReaderK[S1, S2, T any](
//go:inline //go:inline
func BindReaderIOK[S1, S2, T any]( func BindReaderIOK[S1, S2, T any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
f readerio.Kleisli[context.Context, S1, T], f readerio.Kleisli[S1, T],
) Operator[S1, S2] { ) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromReaderIO[T])) return Bind(setter, F.Flow2(f, FromReaderIO[T]))
} }
@@ -443,7 +449,7 @@ func BindResultK[S1, S2, T any](
// //
//go:inline //go:inline
func BindIOEitherKL[S, T any]( func BindIOEitherKL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f ioresult.Kleisli[T, T], f ioresult.Kleisli[T, T],
) Operator[S, S] { ) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[T])) return BindL(lens, F.Flow2(f, FromIOEither[T]))
@@ -458,7 +464,7 @@ func BindIOEitherKL[S, T any](
// //
//go:inline //go:inline
func BindIOResultKL[S, T any]( func BindIOResultKL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f ioresult.Kleisli[T, T], f ioresult.Kleisli[T, T],
) Operator[S, S] { ) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[T])) return BindL(lens, F.Flow2(f, FromIOEither[T]))
@@ -474,7 +480,7 @@ func BindIOResultKL[S, T any](
// //
//go:inline //go:inline
func BindIOKL[S, T any]( func BindIOKL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f io.Kleisli[T, T], f io.Kleisli[T, T],
) Operator[S, S] { ) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromIO[T])) return BindL(lens, F.Flow2(f, FromIO[T]))
@@ -490,7 +496,7 @@ func BindIOKL[S, T any](
// //
//go:inline //go:inline
func BindReaderKL[S, T any]( func BindReaderKL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f reader.Kleisli[context.Context, T, T], f reader.Kleisli[context.Context, T, T],
) Operator[S, S] { ) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromReader[T])) return BindL(lens, F.Flow2(f, FromReader[T]))
@@ -506,8 +512,8 @@ func BindReaderKL[S, T any](
// //
//go:inline //go:inline
func BindReaderIOKL[S, T any]( func BindReaderIOKL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f readerio.Kleisli[context.Context, T, T], f readerio.Kleisli[T, T],
) Operator[S, S] { ) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromReaderIO[T])) return BindL(lens, F.Flow2(f, FromReaderIO[T]))
} }
@@ -627,7 +633,7 @@ func ApResultS[S1, S2, T any](
// //
//go:inline //go:inline
func ApIOEitherSL[S, T any]( func ApIOEitherSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa IOResult[T], fa IOResult[T],
) Operator[S, S] { ) Operator[S, S] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa)) return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
@@ -642,7 +648,7 @@ func ApIOEitherSL[S, T any](
// //
//go:inline //go:inline
func ApIOResultSL[S, T any]( func ApIOResultSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa IOResult[T], fa IOResult[T],
) Operator[S, S] { ) Operator[S, S] {
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa)) return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
@@ -657,7 +663,7 @@ func ApIOResultSL[S, T any](
// //
//go:inline //go:inline
func ApIOSL[S, T any]( func ApIOSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa IO[T], fa IO[T],
) Operator[S, S] { ) Operator[S, S] {
return ApSL(lens, FromIO(fa)) return ApSL(lens, FromIO(fa))
@@ -672,7 +678,7 @@ func ApIOSL[S, T any](
// //
//go:inline //go:inline
func ApReaderSL[S, T any]( func ApReaderSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa Reader[context.Context, T], fa Reader[context.Context, T],
) Operator[S, S] { ) Operator[S, S] {
return ApSL(lens, FromReader(fa)) return ApSL(lens, FromReader(fa))
@@ -687,7 +693,7 @@ func ApReaderSL[S, T any](
// //
//go:inline //go:inline
func ApReaderIOSL[S, T any]( func ApReaderIOSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa ReaderIO[T], fa ReaderIO[T],
) Operator[S, S] { ) Operator[S, S] {
return ApSL(lens, FromReaderIO(fa)) return ApSL(lens, FromReaderIO(fa))
@@ -702,7 +708,7 @@ func ApReaderIOSL[S, T any](
// //
//go:inline //go:inline
func ApEitherSL[S, T any]( func ApEitherSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa Result[T], fa Result[T],
) Operator[S, S] { ) Operator[S, S] {
return ApSL(lens, FromEither(fa)) return ApSL(lens, FromEither(fa))
@@ -717,7 +723,7 @@ func ApEitherSL[S, T any](
// //
//go:inline //go:inline
func ApResultSL[S, T any]( func ApResultSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa Result[T], fa Result[T],
) Operator[S, S] { ) Operator[S, S] {
return ApSL(lens, FromResult(fa)) return ApSL(lens, FromResult(fa))

View File

@@ -203,9 +203,7 @@ func TestApS_EmptyState(t *testing.T) {
result := res(t.Context())() result := res(t.Context())()
assert.True(t, E.IsRight(result)) assert.True(t, E.IsRight(result))
emptyOpt := E.ToOption(result) emptyOpt := E.ToOption(result)
assert.True(t, O.IsSome(emptyOpt)) assert.Equal(t, O.Of(Empty{}), emptyOpt)
empty, _ := O.Unwrap(emptyOpt)
assert.Equal(t, Empty{}, empty)
} }
func TestApS_ChainedWithBind(t *testing.T) { func TestApS_ChainedWithBind(t *testing.T) {

View File

@@ -16,11 +16,14 @@
package readerioresult package readerioresult
import ( import (
F "github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult" RIOR "github.com/IBM/fp-go/v2/readerioresult"
) )
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of // Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
// whether the body action returns and error or not. // whether the body action returns and error or not.
//
//go:inline
func Bracket[ func Bracket[
A, B, ANY any]( A, B, ANY any](
@@ -28,5 +31,5 @@ func Bracket[
use Kleisli[A, B], use Kleisli[A, B],
release func(A, Either[B]) ReaderIOResult[ANY], release func(A, Either[B]) ReaderIOResult[ANY],
) ReaderIOResult[B] { ) ReaderIOResult[B] {
return RIOR.Bracket(acquire, use, release) return RIOR.Bracket(acquire, F.Flow2(use, WithContext), release)
} }

View File

@@ -19,6 +19,7 @@ import (
"context" "context"
CIOE "github.com/IBM/fp-go/v2/context/ioresult" CIOE "github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither" "github.com/IBM/fp-go/v2/ioeither"
) )
@@ -34,9 +35,17 @@ import (
// Returns a ReaderIOResult that checks for cancellation before executing. // Returns a ReaderIOResult that checks for cancellation before executing.
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] { func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOEither[A] { return func(ctx context.Context) IOEither[A] {
if err := context.Cause(ctx); err != nil { if ctx.Err() != nil {
return ioeither.Left[A](err) return ioeither.Left[A](context.Cause(ctx))
} }
return CIOE.WithContext(ctx, ma(ctx)) return CIOE.WithContext(ctx, ma(ctx))
} }
} }
//go:inline
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
return F.Flow2(
f,
WithContext,
)
}

View File

@@ -0,0 +1,13 @@
package readerioresult
import "github.com/IBM/fp-go/v2/io"
//go:inline
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return ChainIOK(io.FromConsumerK(c))
}
//go:inline
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
return ChainFirstIOK(io.FromConsumerK(c))
}

View File

@@ -83,7 +83,7 @@ import (
// ) // )
// //
//go:inline //go:inline
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] { func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReader(ma) return RIOR.SequenceReader(ma)
} }
@@ -145,7 +145,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[co
// ) // )
// //
//go:inline //go:inline
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] { func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReaderIO(ma) return RIOR.SequenceReaderIO(ma)
} }
@@ -212,7 +212,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kl
// ) // )
// //
//go:inline //go:inline
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] { func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReaderEither(ma) return RIOR.SequenceReaderEither(ma)
} }

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ import (
"context" "context"
"time" "time"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/context/readerresult" "github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either" "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors" "github.com/IBM/fp-go/v2/errors"
@@ -26,10 +27,11 @@ import (
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither" "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult" "github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult" RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption" "github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
) )
const ( const (
@@ -150,7 +152,7 @@ func MapTo[A, B any](b B) Operator[A, B] {
// //
//go:inline //go:inline
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] { func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChain(ma, f) return RIOR.MonadChain(ma, WithContextK(f))
} }
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first. // Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
@@ -163,7 +165,7 @@ func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[
// //
//go:inline //go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] { func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return RIOR.Chain(f) return RIOR.Chain(WithContextK(f))
} }
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first. // MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
@@ -177,12 +179,12 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
// //
//go:inline //go:inline
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] { func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirst(ma, f) return RIOR.MonadChainFirst(ma, WithContextK(f))
} }
//go:inline //go:inline
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] { func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTap(ma, f) return RIOR.MonadTap(ma, WithContextK(f))
} }
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first. // ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
@@ -195,12 +197,12 @@ func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A]
// //
//go:inline //go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] { func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirst(f) return RIOR.ChainFirst(WithContextK(f))
} }
//go:inline //go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] { func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.Tap(f) return RIOR.Tap(WithContextK(f))
} }
// Of creates a [ReaderIOResult] that always succeeds with the given value. // Of creates a [ReaderIOResult] that always succeeds with the given value.
@@ -243,14 +245,14 @@ func MonadApPar[B, A any](fab ReaderIOResult[func(A) B], fa ReaderIOResult[A]) R
return func(ctx context.Context) IOResult[B] { return func(ctx context.Context) IOResult[B] {
// quick check for cancellation // quick check for cancellation
if err := context.Cause(ctx); err != nil { if ctx.Err() != nil {
return ioeither.Left[B](err) return ioeither.Left[B](context.Cause(ctx))
} }
return func() Result[B] { return func() Result[B] {
// quick check for cancellation // quick check for cancellation
if err := context.Cause(ctx); err != nil { if ctx.Err() != nil {
return either.Left[B](err) return either.Left[B](context.Cause(ctx))
} }
// create sub-contexts for fa and fab, so they can cancel one other // create sub-contexts for fa and fab, so they can cancel one other
@@ -382,7 +384,7 @@ func Ask() ReaderIOResult[context.Context] {
// Returns a new ReaderIOResult with the chained computation. // Returns a new ReaderIOResult with the chained computation.
// //
//go:inline //go:inline
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[B] { func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainEitherK(ma, f) return RIOR.MonadChainEitherK(ma, f)
} }
@@ -395,7 +397,12 @@ func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Read
// Returns a function that chains the Either-returning function. // Returns a function that chains the Either-returning function.
// //
//go:inline //go:inline
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] { func ChainEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
return RIOR.ChainEitherK[context.Context](f)
}
//go:inline
func ChainResultK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
return RIOR.ChainEitherK[context.Context](f) return RIOR.ChainEitherK[context.Context](f)
} }
@@ -409,12 +416,12 @@ func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
// Returns a ReaderIOResult with the original value if both computations succeed. // Returns a ReaderIOResult with the original value if both computations succeed.
// //
//go:inline //go:inline
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] { func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstEitherK(ma, f) return RIOR.MonadChainFirstEitherK(ma, f)
} }
//go:inline //go:inline
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] { func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapEitherK(ma, f) return RIOR.MonadTapEitherK(ma, f)
} }
@@ -427,12 +434,12 @@ func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Reader
// Returns a function that chains the Either-returning function. // Returns a function that chains the Either-returning function.
// //
//go:inline //go:inline
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] { func ChainFirstEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
return RIOR.ChainFirstEitherK[context.Context](f) return RIOR.ChainFirstEitherK[context.Context](f)
} }
//go:inline //go:inline
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] { func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
return RIOR.TapEitherK[context.Context](f) return RIOR.TapEitherK[context.Context](f)
} }
@@ -445,7 +452,7 @@ func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
// Returns a function that chains Option-returning functions into ReaderIOResult. // Returns a function that chains Option-returning functions into ReaderIOResult.
// //
//go:inline //go:inline
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] { func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainOptionK[context.Context, A, B](onNone) return RIOR.ChainOptionK[context.Context, A, B](onNone)
} }
@@ -527,7 +534,7 @@ func Never[A any]() ReaderIOResult[A] {
// Returns a new ReaderIOResult with the chained IO computation. // Returns a new ReaderIOResult with the chained IO computation.
// //
//go:inline //go:inline
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[B] { func MonadChainIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainIOK(ma, f) return RIOR.MonadChainIOK(ma, f)
} }
@@ -540,7 +547,7 @@ func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResu
// Returns a function that chains the IO-returning function. // Returns a function that chains the IO-returning function.
// //
//go:inline //go:inline
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] { func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainIOK[context.Context](f) return RIOR.ChainIOK[context.Context](f)
} }
@@ -554,12 +561,12 @@ func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
// Returns a ReaderIOResult with the original value after executing the IO. // Returns a ReaderIOResult with the original value after executing the IO.
// //
//go:inline //go:inline
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] { func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstIOK(ma, f) return RIOR.MonadChainFirstIOK(ma, f)
} }
//go:inline //go:inline
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] { func MonadTapIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapIOK(ma, f) return RIOR.MonadTapIOK(ma, f)
} }
@@ -572,12 +579,12 @@ func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult
// Returns a function that chains the IO-returning function. // Returns a function that chains the IO-returning function.
// //
//go:inline //go:inline
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] { func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstIOK[context.Context](f) return RIOR.ChainFirstIOK[context.Context](f)
} }
//go:inline //go:inline
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] { func TapIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapIOK[context.Context](f) return RIOR.TapIOK[context.Context](f)
} }
@@ -590,7 +597,7 @@ func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
// Returns a function that chains the IOResult-returning function. // Returns a function that chains the IOResult-returning function.
// //
//go:inline //go:inline
func ChainIOEitherK[A, B any](f func(A) IOResult[B]) Operator[A, B] { func ChainIOEitherK[A, B any](f ioresult.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainIOEitherK[context.Context](f) return RIOR.ChainIOEitherK[context.Context](f)
} }
@@ -753,7 +760,7 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
// //
//go:inline //go:inline
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] { func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
return RIOR.Fold(onLeft, onRight) return RIOR.Fold(function.Flow2(onLeft, WithContext), function.Flow2(onRight, WithContext))
} }
// GetOrElse extracts the value from a [ReaderIOResult], providing a default via a function if it fails. // GetOrElse extracts the value from a [ReaderIOResult], providing a default via a function if it fails.
@@ -765,7 +772,7 @@ func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A,
// Returns a function that converts a ReaderIOResult to a ReaderIO. // Returns a function that converts a ReaderIOResult to a ReaderIO.
// //
//go:inline //go:inline
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) ReaderIO[A] { func GetOrElse[A any](onLeft readerio.Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIO[A] {
return RIOR.GetOrElse(onLeft) return RIOR.GetOrElse(onLeft)
} }
@@ -858,32 +865,32 @@ func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
} }
//go:inline //go:inline
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] { func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderIOK(ma, f) return RIOR.MonadChainReaderIOK(ma, f)
} }
//go:inline //go:inline
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] { func ChainReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainReaderIOK(f) return RIOR.ChainReaderIOK(f)
} }
//go:inline //go:inline
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] { func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderIOK(ma, f) return RIOR.MonadChainFirstReaderIOK(ma, f)
} }
//go:inline //go:inline
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] { func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderIOK(ma, f) return RIOR.MonadTapReaderIOK(ma, f)
} }
//go:inline //go:inline
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] { func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(f) return RIOR.ChainFirstReaderIOK(f)
} }
//go:inline //go:inline
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] { func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapReaderIOK(f) return RIOR.TapReaderIOK(f)
} }
@@ -913,15 +920,15 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
// //
//go:inline //go:inline
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] { func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
return RIOR.MonadChainLeft(fa, f) return RIOR.MonadChainLeft(fa, WithContextK(f))
} }
// ChainLeft is the curried version of [MonadChainLeft]. // ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult]. // It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
// //
//go:inline //go:inline
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] { func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
return RIOR.ChainLeft(f) return RIOR.ChainLeft(WithContextK(f))
} }
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error. // MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
@@ -934,12 +941,12 @@ func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResul
// //
//go:inline //go:inline
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] { func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstLeft(ma, f) return RIOR.MonadChainFirstLeft(ma, WithContextK(f))
} }
//go:inline //go:inline
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] { func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadTapLeft(ma, f) return RIOR.MonadTapLeft(ma, WithContextK(f))
} }
// ChainFirstLeft is the curried version of [MonadChainFirstLeft]. // ChainFirstLeft is the curried version of [MonadChainFirstLeft].
@@ -951,10 +958,212 @@ func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOR
// //
//go:inline //go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] { func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.ChainFirstLeft[A](f) return RIOR.ChainFirstLeft[A](WithContextK(f))
} }
//go:inline //go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] { func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](f) return RIOR.TapLeft[A](WithContextK(f))
}
// Local transforms the context.Context environment before passing it to a ReaderIOResult computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// The function checks for context cancellation before applying the transformation,
// returning an error immediately if the context is already cancelled.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
// if user := ctx.Value(userKey); user != nil {
// return user.(string)
// }
// return "unknown"
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// value, err := result(context.Background())() // Returns ("Alice", nil)
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}
// WithTimeout adds a timeout to the context for a ReaderIOResult computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderIOResult is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup. If the timeout expires,
// the computation will receive a context.DeadlineExceeded error.
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
//
// Successful Example:
//
// quickFetch := readerioresult.Right(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup. If the deadline passes, the computation
// will receive a context.DeadlineExceeded error.
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerioresult.WithDeadline[Data](deadline),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerioresult.WithDeadline[Data](laterDeadline),
// )
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
})
} }

View File

@@ -567,15 +567,13 @@ func TestMemoize(t *testing.T) {
res1 := computation(context.Background())() res1 := computation(context.Background())()
assert.True(t, E.IsRight(res1)) assert.True(t, E.IsRight(res1))
val1 := E.ToOption(res1) val1 := E.ToOption(res1)
v1, _ := O.Unwrap(val1) assert.Equal(t, O.Of(1), val1)
assert.Equal(t, 1, v1)
// Second execution should return cached value // Second execution should return cached value
res2 := computation(context.Background())() res2 := computation(context.Background())()
assert.True(t, E.IsRight(res2)) assert.True(t, E.IsRight(res2))
val2 := E.ToOption(res2) val2 := E.ToOption(res2)
v2, _ := O.Unwrap(val2) assert.Equal(t, O.Of(1), val2)
assert.Equal(t, 1, v2)
// Counter should only be incremented once // Counter should only be incremented once
assert.Equal(t, 1, counter) assert.Equal(t, 1, counter)
@@ -739,9 +737,7 @@ func TestTraverseArray(t *testing.T) {
res := result(context.Background())() res := result(context.Background())()
assert.True(t, E.IsRight(res)) assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res) arrOpt := E.ToOption(res)
assert.True(t, O.IsSome(arrOpt)) assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
resultArr, _ := O.Unwrap(arrOpt)
assert.Equal(t, []int{2, 4, 6}, resultArr)
}) })
t.Run("TraverseArray with error", func(t *testing.T) { t.Run("TraverseArray with error", func(t *testing.T) {
@@ -765,9 +761,7 @@ func TestSequenceArray(t *testing.T) {
res := result(context.Background())() res := result(context.Background())()
assert.True(t, E.IsRight(res)) assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res) arrOpt := E.ToOption(res)
assert.True(t, O.IsSome(arrOpt)) assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
resultArr, _ := O.Unwrap(arrOpt)
assert.Equal(t, []int{1, 2, 3}, resultArr)
} }
func TestTraverseRecord(t *testing.T) { func TestTraverseRecord(t *testing.T) {

View File

@@ -0,0 +1,184 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// TailRec implements stack-safe tail recursion for the context-aware ReaderIOResult monad.
//
// This function enables recursive computations that combine four powerful concepts:
// - Context awareness: Automatic cancellation checking via [context.Context]
// - Environment dependency (Reader aspect): Access to configuration, context, or dependencies
// - Side effects (IO aspect): Logging, file I/O, network calls, etc.
// - Error handling (Either aspect): Computations that can fail with an error
//
// The function uses an iterative loop to execute the recursion, making it safe for deep
// or unbounded recursion without risking stack overflow. Additionally, it integrates
// context cancellation checking through [WithContext], ensuring that recursive computations
// can be cancelled gracefully.
//
// # How It Works
//
// TailRec takes a Kleisli arrow that returns Either[A, B]:
// - Left(A): Continue recursion with the new state A
// - Right(B): Terminate recursion successfully and return the final result B
//
// The function wraps each iteration with [WithContext] to ensure context cancellation
// is checked before each recursive step. If the context is cancelled, the recursion
// terminates early with a context cancellation error.
//
// # Type Parameters
//
// - A: The state type that changes during recursion
// - B: The final result type when recursion terminates successfully
//
// # Parameters
//
// - f: A Kleisli arrow (A => ReaderIOResult[Either[A, B]]) that:
// - Takes the current state A
// - Returns a ReaderIOResult that depends on [context.Context]
// - Can fail with error (Left in the outer Either)
// - Produces Either[A, B] to control recursion flow (Right in the outer Either)
//
// # Returns
//
// A Kleisli arrow (A => ReaderIOResult[B]) that:
// - Takes an initial state A
// - Returns a ReaderIOResult that requires [context.Context]
// - Can fail with error or context cancellation
// - Produces the final result B after recursion completes
//
// # Context Cancellation
//
// Unlike the base [readerioresult.TailRec], this version automatically integrates
// context cancellation checking:
// - Each recursive iteration checks if the context is cancelled
// - If cancelled, recursion terminates immediately with a cancellation error
// - This prevents runaway recursive computations in cancelled contexts
// - Enables responsive cancellation for long-running recursive operations
//
// # Use Cases
//
// 1. Cancellable recursive algorithms:
// - Tree traversals that can be cancelled mid-operation
// - Graph algorithms with timeout requirements
// - Recursive parsers that respect cancellation
//
// 2. Long-running recursive computations:
// - File system traversals with cancellation support
// - Network operations with timeout handling
// - Database operations with connection timeout awareness
//
// 3. Interactive recursive operations:
// - User-initiated operations that can be cancelled
// - Background tasks with cancellation support
// - Streaming operations with graceful shutdown
//
// # Example: Cancellable Countdown
//
// countdownStep := func(n int) readerioresult.ReaderIOResult[either.Either[int, string]] {
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[int, string]] {
// return func() either.Either[error, either.Either[int, string]] {
// if n <= 0 {
// return either.Right[error](either.Right[int]("Done!"))
// }
// // Simulate some work
// time.Sleep(100 * time.Millisecond)
// return either.Right[error](either.Left[string](n - 1))
// }
// }
// }
//
// countdown := readerioresult.TailRec(countdownStep)
//
// // With cancellation
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
// defer cancel()
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
//
// # Example: Cancellable File Processing
//
// type ProcessState struct {
// files []string
// processed []string
// }
//
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[either.Either[ProcessState, []string]] {
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
// return func() either.Either[error, either.Either[ProcessState, []string]] {
// if len(state.files) == 0 {
// return either.Right[error](either.Right[ProcessState](state.processed))
// }
//
// file := state.files[0]
// // Process file (this could be cancelled via context)
// if err := processFileWithContext(ctx, file); err != nil {
// return either.Left[either.Either[ProcessState, []string]](err)
// }
//
// return either.Right[error](either.Left[[]string](ProcessState{
// files: state.files[1:],
// processed: append(state.processed, file),
// }))
// }
// }
// }
//
// processFiles := readerioresult.TailRec(processStep)
// ctx, cancel := context.WithCancel(context.Background())
//
// // Can be cancelled at any point during processing
// go func() {
// time.Sleep(2 * time.Second)
// cancel() // Cancel after 2 seconds
// }()
//
// result := processFiles(ProcessState{files: manyFiles})(ctx)()
//
// # Stack Safety
//
// The iterative implementation ensures that even deeply recursive computations
// (thousands or millions of iterations) will not cause stack overflow, while
// still respecting context cancellation:
//
// // Safe for very large inputs with cancellation support
// largeCountdown := readerioresult.TailRec(countdownStep)
// ctx := context.Background()
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
//
// # Performance Considerations
//
// - Each iteration includes context cancellation checking overhead
// - Context checking happens before each recursive step
// - For performance-critical code, consider the cancellation checking cost
// - The [WithContext] wrapper adds minimal overhead for cancellation safety
//
// # See Also
//
// - [readerioresult.TailRec]: Base tail recursion without automatic context checking
// - [WithContext]: Context cancellation wrapper used internally
// - [Chain]: For sequencing ReaderIOResult computations
// - [Ask]: For accessing the context
// - [Left]/[Right]: For creating error/success values
//
//go:inline
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
return RIOR.TailRec(F.Flow2(f, WithContext))
}

View File

@@ -0,0 +1,433 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"errors"
"fmt"
"sync/atomic"
"testing"
"time"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTailRec_BasicRecursion(t *testing.T) {
// Test basic countdown recursion
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
countdown := TailRec(countdownStep)
result := countdown(5)(context.Background())()
assert.Equal(t, E.Of[error]("Done!"), result)
}
func TestTailRec_FactorialRecursion(t *testing.T) {
// Test factorial computation using tail recursion
type FactorialState struct {
n int
acc int
}
factorialStep := func(state FactorialState) ReaderIOResult[E.Either[FactorialState, int]] {
return func(ctx context.Context) IOEither[E.Either[FactorialState, int]] {
return func() Either[E.Either[FactorialState, int]] {
if state.n <= 1 {
return E.Right[error](E.Right[FactorialState](state.acc))
}
return E.Right[error](E.Left[int](FactorialState{
n: state.n - 1,
acc: state.acc * state.n,
}))
}
}
}
factorial := TailRec(factorialStep)
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
assert.Equal(t, E.Of[error](120), result) // 5! = 120
}
func TestTailRec_ErrorHandling(t *testing.T) {
// Test that errors are properly propagated
testErr := errors.New("computation error")
errorStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
if n == 3 {
return E.Left[E.Either[int, string]](testErr)
}
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
errorRecursion := TailRec(errorStep)
result := errorRecursion(5)(context.Background())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
assert.Equal(t, testErr, err)
}
func TestTailRec_ContextCancellation(t *testing.T) {
// Test that recursion gets cancelled early when context is canceled
var iterationCount int32
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
atomic.AddInt32(&iterationCount, 1)
// Simulate some work
time.Sleep(50 * time.Millisecond)
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
slowRecursion := TailRec(slowStep)
// Create a context that will be cancelled after 100ms
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
start := time.Now()
result := slowRecursion(10)(ctx)()
elapsed := time.Since(start)
// Should be cancelled and return an error
assert.True(t, E.IsLeft(result))
// Should complete quickly due to cancellation (much less than 10 * 50ms = 500ms)
assert.Less(t, elapsed, 200*time.Millisecond)
// Should have executed only a few iterations before cancellation
iterations := atomic.LoadInt32(&iterationCount)
assert.Less(t, iterations, int32(5), "Should have been cancelled before completing all iterations")
}
func TestTailRec_ImmediateCancellation(t *testing.T) {
// Test with an already cancelled context
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
countdown := TailRec(countdownStep)
// Create an already cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
result := countdown(5)(ctx)()
// Should immediately return a cancellation error
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
assert.Equal(t, context.Canceled, err)
}
func TestTailRec_StackSafety(t *testing.T) {
// Test that deep recursion doesn't cause stack overflow
const largeN = 10000
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
return func(ctx context.Context) IOEither[E.Either[int, int]] {
return func() Either[E.Either[int, int]] {
if n <= 0 {
return E.Right[error](E.Right[int](0))
}
return E.Right[error](E.Left[int](n - 1))
}
}
}
countdown := TailRec(countdownStep)
result := countdown(largeN)(context.Background())()
assert.Equal(t, E.Of[error](0), result)
}
func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
// Test stack safety with cancellation after many iterations
const largeN = 100000
var iterationCount int32
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
return func(ctx context.Context) IOEither[E.Either[int, int]] {
return func() Either[E.Either[int, int]] {
atomic.AddInt32(&iterationCount, 1)
// Add a small delay every 1000 iterations to make cancellation more likely
if n%1000 == 0 {
time.Sleep(1 * time.Millisecond)
}
if n <= 0 {
return E.Right[error](E.Right[int](0))
}
return E.Right[error](E.Left[int](n - 1))
}
}
}
countdown := TailRec(countdownStep)
// Cancel after 50ms to allow some iterations but not all
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
result := countdown(largeN)(ctx)()
// Should be cancelled (or completed if very fast)
// The key is that it doesn't cause a stack overflow
iterations := atomic.LoadInt32(&iterationCount)
assert.Greater(t, iterations, int32(0))
// If it was cancelled, verify it didn't complete all iterations
if E.IsLeft(result) {
assert.Less(t, iterations, int32(largeN))
}
}
func TestTailRec_ComplexState(t *testing.T) {
// Test with more complex state management
type ProcessState struct {
items []string
processed []string
errors []error
}
processStep := func(state ProcessState) ReaderIOResult[E.Either[ProcessState, []string]] {
return func(ctx context.Context) IOEither[E.Either[ProcessState, []string]] {
return func() Either[E.Either[ProcessState, []string]] {
if A.IsEmpty(state.items) {
return E.Right[error](E.Right[ProcessState](state.processed))
}
item := state.items[0]
// Simulate processing that might fail for certain items
if item == "error-item" {
return E.Left[E.Either[ProcessState, []string]](
fmt.Errorf("failed to process item: %s", item))
}
return E.Right[error](E.Left[[]string](ProcessState{
items: state.items[1:],
processed: append(state.processed, item),
errors: state.errors,
}))
}
}
}
processItems := TailRec(processStep)
t.Run("successful processing", func(t *testing.T) {
initialState := ProcessState{
items: []string{"item1", "item2", "item3"},
processed: []string{},
errors: []error{},
}
result := processItems(initialState)(context.Background())()
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
})
t.Run("processing with error", func(t *testing.T) {
initialState := ProcessState{
items: []string{"item1", "error-item", "item3"},
processed: []string{},
errors: []error{},
}
result := processItems(initialState)(context.Background())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
assert.Contains(t, err.Error(), "failed to process item: error-item")
})
}
func TestTailRec_CancellationDuringProcessing(t *testing.T) {
// Test cancellation during a realistic processing scenario
type FileProcessState struct {
files []string
processed int
}
var processedCount int32
processFileStep := func(state FileProcessState) ReaderIOResult[E.Either[FileProcessState, int]] {
return func(ctx context.Context) IOEither[E.Either[FileProcessState, int]] {
return func() Either[E.Either[FileProcessState, int]] {
if A.IsEmpty(state.files) {
return E.Right[error](E.Right[FileProcessState](state.processed))
}
// Simulate file processing time
time.Sleep(20 * time.Millisecond)
atomic.AddInt32(&processedCount, 1)
return E.Right[error](E.Left[int](FileProcessState{
files: state.files[1:],
processed: state.processed + 1,
}))
}
}
}
processFiles := TailRec(processFileStep)
// Create many files to process
files := make([]string, 20)
for i := range files {
files[i] = fmt.Sprintf("file%d.txt", i)
}
initialState := FileProcessState{
files: files,
processed: 0,
}
// Cancel after 100ms (should allow ~5 files to be processed)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
start := time.Now()
result := processFiles(initialState)(ctx)()
elapsed := time.Since(start)
// Should be cancelled
assert.True(t, E.IsLeft(result))
// Should complete quickly due to cancellation
assert.Less(t, elapsed, 150*time.Millisecond)
// Should have processed some but not all files
processed := atomic.LoadInt32(&processedCount)
assert.Greater(t, processed, int32(0))
assert.Less(t, processed, int32(20))
}
func TestTailRec_ZeroIterations(t *testing.T) {
// Test case where recursion terminates immediately
immediateStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
return E.Right[error](E.Right[int]("immediate"))
}
}
}
immediate := TailRec(immediateStep)
result := immediate(100)(context.Background())()
assert.Equal(t, E.Of[error]("immediate"), result)
}
func TestTailRec_ContextWithDeadline(t *testing.T) {
// Test with context deadline
var iterationCount int32
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
atomic.AddInt32(&iterationCount, 1)
time.Sleep(30 * time.Millisecond)
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
slowRecursion := TailRec(slowStep)
// Set deadline 80ms from now
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
defer cancel()
result := slowRecursion(10)(ctx)()
// Should be cancelled due to deadline
assert.True(t, E.IsLeft(result))
// Should have executed only a few iterations
iterations := atomic.LoadInt32(&iterationCount)
assert.Greater(t, iterations, int32(0))
assert.Less(t, iterations, int32(5))
}
func TestTailRec_ContextWithValue(t *testing.T) {
// Test that context values are preserved through recursion
type contextKey string
const testKey contextKey = "test"
valueStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
value := ctx.Value(testKey)
require.NotNil(t, value)
assert.Equal(t, "test-value", value.(string))
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
valueRecursion := TailRec(valueStep)
ctx := context.WithValue(context.Background(), testKey, "test-value")
result := valueRecursion(3)(ctx)()
assert.Equal(t, E.Of[error]("Done!"), result)
}

View File

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

View File

@@ -0,0 +1,179 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"time"
RIO "github.com/IBM/fp-go/v2/context/readerio"
R "github.com/IBM/fp-go/v2/retry"
RG "github.com/IBM/fp-go/v2/retry/generic"
)
// Retrying retries a ReaderIOResult computation according to a retry policy with context awareness.
//
// This function implements a retry mechanism for operations that depend on a [context.Context],
// perform side effects (IO), and can fail (Result). It respects context cancellation, meaning
// that if the context is cancelled during retry delays, the operation will stop immediately
// and return the cancellation error.
//
// The retry loop will continue until one of the following occurs:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable failure)
// - The context is cancelled (returns context.Canceled or context.DeadlineExceeded)
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderIOResult[A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.). The action depends
// on a context.Context and produces a Result[A]. The context passed to the action
// will be the same context used for retry delays, so cancellation is properly propagated.
//
// - check: A predicate function that examines the Result[A] and returns true if the
// operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable failures (e.g., network timeouts) and permanent
// failures (e.g., invalid input). Note that context cancellation errors will
// automatically stop retrying regardless of this function's return value.
//
// Returns:
//
// A ReaderIOResult[A] that, when executed with a context, will perform the retry
// logic with context cancellation support and return the final result.
//
// Type Parameters:
// - A: The type of the success value
//
// Context Cancellation:
//
// The retry mechanism respects context cancellation in two ways:
// 1. During retry delays: If the context is cancelled while waiting between retries,
// the operation stops immediately and returns the context error.
// 2. During action execution: If the action itself checks the context and returns
// an error due to cancellation, the retry loop will stop (assuming the check
// function doesn't force a retry on context errors).
//
// Example:
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := M.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )(retry.Monoid)
//
// // Action that fetches data, with retry status information
// fetchData := func(status retry.RetryStatus) ReaderIOResult[string] {
// return func(ctx context.Context) IOResult[string] {
// return func() Result[string] {
// // Check if context is cancelled
// if ctx.Err() != nil {
// return result.Left[string](ctx.Err())
// }
// // Simulate an HTTP request that might fail
// if status.IterNumber < 3 {
// return result.Left[string](fmt.Errorf("temporary error"))
// }
// return result.Of("success")
// }
// }
// }
//
// // Check function: retry on any error except context cancellation
// shouldRetry := func(r Result[string]) bool {
// return result.IsLeft(r) && !errors.Is(result.GetLeft(r), context.Canceled)
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// ioResult := retryingFetch(ctx)
// finalResult := ioResult()
//
// See also:
// - retry.RetryPolicy for available retry policies
// - retry.RetryStatus for information passed to the action
// - context.Context for context cancellation semantics
//
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(Result[A]) bool,
) ReaderIOResult[A] {
// delayWithCancel implements a context-aware delay mechanism for retry operations.
// It creates a timeout context that will be cancelled when either:
// 1. The delay duration expires (normal case), or
// 2. The parent context is cancelled (early termination)
//
// The function waits on timeoutCtx.Done(), which will be signaled in either case:
// - If the delay expires, timeoutCtx is cancelled by the timeout
// - If the parent ctx is cancelled, timeoutCtx inherits the cancellation
//
// After the wait completes, we dispatch to the next action by calling ri(ctx)().
// This works correctly because the action is wrapped in WithContextK, which handles
// context cancellation by checking ctx.Err() and returning an appropriate error
// (context.Canceled or context.DeadlineExceeded) when the context is cancelled.
//
// This design ensures that:
// - Retry delays respect context cancellation and terminate immediately
// - The cancellation error propagates correctly through the retry chain
// - No unnecessary delays occur when the context is already cancelled
delayWithCancel := func(delay time.Duration) RIO.Operator[R.RetryStatus, R.RetryStatus] {
return func(ri ReaderIO[R.RetryStatus]) ReaderIO[R.RetryStatus] {
return func(ctx context.Context) IO[R.RetryStatus] {
return func() R.RetryStatus {
// Create a timeout context that will be cancelled when either:
// - The delay duration expires, or
// - The parent context is cancelled
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
defer cancelTimeout()
// Wait for either the timeout or parent context cancellation
<-timeoutCtx.Done()
// Dispatch to the next action with the original context.
// WithContextK will handle context cancellation correctly.
return ri(ctx)()
}
}
}
}
// get an implementation for the types
return RG.Retrying(
RIO.Chain[Result[A], Result[A]],
RIO.Chain[R.RetryStatus, Result[A]],
RIO.Of[Result[A]],
RIO.Of[R.RetryStatus],
delayWithCancel,
policy,
WithContextK(action),
check,
)
}

View File

@@ -0,0 +1,511 @@
// 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 readerioresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/IBM/fp-go/v2/result"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// Helper function to create a test retry policy
func testRetryPolicy() R.RetryPolicy {
return R.Monoid.Concat(
R.LimitRetries(5),
R.CapDelay(1*time.Second, R.ExponentialBackoff(10*time.Millisecond)),
)
}
// TestRetrying_SuccessOnFirstAttempt tests that Retrying succeeds immediately
// when the action succeeds on the first attempt.
func TestRetrying_SuccessOnFirstAttempt(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("success")
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Of("success"), res)
}
// TestRetrying_SuccessAfterRetries tests that Retrying eventually succeeds
// after a few failed attempts.
func TestRetrying_SuccessAfterRetries(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
// Fail on first 3 attempts, succeed on 4th
if status.IterNumber < 3 {
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
return result.Of(fmt.Sprintf("success on attempt %d", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Of("success on attempt 3"), res)
}
// TestRetrying_ExhaustsRetries tests that Retrying stops after the retry limit
// is reached and returns the last error.
func TestRetrying_ExhaustsRetries(t *testing.T) {
policy := R.LimitRetries(3)
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.True(t, result.IsLeft(res))
assert.Equal(t, result.Left[string](fmt.Errorf("attempt 3 failed")), res)
}
// TestRetrying_ActionChecksContextCancellation tests that actions can check
// the context and return early if it's cancelled.
func TestRetrying_ActionChecksContextCancellation(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Check context at the start of the action
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Simulate work that might take time
time.Sleep(10 * time.Millisecond)
// Check context again after work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Always fail to trigger retries
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
// Don't retry on context errors
val, err := result.Unwrap(r)
_ = val
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel after a short time
ctx, cancel := context.WithCancel(t.Context())
// Start the retry operation in a goroutine
resultChan := make(chan Result[string], 1)
go func() {
res := retrying(ctx)()
resultChan <- res
}()
// Cancel the context after allowing a couple attempts
time.Sleep(50 * time.Millisecond)
cancel()
// Wait for the result
res := <-resultChan
// Should have stopped due to context cancellation
assert.True(t, result.IsLeft(res))
// Should have stopped early (not all 10 attempts)
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context cancellation")
// The error should be related to context cancellation or an early attempt
val, err := result.Unwrap(res)
_ = val
assert.Error(t, err)
}
// TestRetrying_ContextCancelledBeforeStart tests that if the context is already
// cancelled before starting, the operation fails immediately.
func TestRetrying_ContextCancelledBeforeStart(t *testing.T) {
policy := testRetryPolicy()
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
// Don't retry on context errors
val, err := result.Unwrap(r)
_ = val
if err != nil && errors.Is(err, context.Canceled) {
return false
}
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create an already-cancelled context
ctx, cancel := context.WithCancel(t.Context())
cancel()
res := retrying(ctx)()
assert.True(t, result.IsLeft(res))
val, err := result.Unwrap(res)
_ = val
assert.True(t, errors.Is(err, context.Canceled))
// Should have attempted at most once
assert.LessOrEqual(t, attemptCount, 1)
}
// TestRetrying_ContextTimeoutInAction tests that actions respect context deadlines.
func TestRetrying_ContextTimeoutInAction(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Simulate some work
time.Sleep(50 * time.Millisecond)
// Check context after work
if ctx.Err() != nil {
return result.Left[string](ctx.Err())
}
// Always fail to trigger retries
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
check := func(r Result[string]) bool {
// Don't retry on context errors
val, err := result.Unwrap(r)
_ = val
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create a context with a short timeout
ctx, cancel := context.WithTimeout(t.Context(), 150*time.Millisecond)
defer cancel()
startTime := time.Now()
res := retrying(ctx)()
elapsed := time.Since(startTime)
assert.True(t, result.IsLeft(res))
// Should have stopped before completing all 10 retries
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context timeout")
// Should have stopped around the timeout duration
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop soon after timeout")
}
// TestRetrying_CheckFunctionStopsRetry tests that the check function can
// stop retrying even when errors occur.
func TestRetrying_CheckFunctionStopsRetry(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
if status.IterNumber == 0 {
return result.Left[string](fmt.Errorf("retryable error"))
}
return result.Left[string](fmt.Errorf("permanent error"))
}
}
}
// Only retry on "retryable error"
check := func(r Result[string]) bool {
return result.IsLeft(r) && result.Fold(
func(err error) bool { return err.Error() == "retryable error" },
func(string) bool { return false },
)(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Left[string](fmt.Errorf("permanent error")), res)
}
// TestRetrying_ExponentialBackoff tests that exponential backoff is applied.
func TestRetrying_ExponentialBackoff(t *testing.T) {
// Use a policy with measurable delays
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.ExponentialBackoff(50*time.Millisecond),
)
startTime := time.Now()
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
if status.IterNumber < 2 {
return result.Left[string](fmt.Errorf("retry"))
}
return result.Of("success")
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
elapsed := time.Since(startTime)
assert.Equal(t, result.Of("success"), res)
// With exponential backoff starting at 50ms:
// Iteration 0: no delay
// Iteration 1: 50ms delay
// Iteration 2: 100ms delay
// Total should be at least 150ms
assert.GreaterOrEqual(t, elapsed, 150*time.Millisecond)
}
// TestRetrying_ContextValuePropagation tests that context values are properly
// propagated through the retry mechanism.
func TestRetrying_ContextValuePropagation(t *testing.T) {
policy := R.LimitRetries(2)
type contextKey string
const requestIDKey contextKey = "requestID"
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
// Extract value from context
requestID, ok := ctx.Value(requestIDKey).(string)
if !ok {
return result.Left[string](fmt.Errorf("missing request ID"))
}
if status.IterNumber < 1 {
return result.Left[string](fmt.Errorf("retry needed"))
}
return result.Of(fmt.Sprintf("processed request %s", requestID))
}
}
}
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create context with a value
ctx := context.WithValue(t.Context(), requestIDKey, "12345")
res := retrying(ctx)()
assert.Equal(t, result.Of("processed request 12345"), res)
}
// TestRetrying_RetryStatusProgression tests that the RetryStatus is properly
// updated on each iteration.
func TestRetrying_RetryStatusProgression(t *testing.T) {
policy := testRetryPolicy()
var iterations []uint
action := func(status R.RetryStatus) ReaderIOResult[int] {
return func(ctx context.Context) IOResult[int] {
return func() Result[int] {
iterations = append(iterations, status.IterNumber)
if status.IterNumber < 3 {
return result.Left[int](fmt.Errorf("retry"))
}
return result.Of(int(status.IterNumber))
}
}
}
check := func(r Result[int]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
ctx := t.Context()
res := retrying(ctx)()
assert.Equal(t, result.Of(3), res)
// Should have attempted iterations 0, 1, 2, 3
assert.Equal(t, []uint{0, 1, 2, 3}, iterations)
}
// TestRetrying_ContextCancelledDuringDelay tests that the retry operation
// stops immediately when the context is cancelled during a retry delay,
// even if there are still retries remaining according to the policy.
func TestRetrying_ContextCancelledDuringDelay(t *testing.T) {
// Use a policy with significant delays to ensure we can cancel during the delay
policy := R.Monoid.Concat(
R.LimitRetries(10),
R.ConstantDelay(200*time.Millisecond),
)
attemptCount := 0
action := func(status R.RetryStatus) ReaderIOResult[string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
attemptCount++
// Always fail to trigger retries
return result.Left[string](fmt.Errorf("attempt %d failed", status.IterNumber))
}
}
}
// Always retry on errors (don't check for context cancellation in check function)
check := func(r Result[string]) bool {
return result.IsLeft(r)
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel during the retry delay
ctx, cancel := context.WithCancel(t.Context())
// Start the retry operation in a goroutine
resultChan := make(chan Result[string], 1)
startTime := time.Now()
go func() {
res := retrying(ctx)()
resultChan <- res
}()
// Wait for the first attempt to complete and the delay to start
time.Sleep(50 * time.Millisecond)
// Cancel the context during the retry delay
cancel()
// Wait for the result
res := <-resultChan
elapsed := time.Since(startTime)
// Should have stopped due to context cancellation
assert.True(t, result.IsLeft(res))
// Should have attempted only once or twice (not all 10 attempts)
// because the context was cancelled during the delay
assert.LessOrEqual(t, attemptCount, 2, "Should stop retrying when context is cancelled during delay")
// Should have stopped quickly after cancellation, not waiting for all delays
// With 10 retries and 200ms delays, it would take ~2 seconds without cancellation
// With cancellation during first delay, it should complete in well under 500ms
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop immediately when context is cancelled during delay")
// When context is cancelled during the delay, the retry mechanism
// detects the cancellation and returns a context error
val, err := result.Unwrap(res)
_ = val
assert.Error(t, err)
// The error should be a context cancellation error since cancellation
// happened during the delay between retries
assert.True(t, errors.Is(err, context.Canceled), "Should return context.Canceled when cancelled during delay")
}

View File

@@ -18,6 +18,7 @@ package readerioresult
import ( import (
"github.com/IBM/fp-go/v2/array" "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/function" "github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/record" "github.com/IBM/fp-go/v2/internal/record"
) )
@@ -34,7 +35,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
Map[[]B, func(B) []B], Map[[]B, func(B) []B],
Ap[[]B, B], Ap[[]B, B],
f, F.Flow2(f, WithContext),
) )
} }
@@ -78,7 +79,7 @@ func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, ma
Map[map[K]B, func(B) map[K]B], Map[map[K]B, func(B) map[K]B],
Ap[map[K]B, B], Ap[map[K]B, B],
f, F.Flow2(f, WithContext),
) )
} }
@@ -123,7 +124,7 @@ func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
Map[[]B, func(B) []B], Map[[]B, func(B) []B],
ApSeq[[]B, B], ApSeq[[]B, B],
as, as,
f, F.Flow2(f, WithContext),
) )
} }
@@ -139,7 +140,7 @@ func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
Of[[]B], Of[[]B],
Map[[]B, func(B) []B], Map[[]B, func(B) []B],
ApSeq[[]B, B], ApSeq[[]B, B],
f, F.Flow2(f, WithContext),
) )
} }
@@ -171,7 +172,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
Map[map[K]B, func(B) map[K]B], Map[map[K]B, func(B) map[K]B],
ApSeq[map[K]B, B], ApSeq[map[K]B, B],
as, as,
f, F.Flow2(f, WithContext),
) )
} }
@@ -182,7 +183,7 @@ func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
Map[map[K]B, func(B) map[K]B], Map[map[K]B, func(B) map[K]B],
ApSeq[map[K]B, B], ApSeq[map[K]B, B],
f, F.Flow2(f, WithContext),
) )
} }
@@ -216,7 +217,7 @@ func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
Map[[]B, func(B) []B], Map[[]B, func(B) []B],
ApPar[[]B, B], ApPar[[]B, B],
as, as,
f, F.Flow2(f, WithContext),
) )
} }
@@ -232,7 +233,7 @@ func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
Of[[]B], Of[[]B],
Map[[]B, func(B) []B], Map[[]B, func(B) []B],
ApPar[[]B, B], ApPar[[]B, B],
f, F.Flow2(f, WithContext),
) )
} }
@@ -264,7 +265,7 @@ func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
Map[map[K]B, func(B) map[K]B], Map[map[K]B, func(B) map[K]B],
ApPar[map[K]B, B], ApPar[map[K]B, B],
f, F.Flow2(f, WithContext),
) )
} }
@@ -286,7 +287,7 @@ func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
Map[map[K]B, func(B) map[K]B], Map[map[K]B, func(B) map[K]B],
ApPar[map[K]B, B], ApPar[map[K]B, B],
as, as,
f, F.Flow2(f, WithContext),
) )
} }

View File

@@ -18,12 +18,16 @@ package readerioresult
import ( import (
"context" "context"
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/context/ioresult" "github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/context/readerresult" "github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either" "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither" "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/lazy" "github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither" "github.com/IBM/fp-go/v2/readereither"
@@ -126,4 +130,11 @@ type (
ReaderResult[A any] = readerresult.ReaderResult[A] ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A] ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A] ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Consumer[A any] = consumer.Consumer[A]
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
) )

View File

@@ -15,11 +15,14 @@
package readerresult package readerresult
import "github.com/IBM/fp-go/v2/readereither" import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/readereither"
)
// TraverseArray transforms an array // TraverseArray transforms an array
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] { func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return readereither.TraverseArray(f) return readereither.TraverseArray(F.Flow2(f, WithContext))
} }
// TraverseArrayWithIndex transforms an array // TraverseArrayWithIndex transforms an array

View File

@@ -17,7 +17,6 @@ package readerresult
import ( import (
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
G "github.com/IBM/fp-go/v2/readereither/generic" G "github.com/IBM/fp-go/v2/readereither/generic"
) )
@@ -31,16 +30,26 @@ import (
// TenantID string // TenantID string
// } // }
// result := readereither.Do(State{}) // result := readereither.Do(State{})
//
//go:inline
func Do[S any]( func Do[S any](
empty S, empty S,
) ReaderResult[S] { ) ReaderResult[S] {
return G.Do[ReaderResult[S]](empty) return G.Do[ReaderResult[S]](empty)
} }
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]. // Bind attaches the result of an EFFECTFUL computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps // This enables sequential composition where each step can depend on the results of previous steps
// and access the context.Context from the environment. // and access the context.Context from the environment.
// //
// IMPORTANT: Bind is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The function parameter takes state and returns a ReaderResult[T], which is effectful because
// it depends on context.Context (can be cancelled, has deadlines, carries values).
//
// For PURE FUNCTIONS (side-effect free), use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
// - Let: For pure functions without errors (State -> Value)
//
// The setter function takes the result of the computation and returns a function that // The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2. // updates the context from S1 to S2.
// //
@@ -78,14 +87,27 @@ func Do[S any](
// }, // },
// ), // ),
// ) // )
//
//go:inline
func Bind[S1, S2, T any]( func Bind[S1, S2, T any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
f Kleisli[S1, T], f Kleisli[S1, T],
) Kleisli[ReaderResult[S1], S2] { ) Kleisli[ReaderResult[S1], S2] {
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, f) return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, F.Flow2(f, WithContext))
} }
// Let attaches the result of a computation to a context [S1] to produce a context [S2] // Let attaches the result of a PURE computation to a context [S1] to produce a context [S2].
//
// IMPORTANT: Let is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The function parameter takes state and returns a value directly, with no errors or effects.
//
// For EFFECTFUL FUNCTIONS (that need context.Context), use:
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
//
// For PURE FUNCTIONS with error handling, use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
//
//go:inline
func Let[S1, S2, T any]( func Let[S1, S2, T any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
f func(S1) T, f func(S1) T,
@@ -93,7 +115,10 @@ func Let[S1, S2, T any](
return G.Let[ReaderResult[S1], ReaderResult[S2]](setter, f) return G.Let[ReaderResult[S1], ReaderResult[S2]](setter, f)
} }
// 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 PURE operation (side-effect free) that simply sets a field to a constant value.
//
//go:inline
func LetTo[S1, S2, T any]( func LetTo[S1, S2, T any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
b T, b T,
@@ -102,15 +127,27 @@ func LetTo[S1, S2, T any](
} }
// BindTo initializes a new state [S1] from a value [T] // BindTo initializes a new state [S1] from a value [T]
//
//go:inline
func BindTo[S1, T any]( func BindTo[S1, T any](
setter func(T) S1, setter func(T) S1,
) Kleisli[ReaderResult[T], S1] { ) Operator[T, S1] {
return G.BindTo[ReaderResult[S1], ReaderResult[T]](setter) return G.BindTo[ReaderResult[S1], ReaderResult[T]](setter)
} }
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering // ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad). // the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other. // This allows independent EFFECTFUL computations to be combined without one depending on the result of the other.
//
// IMPORTANT: ApS is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The ReaderResult parameter is effectful because it depends on context.Context.
// //
// Unlike Bind, which sequences operations, ApS can be used when operations are independent // Unlike Bind, which sequences operations, ApS can be used when operations are independent
// and can conceptually run in parallel. // and can conceptually run in parallel.
@@ -145,6 +182,8 @@ func BindTo[S1, T any](
// getTenantID, // getTenantID,
// ), // ),
// ) // )
//
//go:inline
func ApS[S1, S2, T any]( func ApS[S1, S2, T any](
setter func(T) func(S1) S2, setter func(T) func(S1) S2,
fa ReaderResult[T], fa ReaderResult[T],
@@ -183,17 +222,24 @@ func ApS[S1, S2, T any](
// readereither.Do(Person{Name: "Alice", Age: 25}), // readereither.Do(Person{Name: "Alice", Age: 25}),
// readereither.ApSL(ageLens, getAge), // readereither.ApSL(ageLens, getAge),
// ) // )
//
//go:inline
func ApSL[S, T any]( func ApSL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
fa ReaderResult[T], fa ReaderResult[T],
) Kleisli[ReaderResult[S], S] { ) Kleisli[ReaderResult[S], S] {
return ApS(lens.Set, fa) return ApS(lens.Set, fa)
} }
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state. // BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
// It combines the lens-based field access with monadic composition, allowing you to: // It combines the lens-based field access with monadic composition for EFFECTFUL computations.
//
// IMPORTANT: BindL is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The function parameter returns a ReaderResult, which is effectful.
//
// It allows you to:
// 1. Extract a field value using the lens // 1. Extract a field value using the lens
// 2. Use that value in a computation that may fail // 2. Use that value in an effectful computation that may fail
// 3. Update the field with the result // 3. Update the field with the result
// //
// Parameters: // Parameters:
@@ -227,15 +273,20 @@ func ApSL[S, T any](
// readereither.Of[error](Counter{Value: 42}), // readereither.Of[error](Counter{Value: 42}),
// readereither.BindL(valueLens, increment), // readereither.BindL(valueLens, increment),
// ) // )
//
//go:inline
func BindL[S, T any]( func BindL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f Kleisli[T, T], f Kleisli[T, T],
) Kleisli[ReaderResult[S], S] { ) Kleisli[ReaderResult[S], S] {
return Bind(lens.Set, F.Flow2(lens.Get, f)) return Bind(lens.Set, F.Flow2(lens.Get, F.Flow2(f, WithContext)))
} }
// LetL is a variant of Let that uses a lens to focus on a specific field in the state. // LetL is a variant of Let that uses a lens to focus on a specific field in the state.
// It applies a pure transformation to the focused field without any effects. // It applies a PURE transformation to the focused field without any effects.
//
// IMPORTANT: LetL is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The function parameter is a pure endomorphism (T -> T) with no errors or effects.
// //
// Parameters: // Parameters:
// - lens: A lens that focuses on a field of type T within state S // - lens: A lens that focuses on a field of type T within state S
@@ -262,15 +313,17 @@ func BindL[S, T any](
// readereither.LetL(valueLens, double), // readereither.LetL(valueLens, double),
// ) // )
// // result when executed will be Right(Counter{Value: 42}) // // result when executed will be Right(Counter{Value: 42})
//
//go:inline
func LetL[S, T any]( func LetL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
f func(T) T, f Endomorphism[T],
) Kleisli[ReaderResult[S], S] { ) Kleisli[ReaderResult[S], S] {
return Let(lens.Set, F.Flow2(lens.Get, f)) return Let(lens.Set, F.Flow2(lens.Get, f))
} }
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state. // LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
// It sets the focused field to a constant value. // It sets the focused field to a constant value. This is a PURE operation (side-effect free).
// //
// Parameters: // Parameters:
// - lens: A lens that focuses on a field of type T within state S // - lens: A lens that focuses on a field of type T within state S
@@ -296,8 +349,10 @@ func LetL[S, T any](
// readereither.LetToL(debugLens, false), // readereither.LetToL(debugLens, false),
// ) // )
// // result when executed will be Right(Config{Debug: false, Timeout: 30}) // // result when executed will be Right(Config{Debug: false, Timeout: 30})
//
//go:inline
func LetToL[S, T any]( func LetToL[S, T any](
lens L.Lens[S, T], lens Lens[S, T],
b T, b T,
) Kleisli[ReaderResult[S], S] { ) Kleisli[ReaderResult[S], S] {
return LetTo(lens.Set, b) return LetTo(lens.Set, b)

View File

@@ -19,14 +19,23 @@ import (
"context" "context"
E "github.com/IBM/fp-go/v2/either" E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
) )
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating // withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] { func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) E.Either[error, A] { return func(ctx context.Context) E.Either[error, A] {
if err := context.Cause(ctx); err != nil { if ctx.Err() != nil {
return E.Left[A](err) return E.Left[A](context.Cause(ctx))
} }
return ma(ctx) return ma(ctx)
} }
} }
//go:inline
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
return F.Flow2(
f,
WithContext,
)
}

View File

@@ -1,3 +1,18 @@
// 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 readerresult package readerresult
import ( import (
@@ -7,11 +22,131 @@ import (
RR "github.com/IBM/fp-go/v2/readerresult" RR "github.com/IBM/fp-go/v2/readerresult"
) )
// SequenceReader swaps the order of environment parameters when the inner computation is a Reader.
//
// This function is specialized for the context.Context-based ReaderResult monad. It takes a
// ReaderResult that produces a Reader and returns a reader.Kleisli that produces Results.
// The context.Context is implicitly used as the outer environment type.
//
// Type Parameters:
// - R: The inner environment type (becomes outer after flip)
// - A: The success value type
//
// Parameters:
// - ma: A ReaderResult that takes context.Context and may produce a Reader[R, A]
//
// Returns:
// - A reader.Kleisli[context.Context, R, Result[A]], which is func(context.Context) func(R) Result[A]
//
// The function preserves error handling from the outer ReaderResult layer. If the outer
// computation fails, the error is propagated to the inner Result.
//
// Note: This is an inline wrapper around readerresult.SequenceReader, specialized for
// context.Context as the outer environment type.
//
// Example:
//
// type Database struct {
// ConnectionString string
// }
//
// // Original: takes context, may fail, produces Reader[Database, string]
// original := func(ctx context.Context) result.Result[reader.Reader[Database, string]] {
// if ctx.Err() != nil {
// return result.Error[reader.Reader[Database, string]](ctx.Err())
// }
// return result.Ok[error](func(db Database) string {
// return fmt.Sprintf("Query on %s", db.ConnectionString)
// })
// }
//
// // Sequenced: takes context first, then Database
// sequenced := SequenceReader(original)
//
// ctx := context.Background()
// db := Database{ConnectionString: "localhost:5432"}
//
// // Apply context first to get a function that takes database
// dbReader := sequenced(ctx)
// // Then apply database to get the final result
// result := dbReader(db)
// // result is Result[string]
//
// Use Cases:
// - Dependency injection: Flip parameter order to inject context first, then dependencies
// - Testing: Separate context handling from business logic for easier testing
// - Composition: Enable point-free style by fixing the context parameter first
//
//go:inline //go:inline
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] { func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
return RR.SequenceReader(ma) return RR.SequenceReader(ma)
} }
// TraverseReader transforms a value using a Reader function and swaps environment parameter order.
//
// This function combines mapping and parameter flipping in a single operation. It takes a
// Reader function (pure computation without error handling) and returns a function that:
// 1. Maps a ReaderResult[A] to ReaderResult[B] using the provided Reader function
// 2. Flips the parameter order so R comes before context.Context
//
// Type Parameters:
// - R: The inner environment type (becomes outer after flip)
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A reader.Kleisli[R, A, B], which is func(R) func(A) B - a pure Reader function
//
// Returns:
// - A function that takes ReaderResult[A] and returns Kleisli[R, B]
// - Kleisli[R, B] is func(R) ReaderResult[B], which is func(R) func(context.Context) Result[B]
//
// The function preserves error handling from the input ReaderResult. If the input computation
// fails, the error is propagated without applying the transformation function.
//
// Note: This is a wrapper around readerresult.TraverseReader, specialized for context.Context.
//
// Example:
//
// type Config struct {
// MaxRetries int
// }
//
// // A pure Reader function that depends on Config
// formatMessage := func(cfg Config) func(int) string {
// return func(value int) string {
// return fmt.Sprintf("Value: %d, MaxRetries: %d", value, cfg.MaxRetries)
// }
// }
//
// // Original computation that may fail
// computation := func(ctx context.Context) result.Result[int] {
// if ctx.Err() != nil {
// return result.Error[int](ctx.Err())
// }
// return result.Ok[error](42)
// }
//
// // Create a traversal that applies formatMessage and flips parameters
// traverse := TraverseReader[Config, int, string](formatMessage)
//
// // Apply to the computation
// flipped := traverse(computation)
//
// // Now we can provide Config first, then context
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
//
// result := flipped(cfg)(ctx)
// // result is Result[string] containing "Value: 42, MaxRetries: 3"
//
// Use Cases:
// - Dependency injection: Inject configuration/dependencies before context
// - Testing: Separate pure business logic from context handling
// - Composition: Build pipelines where dependencies are fixed before execution
// - Point-free style: Enable partial application by fixing dependencies first
//
//go:inline
func TraverseReader[R, A, B any]( func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B], f reader.Kleisli[R, A, B],
) func(ReaderResult[A]) Kleisli[R, B] { ) func(ReaderResult[A]) Kleisli[R, B] {

View File

@@ -0,0 +1,215 @@
// 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 readerresult provides logging utilities for the ReaderResult monad,
// which combines the Reader monad (for dependency injection via context.Context)
// with the Result monad (for error handling).
//
// The logging functions in this package allow you to log Result values (both
// successes and errors) while preserving the functional composition style.
package readerresult
import (
"context"
"log/slog"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/logging"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
// curriedLog creates a curried logging function that takes an slog.Attr and a context,
// then logs the attribute with the specified log level and message.
//
// This is an internal helper function used to create the logging pipeline in a
// point-free style. The currying allows for partial application in functional
// composition.
//
// Parameters:
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelError)
// - cb: A callback function that retrieves a logger from the context
// - message: The log message to display
//
// Returns:
// - A curried function that takes an slog.Attr, then a context, and performs logging
func curriedLog(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) func(slog.Attr) Reader[context.Context, struct{}] {
return F.Curry2(func(a slog.Attr, ctx context.Context) struct{} {
cb(ctx).LogAttrs(ctx, logLevel, message, a)
return struct{}{}
})
}
// SLogWithCallback creates a Kleisli arrow that logs a Result value using a custom
// logger callback and log level. The Result value is logged and then returned unchanged,
// making this function suitable for use in functional pipelines.
//
// This function logs both successful values and errors:
// - Success values are logged with the key "value"
// - Error values are logged with the key "error"
//
// The logging is performed as a side effect while preserving the Result value,
// allowing it to be used in the middle of a computation pipeline without
// interrupting the flow.
//
// Type Parameters:
// - A: The type of the success value in the Result
//
// Parameters:
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelDebug, LevelError)
// - cb: A callback function that retrieves a *slog.Logger from the context
// - message: The log message to display
//
// Returns:
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
// The returned ReaderResult, when executed with a context, logs the Result
// and returns it unchanged
//
// Example:
//
// type User struct {
// ID int
// Name string
// }
//
// // Custom logger callback
// getLogger := func(ctx context.Context) *slog.Logger {
// return slog.Default()
// }
//
// // Create a logging function for debug level
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
//
// // Use in a pipeline
// ctx := context.Background()
// user := result.Of(User{ID: 123, Name: "Alice"})
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
// // logged still contains the User value
//
// Example with error:
//
// err := errors.New("user not found")
// userResult := result.Left[User](err)
// logged := logDebug(userResult)(ctx) // Logs: level=DEBUG msg="User data" error="user not found"
// // logged still contains the error
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) Kleisli[Result[A], A] {
return F.Pipe1(
F.Flow2(
result.ToSLogAttr[A](),
curriedLog(logLevel, cb, message),
),
reader.Chain(reader.Sequence(F.Flow2( // this flow is basically the `MapTo` function with side effects
reader.Of[struct{}, Result[A]],
reader.Map[context.Context, struct{}, Result[A]],
))),
)
}
// SLog creates a Kleisli arrow that logs a Result value at INFO level using the
// logger from the context. This is a convenience function that uses SLogWithCallback
// with default settings.
//
// The Result value is logged and then returned unchanged, making this function
// suitable for use in functional pipelines for debugging or monitoring purposes.
//
// This function logs both successful values and errors:
// - Success values are logged with the key "value"
// - Error values are logged with the key "error"
//
// Type Parameters:
// - A: The type of the success value in the Result
//
// Parameters:
// - message: The log message to display
//
// Returns:
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
// The returned ReaderResult, when executed with a context, logs the Result
// at INFO level and returns it unchanged
//
// Example - Logging a successful computation:
//
// ctx := context.Background()
//
// // Simple value logging
// res := result.Of(42)
// logged := SLog[int]("Processing number")(res)(ctx)
// // Logs: level=INFO msg="Processing number" value=42
// // logged == result.Of(42)
//
// Example - Logging in a pipeline:
//
// type User struct {
// ID int
// Name string
// }
//
// fetchUser := func(id int) result.Result[User] {
// return result.Of(User{ID: id, Name: "Alice"})
// }
//
// processUser := func(user User) result.Result[string] {
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
// }
//
// ctx := context.Background()
//
// // Log at each step
// userResult := fetchUser(123)
// logged1 := SLog[User]("Fetched user")(userResult)(ctx)
// // Logs: level=INFO msg="Fetched user" value={ID:123 Name:Alice}
//
// processed := result.Chain(processUser)(logged1)
// logged2 := SLog[string]("Processed user")(processed)(ctx)
// // Logs: level=INFO msg="Processed user" value="Processed: Alice"
//
// Example - Logging errors:
//
// err := errors.New("database connection failed")
// errResult := result.Left[User](err)
// logged := SLog[User]("Database operation")(errResult)(ctx)
// // Logs: level=INFO msg="Database operation" error="database connection failed"
// // logged still contains the error
//
// Example - Using with context logger:
//
// // Set up a custom logger in the context
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// ctx := logging.WithLogger(logger)(context.Background())
//
// res := result.Of("important data")
// logged := SLog[string]("Critical operation")(res)(ctx)
// // Uses the logger from context to log the message
//
// Note: The function uses logging.GetLoggerFromContext to retrieve the logger,
// which falls back to the global logger if no logger is found in the context.
//
//go:inline
func SLog[A any](message string) Kleisli[Result[A], A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}
//go:inline
func TapSLog[A any](message string) Operator[A, A] {
return reader.Chain(SLog[A](message))
}

View File

@@ -0,0 +1,302 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"bytes"
"context"
"errors"
"log/slog"
"testing"
"github.com/IBM/fp-go/v2/logging"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
func TestSLogLogsSuccessValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// Create a Result and log it
res1 := result.Of(42)
logged := SLog[int]("Result value")(res1)(ctx)
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "value=42")
}
// TestSLogLogsErrorValue tests that SLog logs error Result values
func TestSLogLogsErrorValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
testErr := errors.New("test error")
// Create an error Result and log it
res1 := result.Left[int](testErr)
logged := SLog[int]("Result value")(res1)(ctx)
assert.Equal(t, res1, logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "test error")
}
// TestSLogInPipeline tests SLog in a functional pipeline
func TestSLogInPipeline(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// SLog takes a Result[A] and returns ReaderResult[A]
// So we need to start with a Result, apply SLog, then execute with context
res1 := result.Of(10)
logged := SLog[int]("Initial value")(res1)(ctx)
assert.Equal(t, result.Of(10), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Initial value")
assert.Contains(t, logOutput, "value=10")
}
// TestSLogWithContextLogger tests SLog using logger from context
func TestSLogWithContextLogger(t *testing.T) {
var buf bytes.Buffer
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
res1 := result.Of("test value")
logged := SLog[string]("Context logger test")(res1)(ctx)
assert.Equal(t, result.Of("test value"), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Context logger test")
assert.Contains(t, logOutput, `value="test value"`)
}
// TestSLogDisabled tests that SLog respects logger level
func TestSLogDisabled(t *testing.T) {
var buf bytes.Buffer
// Create logger with level that disables info logs
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelError, // Only log errors
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
res1 := result.Of(42)
logged := SLog[int]("This should not be logged")(res1)(ctx)
assert.Equal(t, result.Of(42), logged)
// Should have no logs since level is ERROR
logOutput := buf.String()
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
}
// TestSLogWithStruct tests SLog with structured data
func TestSLogWithStruct(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
type User struct {
ID int
Name string
}
ctx := context.Background()
user := User{ID: 123, Name: "Alice"}
res1 := result.Of(user)
logged := SLog[User]("User data")(res1)(ctx)
assert.Equal(t, result.Of(user), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "User data")
assert.Contains(t, logOutput, "ID:123")
assert.Contains(t, logOutput, "Name:Alice")
}
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
func TestSLogWithCallbackCustomLevel(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
// Create a Result and log it with custom callback
res1 := result.Of(42)
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Debug result")
assert.Contains(t, logOutput, "value=42")
assert.Contains(t, logOutput, "level=DEBUG")
}
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
func TestSLogWithCallbackLogsError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelWarn,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
testErr := errors.New("warning error")
// Create an error Result and log it with custom callback
res1 := result.Left[int](testErr)
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)
assert.Equal(t, res1, logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Warning result")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "warning error")
assert.Contains(t, logOutput, "level=WARN")
}
// TestSLogChainedOperations tests SLog in chained operations
func TestSLogChainedOperations(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// First log step 1
res1 := result.Of(5)
logged1 := SLog[int]("Step 1")(res1)(ctx)
// Then log step 2 with doubled value
res2 := result.Map(N.Mul(2))(logged1)
logged2 := SLog[int]("Step 2")(res2)(ctx)
assert.Equal(t, result.Of(10), logged2)
logOutput := buf.String()
assert.Contains(t, logOutput, "Step 1")
assert.Contains(t, logOutput, "value=5")
assert.Contains(t, logOutput, "Step 2")
assert.Contains(t, logOutput, "value=10")
}
// TestSLogPreservesError tests that SLog preserves error through the pipeline
func TestSLogPreservesError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
testErr := errors.New("original error")
res1 := result.Left[int](testErr)
logged := SLog[int]("Logging error")(res1)(ctx)
// Apply map to verify error is preserved
res2 := result.Map(N.Mul(2))(logged)
assert.Equal(t, res1, res2)
logOutput := buf.String()
assert.Contains(t, logOutput, "Logging error")
assert.Contains(t, logOutput, "original error")
}
// TestSLogMultipleValues tests logging multiple different values
func TestSLogMultipleValues(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// Test with different types
intRes := SLog[int]("Integer")(result.Of(42))(ctx)
assert.Equal(t, result.Of(42), intRes)
strRes := SLog[string]("String")(result.Of("hello"))(ctx)
assert.Equal(t, result.Of("hello"), strRes)
boolRes := SLog[bool]("Boolean")(result.Of(true))(ctx)
assert.Equal(t, result.Of(true), boolRes)
logOutput := buf.String()
assert.Contains(t, logOutput, "Integer")
assert.Contains(t, logOutput, "value=42")
assert.Contains(t, logOutput, "String")
assert.Contains(t, logOutput, "value=hello")
assert.Contains(t, logOutput, "Boolean")
assert.Contains(t, logOutput, "value=true")
}

View File

@@ -18,9 +18,17 @@ package readerresult
import ( import (
"context" "context"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither" "github.com/IBM/fp-go/v2/readereither"
) )
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
return readereither.FromReader[error](r)
}
func FromEither[A any](e Either[A]) ReaderResult[A] { func FromEither[A any](e Either[A]) ReaderResult[A] {
return readereither.FromEither[context.Context](e) return readereither.FromEither[context.Context](e)
} }
@@ -42,11 +50,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
} }
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] { func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
return readereither.MonadChain(ma, f) return readereither.MonadChain(ma, F.Flow2(f, WithContext))
} }
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] { func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return readereither.Chain(f) return readereither.Chain(F.Flow2(f, WithContext))
} }
func Of[A any](a A) ReaderResult[A] { func Of[A any](a A) ReaderResult[A] {
@@ -66,7 +74,7 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
} }
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] { func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
return readereither.OrElse(onLeft) return readereither.OrElse(F.Flow2(onLeft, WithContext))
} }
func Ask() ReaderResult[context.Context] { func Ask() ReaderResult[context.Context] {
@@ -81,7 +89,7 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderResult[A]) Reader
return readereither.ChainEitherK[context.Context](f) return readereither.ChainEitherK[context.Context](f)
} }
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] { func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
return readereither.ChainOptionK[context.Context, A, B](onNone) return readereither.ChainOptionK[context.Context, A, B](onNone)
} }
@@ -97,3 +105,197 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] { func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
return readereither.Read[error, A](r) return readereither.Read[error, A](r)
} }
// MonadMapTo executes a ReaderResult computation, discards its success value, and returns a constant value.
// This is the monadic version that takes both the ReaderResult and the constant value as parameters.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadMapTo WILL
// execute the original ReaderResult to allow any side effects to occur, then discard the success result
// and return the constant value. If the original computation fails, the error is preserved.
//
// Type Parameters:
// - A: The success type of the first ReaderResult (will be discarded if successful)
// - B: The type of the constant value to return on success
//
// Parameters:
// - ma: The ReaderResult to execute (side effects will occur, success value discarded)
// - b: The constant value to return if ma succeeds
//
// Returns:
// - A ReaderResult that executes ma, preserves errors, but replaces success values with b
//
// Example:
//
// type Config struct { Counter int }
// increment := func(ctx context.Context) result.Result[int] {
// // Side effect: log the operation
// fmt.Println("incrementing")
// return result.Of(5)
// }
// r := readerresult.MonadMapTo(increment, "done")
// result := r(context.Background()) // Prints "incrementing", returns Right("done")
//
//go:inline
func MonadMapTo[A, B any](ma ReaderResult[A], b B) ReaderResult[B] {
return MonadMap(ma, reader.Of[A](b))
}
// MapTo creates an operator that executes a ReaderResult computation, discards its success value,
// and returns a constant value. This is the curried version where the constant value is provided first,
// returning a function that can be applied to any ReaderResult.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MapTo WILL
// execute the input ReaderResult to allow any side effects to occur, then discard the success result
// and return the constant value. If the computation fails, the error is preserved.
//
// Type Parameters:
// - A: The success type of the input ReaderResult (will be discarded if successful)
// - B: The type of the constant value to return on success
//
// Parameters:
// - b: The constant value to return on success
//
// Returns:
// - An Operator that executes a ReaderResult[A], preserves errors, but replaces success with b
//
// Example:
//
// logStep := func(ctx context.Context) result.Result[int] {
// fmt.Println("step executed")
// return result.Of(42)
// }
// toDone := readerresult.MapTo[int, string]("done")
// pipeline := toDone(logStep)
// result := pipeline(context.Background()) // Prints "step executed", returns Right("done")
//
// Example - In a functional pipeline:
//
// step1 := func(ctx context.Context) result.Result[int] {
// fmt.Println("processing")
// return result.Of(1)
// }
// pipeline := F.Pipe1(
// step1,
// readerresult.MapTo[int, string]("complete"),
// )
// output := pipeline(context.Background()) // Prints "processing", returns Right("complete")
//
//go:inline
func MapTo[A, B any](b B) Operator[A, B] {
return Map(reader.Of[A](b))
}
// MonadChainTo sequences two ReaderResult computations where the second ignores the first's success value.
// This is the monadic version that takes both ReaderResults as parameters.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadChainTo WILL
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
// execute the second ReaderResult with the same context. If the first computation fails, the error is
// returned immediately without executing the second computation.
//
// Type Parameters:
// - A: The success type of the first ReaderResult (will be discarded if successful)
// - B: The success type of the second ReaderResult
//
// Parameters:
// - ma: The first ReaderResult to execute (side effects will occur, success value discarded)
// - b: The second ReaderResult to execute if ma succeeds
//
// Returns:
// - A ReaderResult that executes ma, then b if ma succeeds, returning b's result
//
// Example:
//
// logStart := func(ctx context.Context) result.Result[int] {
// fmt.Println("starting")
// return result.Of(1)
// }
// logEnd := func(ctx context.Context) result.Result[string] {
// fmt.Println("ending")
// return result.Of("done")
// }
// r := readerresult.MonadChainTo(logStart, logEnd)
// result := r(context.Background()) // Prints "starting" then "ending", returns Right("done")
//
//go:inline
func MonadChainTo[A, B any](ma ReaderResult[A], b ReaderResult[B]) ReaderResult[B] {
return MonadChain(ma, reader.Of[A](b))
}
// ChainTo creates an operator that sequences two ReaderResult computations where the second ignores
// the first's success value. This is the curried version where the second ReaderResult is provided first,
// returning a function that can be applied to any first ReaderResult.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, ChainTo WILL
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
// execute the second ReaderResult with the same context. If the first computation fails, the error is
// returned immediately without executing the second computation.
//
// Type Parameters:
// - A: The success type of the first ReaderResult (will be discarded if successful)
// - B: The success type of the second ReaderResult
//
// Parameters:
// - b: The second ReaderResult to execute after the first succeeds
//
// Returns:
// - An Operator that executes the first ReaderResult, then b if successful
//
// Example:
//
// logEnd := func(ctx context.Context) result.Result[string] {
// fmt.Println("ending")
// return result.Of("done")
// }
// thenLogEnd := readerresult.ChainTo[int, string](logEnd)
//
// logStart := func(ctx context.Context) result.Result[int] {
// fmt.Println("starting")
// return result.Of(1)
// }
// pipeline := thenLogEnd(logStart)
// result := pipeline(context.Background()) // Prints "starting" then "ending", returns Right("done")
//
// Example - In a functional pipeline:
//
// step1 := func(ctx context.Context) result.Result[int] {
// fmt.Println("step 1")
// return result.Of(1)
// }
// step2 := func(ctx context.Context) result.Result[string] {
// fmt.Println("step 2")
// return result.Of("complete")
// }
// pipeline := F.Pipe1(
// step1,
// readerresult.ChainTo[int, string](step2),
// )
// output := pipeline(context.Background()) // Prints "step 1" then "step 2", returns Right("complete")
//
//go:inline
func ChainTo[A, B any](b ReaderResult[B]) Operator[A, B] {
return Chain(reader.Of[A](b))
}
//go:inline
func MonadChainFirst[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[A] {
return chain.MonadChainFirst(
MonadChain,
MonadMap,
ma,
F.Flow2(f, WithContext),
)
}
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(
Chain,
Map,
F.Flow2(f, WithContext),
)
}

View File

@@ -0,0 +1,315 @@
// 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 readerresult
import (
"context"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestMapTo(t *testing.T) {
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
executed := false
originalReader := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
// Apply MapTo operator
toDone := MapTo[int]("done")
resultReader := toDone(originalReader)
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the constant value is returned
assert.Equal(t, E.Of[error]("done"), result)
// Verify the original reader WAS executed (side effect occurred)
assert.True(t, executed, "original reader should be executed to allow side effects")
})
t.Run("executes reader in functional pipeline", func(t *testing.T) {
executed := false
step1 := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](100)
}
pipeline := F.Pipe1(
step1,
MapTo[int]("complete"),
)
result := pipeline(context.Background())
assert.Equal(t, E.Of[error]("complete"), result)
assert.True(t, executed, "original reader should be executed in pipeline")
})
t.Run("executes reader with side effects", func(t *testing.T) {
sideEffectOccurred := false
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
sideEffectOccurred = true
return E.Of[error](42)
}
resultReader := MapTo[int](true)(readerWithSideEffect)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error](true), result)
assert.True(t, sideEffectOccurred, "side effect should occur")
})
t.Run("preserves errors from original reader", func(t *testing.T) {
executed := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Left[int](testErr)
}
resultReader := MapTo[int]("done")(failingReader)
result := resultReader(context.Background())
assert.Equal(t, E.Left[string](testErr), result)
assert.True(t, executed, "failing reader should still be executed")
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
executed := false
originalReader := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
// Apply MonadMapTo
resultReader := MonadMapTo(originalReader, "done")
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the constant value is returned
assert.Equal(t, E.Of[error]("done"), result)
// Verify the original reader WAS executed (side effect occurred)
assert.True(t, executed, "original reader should be executed to allow side effects")
})
t.Run("executes complex computation with side effects", func(t *testing.T) {
computationExecuted := false
complexReader := func(ctx context.Context) E.Either[error, string] {
computationExecuted = true
return E.Of[error]("complex result")
}
resultReader := MonadMapTo(complexReader, 42)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, computationExecuted, "complex computation should be executed")
})
t.Run("preserves errors from original reader", func(t *testing.T) {
executed := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, []string] {
executed = true
return E.Left[[]string](testErr)
}
resultReader := MonadMapTo(failingReader, 99)
result := resultReader(context.Background())
assert.Equal(t, E.Left[int](testErr), result)
assert.True(t, executed, "failing reader should still be executed")
})
}
func TestChainTo(t *testing.T) {
t.Run("executes first reader then second reader on success", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
firstReader := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](42)
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("result")
}
// Apply ChainTo operator
thenSecond := ChainTo[int](secondReader)
resultReader := thenSecond(firstReader)
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the second reader's result is returned
assert.Equal(t, E.Of[error]("result"), result)
// Verify both readers were executed
assert.True(t, firstExecuted, "first reader should be executed")
assert.True(t, secondExecuted, "second reader should be executed")
})
t.Run("executes both readers in functional pipeline", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
step1 := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](100)
}
step2 := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("complete")
}
pipeline := F.Pipe1(
step1,
ChainTo[int](step2),
)
result := pipeline(context.Background())
assert.Equal(t, E.Of[error]("complete"), result)
assert.True(t, firstExecuted, "first reader should be executed in pipeline")
assert.True(t, secondExecuted, "second reader should be executed in pipeline")
})
t.Run("executes first reader with side effects", func(t *testing.T) {
sideEffectOccurred := false
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
sideEffectOccurred = true
return E.Of[error](42)
}
secondReader := func(ctx context.Context) E.Either[error, bool] {
return E.Of[error](true)
}
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error](true), result)
assert.True(t, sideEffectOccurred, "side effect should occur in first reader")
})
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Left[int](testErr)
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("result")
}
resultReader := ChainTo[int](secondReader)(failingReader)
result := resultReader(context.Background())
assert.Equal(t, E.Left[string](testErr), result)
assert.True(t, firstExecuted, "first reader should be executed")
assert.False(t, secondExecuted, "second reader should not be executed on error")
})
}
func TestMonadChainTo(t *testing.T) {
t.Run("executes first reader then second reader on success", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
firstReader := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](42)
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("result")
}
// Apply MonadChainTo
resultReader := MonadChainTo(firstReader, secondReader)
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the second reader's result is returned
assert.Equal(t, E.Of[error]("result"), result)
// Verify both readers were executed
assert.True(t, firstExecuted, "first reader should be executed")
assert.True(t, secondExecuted, "second reader should be executed")
})
t.Run("executes complex first computation with side effects", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
complexFirstReader := func(ctx context.Context) E.Either[error, []int] {
firstExecuted = true
return E.Of[error]([]int{1, 2, 3})
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("done")
}
resultReader := MonadChainTo(complexFirstReader, secondReader)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error]("done"), result)
assert.True(t, firstExecuted, "complex first computation should be executed")
assert.True(t, secondExecuted, "second reader should be executed")
})
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, map[string]int] {
firstExecuted = true
return E.Left[map[string]int](testErr)
}
secondReader := func(ctx context.Context) E.Either[error, float64] {
secondExecuted = true
return E.Of[error](3.14)
}
resultReader := MonadChainTo(failingReader, secondReader)
result := resultReader(context.Background())
assert.Equal(t, E.Left[float64](testErr), result)
assert.True(t, firstExecuted, "first reader should be executed")
assert.False(t, secondExecuted, "second reader should not be executed on error")
})
}

View File

@@ -0,0 +1,106 @@
// 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 readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
package readerresult
import (
"context"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/result"
)
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
//
// TailRec takes a Kleisli function that returns Either[A, B] and converts it into a stack-safe,
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Right value.
//
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns a
// Left result containing the context's cause error, preventing unnecessary computation.
//
// Type Parameters:
// - A: The input type for the recursive step
// - B: The final result type
//
// Parameters:
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Either[A, B].
// When the result is Left[B](a), recursion continues with the new value 'a'.
// When the result is Right[A](b), recursion terminates with the final value 'b'.
//
// Returns:
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
//
// Behavior:
// - On each iteration, checks if the context has been canceled (short circuit)
// - If canceled, returns result.Left[B](context.Cause(ctx))
// - If the step returns Left[B](error), propagates the error
// - If the step returns Right[A](Left[B](a)), continues recursion with new value 'a'
// - If the step returns Right[A](Right[A](b)), terminates with success value 'b'
//
// Example - Factorial computation with context:
//
// type State struct {
// n int
// acc int
// }
//
// factorialStep := func(state State) ReaderResult[either.Either[State, int]] {
// return func(ctx context.Context) result.Result[either.Either[State, int]] {
// if state.n <= 0 {
// return result.Of(either.Right[State](state.acc))
// }
// return result.Of(either.Left[int](State{state.n - 1, state.acc * state.n}))
// }
// }
//
// factorial := TailRec(factorialStep)
// result := factorial(State{5, 1})(ctx) // Returns result.Of(120)
//
// Example - Context cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
//
// computation := TailRec(someStep)
// result := computation(initialValue)(ctx)
// // Returns result.Left[B](context.Cause(ctx)) without executing any steps
//
//go:inline
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
return func(a A) ReaderResult[B] {
initialReader := f(a)
return func(ctx context.Context) Result[B] {
rdr := initialReader
for {
// short circuit
if ctx.Err() != nil {
return result.Left[B](context.Cause(ctx))
}
current := rdr(ctx)
rec, e := either.Unwrap(current)
if either.IsLeft(current) {
return result.Left[B](e)
}
b, a := either.Unwrap(rec)
if either.IsRight(rec) {
return result.Of(b)
}
rdr = f(a)
}
}
}
}

View File

@@ -0,0 +1,498 @@
// 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 readerresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests factorial computation with context
func TestTailRecFactorial(t *testing.T) {
type State struct {
n int
acc int
}
factorialStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.acc))
}
return R.Of(E.Left[int](State{state.n - 1, state.acc * state.n}))
}
}
factorial := TailRec(factorialStep)
result := factorial(State{5, 1})(context.Background())
assert.Equal(t, R.Of(120), result)
}
// TestTailRecFibonacci tests Fibonacci computation
func TestTailRecFibonacci(t *testing.T) {
type State struct {
n int
prev int
curr int
}
fibStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.curr))
}
return R.Of(E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
}
}
fib := TailRec(fibStep)
result := fib(State{10, 0, 1})(context.Background())
assert.Equal(t, R.Of(89), result) // 10th Fibonacci number
}
// TestTailRecCountdown tests countdown computation
func TestTailRecCountdown(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(context.Background())
assert.Equal(t, R.Of(0), result)
}
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
func TestTailRecImmediateTermination(t *testing.T) {
immediateStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
return R.Of(E.Right[int](n * 2))
}
}
immediate := TailRec(immediateStep)
result := immediate(42)(context.Background())
assert.Equal(t, R.Of(84), result)
}
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
func TestTailRecStackSafety(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10000)(context.Background())
assert.Equal(t, R.Of(0), result)
}
// TestTailRecSumList tests summing a list
func TestTailRecSumList(t *testing.T) {
type State struct {
list []int
sum int
}
sumStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if A.IsEmpty(state.list) {
return R.Of(E.Right[State](state.sum))
}
return R.Of(E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
}
}
sumList := TailRec(sumStep)
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
assert.Equal(t, R.Of(15), result)
}
// TestTailRecCollatzConjecture tests the Collatz conjecture
func TestTailRecCollatzConjecture(t *testing.T) {
collatzStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 1 {
return R.Of(E.Right[int](n))
}
if n%2 == 0 {
return R.Of(E.Left[int](n / 2))
}
return R.Of(E.Left[int](3*n + 1))
}
}
collatz := TailRec(collatzStep)
result := collatz(10)(context.Background())
assert.Equal(t, R.Of(1), result)
}
// TestTailRecGCD tests greatest common divisor
func TestTailRecGCD(t *testing.T) {
type State struct {
a int
b int
}
gcdStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.b == 0 {
return R.Of(E.Right[State](state.a))
}
return R.Of(E.Left[int](State{state.b, state.a % state.b}))
}
}
gcd := TailRec(gcdStep)
result := gcd(State{48, 18})(context.Background())
assert.Equal(t, R.Of(6), result)
}
// TestTailRecErrorPropagation tests that errors are properly propagated
func TestTailRecErrorPropagation(t *testing.T) {
expectedErr := errors.New("computation error")
errorStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n == 5 {
return R.Left[E.Either[int, int]](expectedErr)
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
computation := TailRec(errorStep)
result := computation(10)(context.Background())
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, expectedErr, err)
}
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
func TestTailRecContextCancellationImmediate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately before execution
stepExecuted := false
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
stepExecuted = true
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
// Should short circuit without executing any steps
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
// Cancel after 3 iterations
if executionCount == 3 {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(100)(ctx)
// Should stop after cancellation
assert.True(t, R.IsLeft(result))
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextWithTimeout tests behavior with timeout context
func TestTailRecContextWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
executionCount := 0
slowStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
// Simulate slow computation
time.Sleep(20 * time.Millisecond)
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
computation := TailRec(slowStep)
result := computation(100)(ctx)
// Should timeout and return error
assert.True(t, R.IsLeft(result))
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
_, err := R.Unwrap(result)
assert.Equal(t, context.DeadlineExceeded, err)
}
// TestTailRecContextWithCause tests that context.Cause is properly returned
func TestTailRecContextWithCause(t *testing.T) {
customErr := errors.New("custom cancellation reason")
ctx, cancel := context.WithCancelCause(context.Background())
cancel(customErr)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, customErr, err)
}
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
maxExecutions := 5
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
if executionCount == maxExecutions {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(1000)(ctx)
// Should detect cancellation on next iteration check
assert.True(t, R.IsLeft(result))
// Should stop within 1-2 iterations after cancellation
assert.LessOrEqual(t, executionCount, maxExecutions+2)
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
func TestTailRecContextNotCanceled(t *testing.T) {
ctx := context.Background()
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
assert.Equal(t, R.Of(0), result)
}
// TestTailRecPowerOfTwo tests computing power of 2
func TestTailRecPowerOfTwo(t *testing.T) {
type State struct {
exponent int
result int
target int
}
powerStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.exponent >= state.target {
return R.Of(E.Right[State](state.result))
}
return R.Of(E.Left[int](State{state.exponent + 1, state.result * 2, state.target}))
}
}
power := TailRec(powerStep)
result := power(State{0, 1, 10})(context.Background())
assert.Equal(t, R.Of(1024), result) // 2^10
}
// TestTailRecFindInRange tests finding a value in a range
func TestTailRecFindInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
}
}
find := TailRec(findStep)
result := find(State{0, 100, 42})(context.Background())
assert.Equal(t, R.Of(42), result)
}
// TestTailRecFindNotInRange tests finding a value not in range
func TestTailRecFindNotInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
}
}
find := TailRec(findStep)
result := find(State{0, 100, 200})(context.Background())
assert.Equal(t, R.Of(-1), result)
}
// TestTailRecWithContextValue tests that context values are accessible
func TestTailRecWithContextValue(t *testing.T) {
type contextKey string
const multiplierKey contextKey = "multiplier"
ctx := context.WithValue(context.Background(), multiplierKey, 3)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
multiplier := ctx.Value(multiplierKey).(int)
return R.Of(E.Right[int](n * multiplier))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(5)(ctx)
assert.Equal(t, R.Of(0), result) // 0 * 3 = 0
}
// TestTailRecComplexState tests with complex state structure
func TestTailRecComplexState(t *testing.T) {
type ComplexState struct {
counter int
sum int
product int
completed bool
}
complexStep := func(state ComplexState) ReaderResult[E.Either[ComplexState, string]] {
return func(ctx context.Context) Result[E.Either[ComplexState, string]] {
if state.counter <= 0 || state.completed {
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
return R.Of(E.Right[ComplexState](result))
}
newState := ComplexState{
counter: state.counter - 1,
sum: state.sum + state.counter,
product: state.product * state.counter,
completed: state.counter == 1,
}
return R.Of(E.Left[string](newState))
}
}
computation := TailRec(complexStep)
result := computation(ComplexState{5, 0, 1, false})(context.Background())
assert.Equal(t, R.Of("sum=15, product=120"), result)
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 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 readerresult
import (
"context"
"time"
RD "github.com/IBM/fp-go/v2/reader"
R "github.com/IBM/fp-go/v2/retry"
RG "github.com/IBM/fp-go/v2/retry/generic"
)
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(Result[A]) bool,
) ReaderResult[A] {
// delayWithCancel implements a context-aware delay mechanism for retry operations.
// It creates a timeout context that will be cancelled when either:
// 1. The delay duration expires (normal case), or
// 2. The parent context is cancelled (early termination)
//
// The function waits on timeoutCtx.Done(), which will be signaled in either case:
// - If the delay expires, timeoutCtx is cancelled by the timeout
// - If the parent ctx is cancelled, timeoutCtx inherits the cancellation
//
// After the wait completes, we dispatch to the next action by calling ri(ctx)().
// This works correctly because the action is wrapped in WithContextK, which handles
// context cancellation by checking ctx.Err() and returning an appropriate error
// (context.Canceled or context.DeadlineExceeded) when the context is cancelled.
//
// This design ensures that:
// - Retry delays respect context cancellation and terminate immediately
// - The cancellation error propagates correctly through the retry chain
// - No unnecessary delays occur when the context is already cancelled
delayWithCancel := func(delay time.Duration) RD.Operator[context.Context, R.RetryStatus, R.RetryStatus] {
return func(ri Reader[context.Context, R.RetryStatus]) Reader[context.Context, R.RetryStatus] {
return func(ctx context.Context) R.RetryStatus {
// Create a timeout context that will be cancelled when either:
// - The delay duration expires, or
// - The parent context is cancelled
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, delay)
defer cancelTimeout()
// Wait for either the timeout or parent context cancellation
<-timeoutCtx.Done()
// Dispatch to the next action with the original context.
// WithContextK will handle context cancellation correctly.
return ri(ctx)
}
}
}
// get an implementation for the types
return RG.Retrying(
RD.Chain[context.Context, Result[A], Result[A]],
RD.Chain[context.Context, R.RetryStatus, Result[A]],
RD.Of[context.Context, Result[A]],
RD.Of[context.Context, R.RetryStatus],
delayWithCancel,
policy,
WithContextK(action),
check,
)
}

View File

@@ -13,13 +13,40 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error // Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error.
//
// # Pure vs Effectful Functions
//
// This package distinguishes between pure (side-effect free) and effectful (side-effectful) functions:
//
// EFFECTFUL FUNCTIONS (depend on context.Context):
// - ReaderResult[A]: func(context.Context) (A, error) - Effectful computation that needs context
// - These functions are effectful because context.Context is effectful (can be cancelled, has deadlines, carries values)
// - Use for: operations that need cancellation, timeouts, context values, or any context-dependent behavior
// - Examples: database queries, HTTP requests, operations that respect cancellation
//
// PURE FUNCTIONS (side-effect free):
// - func(State) (Value, error) - Pure computation that only depends on state, not context
// - func(State) Value - Pure transformation without errors
// - These functions are pure because they only read from their input state and don't depend on external context
// - Use for: parsing, validation, calculations, data transformations that don't need context
// - Examples: JSON parsing, input validation, mathematical computations
//
// The package provides different bind operations for each:
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
// - BindResultK: For pure functions with errors (State -> (Value, error))
// - Let: For pure functions without errors (State -> Value)
// - BindReaderK: For context-dependent pure functions (State -> Reader[Context, Value])
// - BindEitherK: For pure Result/Either values (State -> Result[Value])
package readerresult package readerresult
import ( import (
"context" "context"
"github.com/IBM/fp-go/v2/either" "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither" "github.com/IBM/fp-go/v2/readereither"
@@ -34,6 +61,9 @@ type (
// ReaderResult is a specialization of the Reader monad for the typical golang scenario // ReaderResult is a specialization of the Reader monad for the typical golang scenario
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A] ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]] Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B] Operator[A, B any] = Kleisli[ReaderResult[A], B]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
) )

File diff suppressed because it is too large Load Diff

View File

@@ -103,11 +103,11 @@ func (t *token[T]) Unerase(val any) Result[T] {
func (t *token[T]) ProviderFactory() Option[DIE.ProviderFactory] { func (t *token[T]) ProviderFactory() Option[DIE.ProviderFactory] {
return t.base.providerFactory return t.base.providerFactory
} }
func makeTokenBase(name string, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase { func makeTokenBase(name, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
return &tokenBase{name, id, typ, providerFactory} return &tokenBase{name, id, typ, providerFactory}
} }
func makeToken[T any](name string, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] { func makeToken[T any](name, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase} return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
} }

View File

@@ -75,7 +75,7 @@ func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
// Example: // Example:
// //
// validate := func(i int, s string) either.Either[error, string] { // validate := func(i int, s string) either.Either[error, string] {
// if len(s) > 0 { // if S.IsNonEmpty(s) {
// return either.Right[error](fmt.Sprintf("%d:%s", i, s)) // return either.Right[error](fmt.Sprintf("%d:%s", i, s))
// } // }
// return either.Left[string](fmt.Errorf("empty at index %d", i)) // return either.Left[string](fmt.Errorf("empty at index %d", i))
@@ -105,7 +105,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, E, A, B any](f func(int, A) Eithe
// Example: // Example:
// //
// validate := func(i int, s string) either.Either[error, string] { // validate := func(i int, s string) either.Either[error, string] {
// if len(s) > 0 { // if S.IsNonEmpty(s) {
// return either.Right[error](fmt.Sprintf("%d:%s", i, s)) // return either.Right[error](fmt.Sprintf("%d:%s", i, s))
// } // }
// return either.Left[string](fmt.Errorf("empty at index %d", i)) // return either.Left[string](fmt.Errorf("empty at index %d", i))

View File

@@ -34,7 +34,7 @@ func Curry0[R any](f func() (R, error)) func() Either[error, R] {
// //
// Example: // Example:
// //
// parse := func(s string) (int, error) { return strconv.Atoi(s) } // parse := strconv.Atoi
// curried := either.Curry1(parse) // curried := either.Curry1(parse)
// result := curried("42") // Right(42) // result := curried("42") // Right(42)
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] { func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] {

View File

@@ -19,6 +19,21 @@
// - Left represents an error or failure case (type E) // - Left represents an error or failure case (type E)
// - Right represents a success case (type A) // - Right represents a success case (type A)
// //
// # Fantasy Land Specification
//
// This implementation corresponds to the Fantasy Land Either type:
// https://github.com/fantasyland/fantasy-land#either
//
// Implemented Fantasy Land algebras:
// - Functor: https://github.com/fantasyland/fantasy-land#functor
// - Bifunctor: https://github.com/fantasyland/fantasy-land#bifunctor
// - Apply: https://github.com/fantasyland/fantasy-land#apply
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
// - Chain: https://github.com/fantasyland/fantasy-land#chain
// - Monad: https://github.com/fantasyland/fantasy-land#monad
// - Alt: https://github.com/fantasyland/fantasy-land#alt
// - Foldable: https://github.com/fantasyland/fantasy-land#foldable
//
// # Core Concepts // # Core Concepts
// //
// The Either type is a discriminated union that can hold either a Left value (typically an error) // The Either type is a discriminated union that can hold either a Left value (typically an error)

View File

@@ -22,8 +22,9 @@ import (
"testing" "testing"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid" N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -305,7 +306,7 @@ func TestTraverseArray(t *testing.T) {
// Test TraverseArrayWithIndex // Test TraverseArrayWithIndex
func TestTraverseArrayWithIndex(t *testing.T) { func TestTraverseArrayWithIndex(t *testing.T) {
validate := func(i int, s string) Either[error, string] { validate := func(i int, s string) Either[error, string] {
if len(s) > 0 { if S.IsNonEmpty(s) {
return Right[error](fmt.Sprintf("%d:%s", i, s)) return Right[error](fmt.Sprintf("%d:%s", i, s))
} }
return Left[string](fmt.Errorf("empty at index %d", i)) return Left[string](fmt.Errorf("empty at index %d", i))
@@ -334,7 +335,7 @@ func TestTraverseRecord(t *testing.T) {
// Test TraverseRecordWithIndex // Test TraverseRecordWithIndex
func TestTraverseRecordWithIndex(t *testing.T) { func TestTraverseRecordWithIndex(t *testing.T) {
validate := func(k string, v string) Either[error, string] { validate := func(k string, v string) Either[error, string] {
if len(v) > 0 { if S.IsNonEmpty(v) {
return Right[error](k + ":" + v) return Right[error](k + ":" + v)
} }
return Left[string](fmt.Errorf("empty value for key %s", k)) return Left[string](fmt.Errorf("empty value for key %s", k))
@@ -373,7 +374,7 @@ func TestCurry0(t *testing.T) {
} }
func TestCurry1(t *testing.T) { func TestCurry1(t *testing.T) {
parse := func(s string) (int, error) { return strconv.Atoi(s) } parse := strconv.Atoi
curried := Curry1(parse) curried := Curry1(parse)
result := curried("42") result := curried("42")
assert.Equal(t, Right[error](42), result) assert.Equal(t, Right[error](42), result)
@@ -645,7 +646,7 @@ func TestAltSemigroup(t *testing.T) {
// Test AlternativeMonoid // Test AlternativeMonoid
func TestAlternativeMonoid(t *testing.T) { func TestAlternativeMonoid(t *testing.T) {
intAdd := M.MakeMonoid(func(a, b int) int { return a + b }, 0) intAdd := N.MonoidSum[int]()
m := AlternativeMonoid[error](intAdd) m := AlternativeMonoid[error](intAdd)
result := m.Concat(Right[error](1), Right[error](2)) result := m.Concat(Right[error](1), Right[error](2))

View File

@@ -22,7 +22,6 @@ import (
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils" "github.com/IBM/fp-go/v2/internal/utils"
IO "github.com/IBM/fp-go/v2/io"
O "github.com/IBM/fp-go/v2/option" O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string" S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -120,10 +119,3 @@ func TestStringer(t *testing.T) {
var s fmt.Stringer = &e var s fmt.Stringer = &e
assert.Equal(t, exp, s.String()) assert.Equal(t, exp, s.String())
} }
func TestFromIO(t *testing.T) {
f := IO.Of("abc")
e := FromIO[error](f)
assert.Equal(t, Right[error]("abc"), e)
}

View File

@@ -17,11 +17,19 @@ package either
import ( import (
"log" "log"
"log/slog"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/logging" L "github.com/IBM/fp-go/v2/logging"
) )
var (
// slogError creates a slog.Attr with key "error" for logging error values
slogError = F.Bind1st(slog.Any, "error")
// slogValue creates a slog.Attr with key "value" for logging success values
slogValue = F.Bind1st(slog.Any, "value")
)
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] { func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
return Fold( return Fold(
func(e E) Either[E, A] { func(e E) Either[E, A] {
@@ -62,3 +70,91 @@ func Logger[E, A any](loggers ...*log.Logger) func(string) Operator[E, A, A] {
} }
} }
} }
// ToSLogAttr converts an Either value to a structured logging attribute (slog.Attr).
//
// This function creates a converter that transforms Either values into slog.Attr for use
// with Go's structured logging (log/slog). It maps:
// - Left values to an "error" attribute
// - Right values to a "value" attribute
//
// This is particularly useful when integrating Either-based error handling with structured
// logging systems, allowing you to log both successful values and errors in a consistent,
// structured format.
//
// Type Parameters:
// - E: The Left (error) type of the Either
// - A: The Right (success) type of the Either
//
// Returns:
// - A function that converts Either[E, A] to slog.Attr
//
// Example with Left (error):
//
// converter := either.ToSLogAttr[error, int]()
// leftValue := either.Left[int](errors.New("connection failed"))
// attr := converter(leftValue)
// // attr is: slog.Any("error", errors.New("connection failed"))
//
// logger.LogAttrs(ctx, slog.LevelError, "Operation failed", attr)
// // Logs: {"level":"error","msg":"Operation failed","error":"connection failed"}
//
// Example with Right (success):
//
// converter := either.ToSLogAttr[error, User]()
// rightValue := either.Right[error](User{ID: 123, Name: "Alice"})
// attr := converter(rightValue)
// // attr is: slog.Any("value", User{ID: 123, Name: "Alice"})
//
// logger.LogAttrs(ctx, slog.LevelInfo, "User fetched", attr)
// // Logs: {"level":"info","msg":"User fetched","value":{"ID":123,"Name":"Alice"}}
//
// Example in a pipeline with structured logging:
//
// toAttr := either.ToSLogAttr[error, Data]()
//
// result := F.Pipe2(
// fetchData(id),
// either.Map(processData),
// either.Map(validateData),
// )
//
// attr := toAttr(result)
// logger.LogAttrs(ctx, slog.LevelInfo, "Data processing complete", attr)
// // Logs success: {"level":"info","msg":"Data processing complete","value":{...}}
// // Or error: {"level":"info","msg":"Data processing complete","error":"validation failed"}
//
// Example with custom log levels based on Either:
//
// toAttr := either.ToSLogAttr[error, Response]()
// result := callAPI(endpoint)
//
// level := either.Fold(
// func(error) slog.Level { return slog.LevelError },
// func(Response) slog.Level { return slog.LevelInfo },
// )(result)
//
// logger.LogAttrs(ctx, level, "API call completed", toAttr(result))
//
// Use Cases:
// - Structured logging: Convert Either results to structured log attributes
// - Error tracking: Log errors with consistent "error" key in structured logs
// - Success monitoring: Log successful values with consistent "value" key
// - Observability: Integrate Either-based error handling with logging systems
// - Debugging: Inspect Either values in logs with proper structure
// - Metrics: Extract Either values for metrics collection in logging pipelines
//
// Note: The returned slog.Attr uses "error" for Left values and "value" for Right values.
// These keys are consistent with common structured logging conventions.
func ToSLogAttr[E, A any]() func(Either[E, A]) slog.Attr {
return Fold(
F.Flow2(
F.ToAny[E],
slogError,
),
F.Flow2(
F.ToAny[A],
slogValue,
),
)
}

View File

@@ -16,9 +16,12 @@
package either package either
import ( import (
"errors"
"log/slog"
"testing" "testing"
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -35,3 +38,139 @@ func TestLogger(t *testing.T) {
assert.Equal(t, r, res) assert.Equal(t, r, res)
} }
func TestToSLogAttr_Left(t *testing.T) {
// Test with Left (error) value
converter := ToSLogAttr[error, int]()
testErr := errors.New("test error")
leftValue := Left[int](testErr)
attr := converter(leftValue)
// Verify the attribute has the correct key
assert.Equal(t, "error", attr.Key)
// Verify the attribute value is the error
assert.Equal(t, testErr, attr.Value.Any())
}
func TestToSLogAttr_Right(t *testing.T) {
// Test with Right (success) value
converter := ToSLogAttr[error, string]()
rightValue := Right[error]("success value")
attr := converter(rightValue)
// Verify the attribute has the correct key
assert.Equal(t, "value", attr.Key)
// Verify the attribute value is the success value
assert.Equal(t, "success value", attr.Value.Any())
}
func TestToSLogAttr_LeftWithCustomType(t *testing.T) {
// Test with custom error type
type CustomError struct {
Code int
Message string
}
converter := ToSLogAttr[CustomError, string]()
customErr := CustomError{Code: 404, Message: "not found"}
leftValue := Left[string](customErr)
attr := converter(leftValue)
assert.Equal(t, "error", attr.Key)
assert.Equal(t, customErr, attr.Value.Any())
}
func TestToSLogAttr_RightWithCustomType(t *testing.T) {
// Test with custom success type
type User struct {
ID int
Name string
}
converter := ToSLogAttr[error, User]()
user := User{ID: 123, Name: "Alice"}
rightValue := Right[error](user)
attr := converter(rightValue)
assert.Equal(t, "value", attr.Key)
assert.Equal(t, user, attr.Value.Any())
}
func TestToSLogAttr_InPipeline(t *testing.T) {
// Test ToSLogAttr in a functional pipeline
converter := ToSLogAttr[error, int]()
// Test with successful pipeline
successResult := F.Pipe2(
Right[error](10),
Map[error](N.Mul(2)),
converter,
)
assert.Equal(t, "value", successResult.Key)
// slog.Any converts int to int64
assert.Equal(t, int64(20), successResult.Value.Any())
// Test with failed pipeline
testErr := errors.New("computation failed")
failureResult := F.Pipe2(
Left[int](testErr),
Map[error](N.Mul(2)),
converter,
)
assert.Equal(t, "error", failureResult.Key)
assert.Equal(t, testErr, failureResult.Value.Any())
}
func TestToSLogAttr_WithNilError(t *testing.T) {
// Test with nil error (edge case)
converter := ToSLogAttr[error, string]()
var nilErr error = nil
leftValue := Left[string](nilErr)
attr := converter(leftValue)
assert.Equal(t, "error", attr.Key)
assert.Nil(t, attr.Value.Any())
}
func TestToSLogAttr_WithZeroValue(t *testing.T) {
// Test with zero value of success type
converter := ToSLogAttr[error, int]()
rightValue := Right[error](0)
attr := converter(rightValue)
assert.Equal(t, "value", attr.Key)
// slog.Any converts int to int64
assert.Equal(t, int64(0), attr.Value.Any())
}
func TestToSLogAttr_WithEmptyString(t *testing.T) {
// Test with empty string as success value
converter := ToSLogAttr[error, string]()
rightValue := Right[error]("")
attr := converter(rightValue)
assert.Equal(t, "value", attr.Key)
assert.Equal(t, "", attr.Value.Any())
}
func TestToSLogAttr_AttributeKind(t *testing.T) {
// Verify that the returned attribute has the correct Kind
converter := ToSLogAttr[error, string]()
leftAttr := converter(Left[string](errors.New("error")))
// Errors are stored as KindAny (which has value 0)
assert.Equal(t, slog.KindAny, leftAttr.Value.Kind())
rightAttr := converter(Right[error]("value"))
// Strings have KindString
assert.Equal(t, slog.KindString, rightAttr.Value.Kind())
}

34
v2/either/rec.go Normal file
View File

@@ -0,0 +1,34 @@
// 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 either
//go:inline
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
return func(a A) Either[E, B] {
current := f(a)
for {
rec, e := Unwrap(current)
if IsLeft(current) {
return Left[B](e)
}
b, a := Unwrap(rec)
if IsRight(rec) {
return Right[E](b)
}
current = f(a)
}
}
}

View File

@@ -41,7 +41,7 @@ import (
// increment := N.Add(1) // increment := N.Add(1)
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment // result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
// // result(5) = double(increment(5)) = double(6) = 12 // // result(5) = double(increment(5)) = double(6) = 12
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] { func MonadAp[A any](fab, fa Endomorphism[A]) Endomorphism[A] {
return MonadCompose(fab, fa) return MonadCompose(fab, fa)
} }
@@ -225,7 +225,7 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
// // Compare with MonadCompose which executes RIGHT-TO-LEFT: // // Compare with MonadCompose which executes RIGHT-TO-LEFT:
// composed := endomorphism.MonadCompose(increment, double) // composed := endomorphism.MonadCompose(increment, double)
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order) // result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] { func MonadChain[A any](ma, f Endomorphism[A]) Endomorphism[A] {
return function.Flow2(ma, f) return function.Flow2(ma, f)
} }
@@ -247,7 +247,7 @@ func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
// log := func(x int) int { fmt.Println(x); return x } // log := func(x int) int { fmt.Println(x); return x }
// chained := endomorphism.MonadChainFirst(double, log) // chained := endomorphism.MonadChainFirst(double, log)
// result := chained(5) // Prints 10, returns 10 // result := chained(5) // Prints 10, returns 10
func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] { func MonadChainFirst[A any](ma, f Endomorphism[A]) Endomorphism[A] {
return func(a A) A { return func(a A) A {
result := ma(a) result := ma(a)
f(result) // Apply f for its effect f(result) // Apply f for its effect

View File

@@ -72,9 +72,7 @@ func TestFromStrictEquals(t *testing.T) {
func TestFromEquals(t *testing.T) { func TestFromEquals(t *testing.T) {
t.Run("case-insensitive string equality", func(t *testing.T) { t.Run("case-insensitive string equality", func(t *testing.T) {
caseInsensitiveEq := FromEquals(func(a, b string) bool { caseInsensitiveEq := FromEquals(strings.EqualFold)
return strings.EqualFold(a, b)
})
assert.True(t, caseInsensitiveEq.Equals("hello", "HELLO")) assert.True(t, caseInsensitiveEq.Equals("hello", "HELLO"))
assert.True(t, caseInsensitiveEq.Equals("Hello", "hello")) assert.True(t, caseInsensitiveEq.Equals("Hello", "hello"))
@@ -243,9 +241,7 @@ func TestContramap(t *testing.T) {
}) })
t.Run("case-insensitive name comparison", func(t *testing.T) { t.Run("case-insensitive name comparison", func(t *testing.T) {
caseInsensitiveEq := FromEquals(func(a, b string) bool { caseInsensitiveEq := FromEquals(strings.EqualFold)
return strings.EqualFold(a, b)
})
personEqByNameCI := Contramap(func(p Person) string { personEqByNameCI := Contramap(func(p Person) string {
return p.Name return p.Name

View File

@@ -53,7 +53,10 @@ func Identity[A any](a A) A {
// //
// getMessage := Constant("Hello") // getMessage := Constant("Hello")
// msg := getMessage() // "Hello" // msg := getMessage() // "Hello"
//
//go:inline
func Constant[A any](a A) func() A { func Constant[A any](a A) func() A {
//go:inline
return func() A { return func() A {
return a return a
} }
@@ -81,7 +84,10 @@ func Constant[A any](a A) func() A {
// //
// defaultName := Constant1[int, string]("Unknown") // defaultName := Constant1[int, string]("Unknown")
// name := defaultName(42) // "Unknown" // name := defaultName(42) // "Unknown"
//
//go:inline
func Constant1[B, A any](a A) func(B) A { func Constant1[B, A any](a A) func(B) A {
//go:inline
return func(_ B) A { return func(_ B) A {
return a return a
} }
@@ -107,7 +113,10 @@ func Constant1[B, A any](a A) func(B) A {
// //
// alwaysTrue := Constant2[int, string, bool](true) // alwaysTrue := Constant2[int, string, bool](true)
// result := alwaysTrue(42, "test") // true // result := alwaysTrue(42, "test") // true
//
//go:inline
func Constant2[B, C, A any](a A) func(B, C) A { func Constant2[B, C, A any](a A) func(B, C) A {
//go:inline
return func(_ B, _ C) A { return func(_ B, _ C) A {
return a return a
} }
@@ -128,6 +137,8 @@ func Constant2[B, C, A any](a A) func(B, C) A {
// //
// value := 42 // value := 42
// IsNil(&value) // false // IsNil(&value) // false
//
//go:inline
func IsNil[A any](a *A) bool { func IsNil[A any](a *A) bool {
return a == nil return a == nil
} }
@@ -149,6 +160,8 @@ func IsNil[A any](a *A) bool {
// //
// value := 42 // value := 42
// IsNonNil(&value) // true // IsNonNil(&value) // true
//
//go:inline
func IsNonNil[A any](a *A) bool { func IsNonNil[A any](a *A) bool {
return a != nil return a != nil
} }
@@ -207,6 +220,8 @@ func Swap[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R {
// //
// result := First(42, "hello") // 42 // result := First(42, "hello") // 42
// result := First(true, 100) // true // result := First(true, 100) // true
//
//go:inline
func First[T1, T2 any](t1 T1, _ T2) T1 { func First[T1, T2 any](t1 T1, _ T2) T1 {
return t1 return t1
} }
@@ -231,6 +246,14 @@ func First[T1, T2 any](t1 T1, _ T2) T1 {
// //
// result := Second(42, "hello") // "hello" // result := Second(42, "hello") // "hello"
// result := Second(true, 100) // 100 // result := Second(true, 100) // 100
//
//go:inline
func Second[T1, T2 any](_ T1, t2 T2) T2 { func Second[T1, T2 any](_ T1, t2 T2) T2 {
return t2 return t2
} }
// Zero returns the zero value of the given type.
func Zero[A comparable]() A {
var zero A
return zero
}

View File

@@ -117,9 +117,13 @@ func Nullary2[F1 ~func() T1, F2 ~func(T1) T2, T1, T2 any](f1 F1, f2 F2) func() T
// Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter. // Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter.
// The inverse function is [Uncurry2] // The inverse function is [Uncurry2]
//go:inline
func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 { func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 {
//go:inline
return func(t0 T0) func(t1 T1) T2 { return func(t0 T0) func(t1 T1) T2 {
//go:inline
return func(t1 T1) T2 { return func(t1 T1) T2 {
//go:inline
return f(t0, t1) return f(t0, t1)
} }
} }

View File

@@ -51,7 +51,7 @@ package function
// ) // )
// result := classify(5) // "positive" // result := classify(5) // "positive"
// result2 := classify(-3) // "non-positive" // result2 := classify(-3) // "non-positive"
func Ternary[A, B any](pred func(A) bool, onTrue func(A) B, onFalse func(A) B) func(A) B { func Ternary[A, B any](pred func(A) bool, onTrue, onFalse func(A) B) func(A) B {
return func(a A) B { return func(a A) B {
if pred(a) { if pred(a) {
return onTrue(a) return onTrue(a)

View File

@@ -246,7 +246,7 @@ func (builder *Builder) GetTargetURL() Result[string] {
parseQuery, parseQuery,
result.Map(F.Flow2( result.Map(F.Flow2(
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()), F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
(url.Values).Encode, url.Values.Encode,
)), )),
), ),
), ),
@@ -351,13 +351,13 @@ func Header(name string) Lens[*Builder, Option[string]] {
LZ.Map(delHeader(name)), LZ.Map(delHeader(name)),
) )
return L.MakeLens(get, func(b *Builder, value Option[string]) *Builder { return L.MakeLensWithName(get, func(b *Builder, value Option[string]) *Builder {
cpy := b.clone() cpy := b.clone()
return F.Pipe1( return F.Pipe1(
value, value,
O.Fold(del(cpy), set(cpy)), O.Fold(del(cpy), set(cpy)),
) )
}) }, fmt.Sprintf("HttpHeader[%s]", name))
} }
// WithHeader creates a [Endomorphism] for a certain header // WithHeader creates a [Endomorphism] for a certain header

View File

@@ -16,6 +16,18 @@
/* /*
Package identity implements the Identity monad, the simplest possible monad. Package identity implements the Identity monad, the simplest possible monad.
# Fantasy Land Specification
This implementation corresponds to the Fantasy Land Identity type:
https://github.com/fantasyland/fantasy-land
Implemented Fantasy Land algebras:
- Functor: https://github.com/fantasyland/fantasy-land#functor
- Apply: https://github.com/fantasyland/fantasy-land#apply
- Applicative: https://github.com/fantasyland/fantasy-land#applicative
- Chain: https://github.com/fantasyland/fantasy-land#chain
- Monad: https://github.com/fantasyland/fantasy-land#monad
# Overview # Overview
The Identity monad is a trivial monad that simply wraps a value without adding The Identity monad is a trivial monad that simply wraps a value without adding

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
// 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 readerresult
import (
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// TraverseArray applies a ReaderResult-returning function to each element of an array,
// collecting the results. If any element fails, the entire operation fails with the first error.
//
// Example:
//
// parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
// ids := []int{1, 2, 3}
// result := readerresult.TraverseArray[DB](parseUser)(ids)
// // result(db) returns ([]User, nil) with all users or (nil, error) on first error
//
//go:inline
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return RR.TraverseArray(f)
}
//go:inline
func MonadTraverseArray[A, B any](as []A, f Kleisli[A, B]) ReaderResult[[]B] {
return RR.MonadTraverseArray(as, f)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index.
// This is useful when the transformation depends on the position in the array.
//
// Example:
//
// processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
// return readerresult.Of[Config](idx + len(item))
// }
// items := []string{"a", "bb", "ccc"}
// result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)
//
//go:inline
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderResult[B]) Kleisli[[]A, []B] {
return RR.TraverseArrayWithIndex(f)
}
// SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array.
// If any element fails, the entire operation fails with the first error encountered.
// All computations share the same environment.
//
// Example:
//
// readers := []readerresult.ReaderResult[Config, int]{
// readerresult.Of[Config](1),
// readerresult.Of[Config](2),
// readerresult.Of[Config](3),
// }
// result := readerresult.SequenceArray(readers)
// // result(cfg) returns ([]int{1, 2, 3}, nil)
//
//go:inline
func SequenceArray[A any](ma []ReaderResult[A]) ReaderResult[[]A] {
return RR.SequenceArray(ma)
}

View File

@@ -0,0 +1,456 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/idiomatic/result"
AP "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
// Do initializes a do-notation context with an empty state.
//
// This is the starting point for do-notation style composition, which allows
// imperative-style sequencing of ReaderResult computations while maintaining
// functional purity.
//
// Type Parameters:
// - S: The state type
//
// Parameters:
// - empty: The initial empty state
//
// Returns:
// - A ReaderResult[S] containing the initial state
//
//go:inline
func Do[S any](
empty S,
) ReaderResult[S] {
return RR.Do[context.Context](empty)
}
// Bind sequences an EFFECTFUL ReaderResult computation and updates the state with its result.
//
// IMPORTANT: Bind is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The Kleisli parameter (State -> ReaderResult[T]) is effectful because ReaderResult
// depends on context.Context (can be cancelled, has deadlines, carries values).
//
// For PURE FUNCTIONS (side-effect free), use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
// - Let: For pure functions without errors (State -> Value)
//
// This is the core operation for do-notation, allowing you to chain computations
// where each step can depend on the accumulated state and update it with new values.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the computation
//
// Parameters:
// - setter: A function that takes the computation result and returns a state updater
// - f: A Kleisli arrow that produces the next effectful computation based on current state
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return C.Bind(
Chain[S1, S2],
Map[T, S2],
setter,
WithContextK(f),
)
}
// Let attaches the result of a PURE computation to a state.
//
// IMPORTANT: Let is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The function parameter (State -> Value) is pure - it only reads from state with no effects.
//
// For EFFECTFUL FUNCTIONS (that need context.Context), use:
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
//
// For PURE FUNCTIONS with error handling, use:
// - BindResultK: For pure functions with errors (State -> (Value, error))
//
// Unlike Bind, Let works with pure functions (not ReaderResult computations).
// This is useful for deriving values from the current state without performing
// any effects.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value computed
//
// Parameters:
// - setter: A function that takes the computed value and returns a state updater
// - f: A pure function that computes a value from the current state
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[S1, S2] {
return RR.Let[context.Context](setter, f)
}
// LetTo attaches a constant value to a state.
// This is a PURE operation (side-effect free).
//
// This is a simplified version of Let for when you want to add a constant
// value to the state without computing it.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of the constant value
//
// Parameters:
// - setter: A function that takes the constant and returns a state updater
// - b: The constant value to attach
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[S1, S2] {
return RR.LetTo[context.Context](setter, b)
}
// BindTo initializes do-notation by binding a value to a state.
//
// This is typically used as the first operation after a computation to
// start building up a state structure.
//
// Type Parameters:
// - S1: The state type to create
// - T: The type of the initial value
//
// Parameters:
// - setter: A function that creates the initial state from a value
//
// Returns:
// - An Operator that transforms ReaderResult[T] to ReaderResult[S1]
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return RR.BindTo[context.Context](setter)
}
// BindToP initializes do-notation by binding a value to a state using a Prism.
//
// This is a variant of BindTo that uses a prism instead of a setter function.
// Prisms are useful for working with sum types and optional values.
//
// Type Parameters:
// - S1: The state type to create
// - T: The type of the initial value
//
// Parameters:
// - setter: A prism that can construct the state from a value
//
// Returns:
// - An Operator that transforms ReaderResult[T] to ReaderResult[S1]
//
//go:inline
func BindToP[S1, T any](
setter Prism[S1, T],
) Operator[T, S1] {
return BindTo(setter.ReverseGet)
}
// ApS attaches a value to a context using applicative style.
//
// IMPORTANT: ApS is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The ReaderResult parameter is effectful because it depends on context.Context.
//
// Unlike Bind (which sequences operations), ApS can be used when operations are
// independent and can conceptually run in parallel.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the computation
//
// Parameters:
// - setter: A function that takes the computation result and returns a state updater
// - fa: An effectful ReaderResult computation
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderResult[T],
) Operator[S1, S2] {
return AP.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
setter,
fa,
)
}
// ApSL is a variant of ApS that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: ApSL is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The ReaderResult parameter is effectful because it depends on context.Context.
//
// Instead of providing a setter function, you provide a lens that knows how to get and set
// the field. This is more convenient when working with nested structures.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - fa: An effectful ReaderResult computation that produces a value of type T
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func ApSL[S, T any](
lens Lens[S, T],
fa ReaderResult[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: BindL is for EFFECTFUL FUNCTIONS that depend on context.Context.
// The Kleisli parameter returns a ReaderResult, which is effectful.
//
// It combines lens-based field access with monadic composition, allowing you to:
// 1. Extract a field value using the lens
// 2. Use that value in an effectful computation that may fail
// 3. Update the field with the result
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - f: An effectful Kleisli arrow that transforms the field value
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func BindL[S, T any](
lens Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return RR.BindL(lens, WithContextK(f))
}
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: LetL is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
// The endomorphism parameter is a pure function (T -> T) with no errors or effects.
//
// It applies a pure transformation to the focused field without any effects.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - f: A pure endomorphism that transforms the field value
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func LetL[S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
return RR.LetL[context.Context](lens, f)
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
//
// IMPORTANT: LetToL is for setting constant values. This is a PURE operation (side-effect free).
//
// It sets the focused field to a constant value.
//
// Type Parameters:
// - S: The state type
// - T: The type of the field to update
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - b: The constant value to set
//
// Returns:
// - An Operator that transforms ReaderResult[S] to ReaderResult[S]
//
//go:inline
func LetToL[S, T any](
lens Lens[S, T],
b T,
) Operator[S, S] {
return RR.LetToL[context.Context](lens, b)
}
// BindReaderK binds a Reader computation (context-dependent but error-free) into the do-notation chain.
//
// IMPORTANT: This is for functions that depend on context.Context but don't return errors.
// The Reader[Context, T] is effectful because it depends on context.Context.
// Use this when you need context values but the operation cannot fail.
//
//go:inline
func BindReaderK[S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[context.Context, S1, T],
) Operator[S1, S2] {
return RR.BindReaderK(setter, f)
}
// BindEitherK binds a Result (Either) computation into the do-notation chain.
//
// IMPORTANT: This is for PURE FUNCTIONS (side-effect free) that return Result[T].
// The function (State -> Result[T]) is pure - it only depends on state, not context.
// Use this for pure error-handling logic that doesn't need context.
//
//go:inline
func BindEitherK[S1, S2, T any](
setter func(T) func(S1) S2,
f RES.Kleisli[S1, T],
) Operator[S1, S2] {
return RR.BindEitherK[context.Context](setter, f)
}
// BindResultK binds an idiomatic Go function (returning value and error) into the do-notation chain.
//
// IMPORTANT: This is for PURE FUNCTIONS (side-effect free) that return (Value, error).
// The function (State -> (Value, error)) is pure - it only depends on state, not context.
// Use this for pure computations with error handling that don't need context.
//
// For EFFECTFUL FUNCTIONS (that need context.Context), use Bind instead.
//
//go:inline
func BindResultK[S1, S2, T any](
setter func(T) func(S1) S2,
f result.Kleisli[S1, T],
) Operator[S1, S2] {
return RR.BindResultK[context.Context](setter, f)
}
// BindToReader converts a Reader computation into a ReaderResult and binds it to create an initial state.
//
// IMPORTANT: Reader[Context, T] is EFFECTFUL because it depends on context.Context.
// Use this when you have a context-dependent computation that cannot fail.
//
//go:inline
func BindToReader[
S1, T any](
setter func(T) S1,
) func(Reader[context.Context, T]) ReaderResult[S1] {
return RR.BindToReader[context.Context](setter)
}
// BindToEither converts a Result (Either) into a ReaderResult and binds it to create an initial state.
//
// IMPORTANT: Result[T] is PURE (side-effect free) - it doesn't depend on context.
// Use this to lift pure error-handling values into the ReaderResult context.
//
//go:inline
func BindToEither[
S1, T any](
setter func(T) S1,
) func(Result[T]) ReaderResult[S1] {
return RR.BindToEither[context.Context](setter)
}
// BindToResult converts an idiomatic Go tuple (value, error) into a ReaderResult and binds it to create an initial state.
//
// IMPORTANT: The (Value, error) tuple is PURE (side-effect free) - it doesn't depend on context.
// Use this to lift pure Go error-handling results into the ReaderResult context.
//
//go:inline
func BindToResult[
S1, T any](
setter func(T) S1,
) func(T, error) ReaderResult[S1] {
return RR.BindToResult[context.Context](setter)
}
// ApReaderS applies a Reader computation in applicative style, combining it with the current state.
//
// IMPORTANT: Reader[Context, T] is EFFECTFUL because it depends on context.Context.
// Use this for context-dependent operations that cannot fail.
//
//go:inline
func ApReaderS[
S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[context.Context, T],
) Operator[S1, S2] {
return RR.ApReaderS(setter, fa)
}
// ApResultS applies an idiomatic Go tuple (value, error) in applicative style.
//
// IMPORTANT: The (Value, error) tuple is PURE (side-effect free) - it doesn't depend on context.
// Use this for pure Go error-handling results.
//
//go:inline
func ApResultS[
S1, S2, T any](
setter func(T) func(S1) S2,
) func(T, error) Operator[S1, S2] {
return RR.ApResultS[context.Context](setter)
}
// ApEitherS applies a Result (Either) in applicative style, combining it with the current state.
//
// IMPORTANT: Result[T] is PURE (side-effect free) - it doesn't depend on context.
// Use this for pure error-handling values.
//
//go:inline
func ApEitherS[
S1, S2, T any](
setter func(T) func(S1) S2,
fa Result[T],
) Operator[S1, S2] {
return RR.ApEitherS[context.Context](setter, fa)
}

View File

@@ -0,0 +1,627 @@
// 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 readerresult
import (
"context"
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestDoInit(t *testing.T) {
initial := SimpleState{Value: 42}
result := Do(initial)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, initial, state)
}
func TestBind(t *testing.T) {
t.Run("successful bind", func(t *testing.T) {
// Effectful function that depends on context
fetchValue := func(s SimpleState) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return s.Value * 2, nil
}
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
Bind(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
fetchValue,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("bind with error", func(t *testing.T) {
fetchValue := func(s SimpleState) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, errors.New("fetch failed")
}
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
Bind(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
fetchValue,
),
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "fetch failed", err.Error())
})
}
func TestLet(t *testing.T) {
// Pure function that doesn't depend on context
double := func(s SimpleState) int {
return s.Value * 2
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
Let(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
double,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestLetTo(t *testing.T) {
result := F.Pipe1(
Do(SimpleState{}),
LetTo(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
100,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, state.Value)
}
func TestBindToInit(t *testing.T) {
getValue := func(ctx context.Context) (int, error) {
return 42, nil
}
result := F.Pipe1(
getValue,
BindTo(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestApS(t *testing.T) {
t.Run("successful ApS", func(t *testing.T) {
getValue := func(ctx context.Context) (int, error) {
return 100, nil
}
result := F.Pipe1(
Do(SimpleState{Value: 42}),
ApS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getValue,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, state.Value)
})
t.Run("ApS with error", func(t *testing.T) {
getValue := func(ctx context.Context) (int, error) {
return 0, errors.New("failed")
}
result := F.Pipe1(
Do(SimpleState{Value: 42}),
ApS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getValue,
),
)
_, err := result(context.Background())
assert.Error(t, err)
})
}
func TestApSL(t *testing.T) {
lenses := MakeSimpleStateLenses()
getValue := func(ctx context.Context) (int, error) {
return 100, nil
}
result := F.Pipe1(
Do(SimpleState{Value: 42}),
ApSL(lenses.Value, getValue),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, state.Value)
}
func TestBindL(t *testing.T) {
lenses := MakeSimpleStateLenses()
// Effectful function
increment := func(v int) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return v + 1, nil
}
}
result := F.Pipe1(
Do(SimpleState{Value: 41}),
BindL(lenses.Value, increment),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestLetL(t *testing.T) {
lenses := MakeSimpleStateLenses()
result := F.Pipe1(
Do(SimpleState{Value: 21}),
LetL(lenses.Value, N.Mul(2)),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestLetToL(t *testing.T) {
lenses := MakeSimpleStateLenses()
result := F.Pipe1(
Do(SimpleState{}),
LetToL(lenses.Value, 42),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestBindReaderK(t *testing.T) {
t.Run("successful BindReaderK", func(t *testing.T) {
// Context-dependent function that doesn't return error
getFromContext := func(s SimpleState) reader.Reader[context.Context, int] {
return func(ctx context.Context) int {
if val := ctx.Value("multiplier"); val != nil {
return s.Value * val.(int)
}
return s.Value
}
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindReaderK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getFromContext,
),
)
ctx := context.WithValue(context.Background(), "multiplier", 2)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
}
func TestBindEitherK(t *testing.T) {
t.Run("successful BindEitherK", func(t *testing.T) {
// Pure function returning Result
validate := func(s SimpleState) RES.Result[int] {
if s.Value > 0 {
return RES.Of(s.Value * 2)
}
return RES.Left[int](errors.New("value must be positive"))
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindEitherK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
validate,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("BindEitherK with error", func(t *testing.T) {
validate := func(s SimpleState) RES.Result[int] {
return RES.Left[int](errors.New("validation failed"))
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindEitherK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
validate,
),
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "validation failed", err.Error())
})
}
func TestBindResultK(t *testing.T) {
t.Run("successful BindResultK", func(t *testing.T) {
// Pure function returning (value, error)
parse := func(s SimpleState) (int, error) {
return s.Value * 2, nil
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindResultK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
parse,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("BindResultK with error", func(t *testing.T) {
parse := func(s SimpleState) (int, error) {
return 0, errors.New("parse failed")
}
result := F.Pipe1(
Do(SimpleState{Value: 21}),
BindResultK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
parse,
),
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "parse failed", err.Error())
})
}
func TestBindToReader(t *testing.T) {
getFromContext := func(ctx context.Context) int {
if val := ctx.Value("value"); val != nil {
return val.(int)
}
return 0
}
result := F.Pipe1(
getFromContext,
BindToReader(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
ctx := context.WithValue(context.Background(), "value", 42)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestBindToEither(t *testing.T) {
t.Run("successful BindToEither", func(t *testing.T) {
resultValue := RES.Of(42)
result := F.Pipe1(
resultValue,
BindToEither(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("BindToEither with error", func(t *testing.T) {
resultValue := RES.Left[int](errors.New("failed"))
result := F.Pipe1(
resultValue,
BindToEither(func(v int) SimpleState {
return SimpleState{Value: v}
}),
)
_, err := result(context.Background())
assert.Error(t, err)
})
}
func TestBindToResult(t *testing.T) {
t.Run("successful BindToResult", func(t *testing.T) {
value, err := 42, error(nil)
result := F.Pipe1(
BindToResult(func(v int) SimpleState {
return SimpleState{Value: v}
}),
func(f func(int, error) ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return f(value, err)
},
)
state, resultErr := result(context.Background())
assert.NoError(t, resultErr)
assert.Equal(t, 42, state.Value)
})
t.Run("BindToResult with error", func(t *testing.T) {
value, err := 0, errors.New("failed")
result := F.Pipe1(
BindToResult(func(v int) SimpleState {
return SimpleState{Value: v}
}),
func(f func(int, error) ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return f(value, err)
},
)
_, resultErr := result(context.Background())
assert.Error(t, resultErr)
})
}
func TestApReaderS(t *testing.T) {
getFromContext := func(ctx context.Context) int {
if val := ctx.Value("value"); val != nil {
return val.(int)
}
return 0
}
result := F.Pipe1(
Do(SimpleState{}),
ApReaderS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
getFromContext,
),
)
ctx := context.WithValue(context.Background(), "value", 42)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}
func TestApResultS(t *testing.T) {
t.Run("successful ApResultS", func(t *testing.T) {
value, err := 42, error(nil)
result := F.Pipe1(
Do(SimpleState{}),
func(rr ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return F.Pipe1(
rr,
ApResultS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
)(value, err),
)
},
)
state, resultErr := result(context.Background())
assert.NoError(t, resultErr)
assert.Equal(t, 42, state.Value)
})
t.Run("ApResultS with error", func(t *testing.T) {
value, err := 0, errors.New("failed")
result := F.Pipe1(
Do(SimpleState{}),
func(rr ReaderResult[SimpleState]) ReaderResult[SimpleState] {
return F.Pipe1(
rr,
ApResultS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
)(value, err),
)
},
)
_, resultErr := result(context.Background())
assert.Error(t, resultErr)
})
}
func TestApEitherS(t *testing.T) {
t.Run("successful ApEitherS", func(t *testing.T) {
resultValue := RES.Of(42)
result := F.Pipe1(
Do(SimpleState{}),
ApEitherS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
resultValue,
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
})
t.Run("ApEitherS with error", func(t *testing.T) {
resultValue := RES.Left[int](errors.New("failed"))
result := F.Pipe1(
Do(SimpleState{}),
ApEitherS(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
resultValue,
),
)
_, err := result(context.Background())
assert.Error(t, err)
})
}
func TestComplexPipeline(t *testing.T) {
lenses := MakeSimpleStateLenses()
// Complex pipeline combining multiple operations
result := F.Pipe3(
Do(SimpleState{}),
LetToL(lenses.Value, 10),
LetL(lenses.Value, N.Mul(2)),
BindResultK(
func(v int) func(SimpleState) SimpleState {
return func(s SimpleState) SimpleState {
s.Value = v
return s
}
},
func(s SimpleState) (int, error) {
return s.Value + 22, nil
},
),
)
state, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, state.Value)
}

View File

@@ -0,0 +1,409 @@
// 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 readerresult
import (
"context"
"io"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// Bracket ensures safe resource management with guaranteed cleanup in the ReaderResult monad.
//
// This function implements the bracket pattern (also known as try-with-resources or RAII)
// for ReaderResult computations. It guarantees that the release action is called regardless
// of whether the use action succeeds or fails, making it ideal for managing resources like
// file handles, database connections, network sockets, or locks.
//
// The execution flow is:
// 1. Acquire the resource (lazily evaluated)
// 2. Use the resource with the provided function
// 3. Release the resource with access to: the resource, the result (if successful), and any error
//
// The release function is always called, even if:
// - The acquire action fails (release is not called in this case)
// - The use action fails (release receives the error)
// - The use action succeeds (release receives nil error)
//
// Type Parameters:
// - A: The type of the acquired resource
// - B: The type of the result produced by using the resource
// - ANY: The type returned by the release action (typically ignored)
//
// Parameters:
// - acquire: Lazy computation that acquires the resource
// - use: Function that uses the resource to produce a result
// - release: Function that releases the resource, receiving the resource, result, and any error
//
// Returns:
// - A ReaderResult[B] that safely manages the resource lifecycle
//
// Example - File handling:
//
// import (
// "context"
// "os"
// )
//
// readFile := readerresult.Bracket(
// // Acquire: Open file
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("data.txt")
// }
// },
// // Use: Read file contents
// func(file *os.File) readerresult.ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// data, err := io.ReadAll(file)
// return string(data), err
// }
// },
// // Release: Close file (always called)
// func(file *os.File, content string, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, file.Close()
// }
// },
// )
//
// content, err := readFile(context.Background())
//
// Example - Database connection:
//
// queryDB := readerresult.Bracket(
// // Acquire: Open connection
// func() readerresult.ReaderResult[*sql.DB] {
// return func(ctx context.Context) (*sql.DB, error) {
// return sql.Open("postgres", connString)
// }
// },
// // Use: Execute query
// func(db *sql.DB) readerresult.ReaderResult[[]User] {
// return func(ctx context.Context) ([]User, error) {
// return queryUsers(ctx, db)
// }
// },
// // Release: Close connection (always called)
// func(db *sql.DB, users []User, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, db.Close()
// }
// },
// )
//
// Example - Lock management:
//
// withLock := readerresult.Bracket(
// // Acquire: Lock mutex
// func() readerresult.ReaderResult[*sync.Mutex] {
// return func(ctx context.Context) (*sync.Mutex, error) {
// mu.Lock()
// return mu, nil
// }
// },
// // Use: Perform critical section work
// func(mu *sync.Mutex) readerresult.ReaderResult[int] {
// return func(ctx context.Context) (int, error) {
// return performCriticalWork(ctx)
// }
// },
// // Release: Unlock mutex (always called)
// func(mu *sync.Mutex, result int, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// mu.Unlock()
// return nil, nil
// }
// },
// )
//
//go:inline
func Bracket[
A, B, ANY any](
acquire Lazy[ReaderResult[A]],
use Kleisli[A, B],
release func(A, B, error) ReaderResult[ANY],
) ReaderResult[B] {
return RR.Bracket(acquire, WithContextK(use), release)
}
// WithResource creates a higher-order function for resource management with automatic cleanup.
//
// This function provides a more composable alternative to Bracket by creating a function
// that takes a resource-using function and automatically handles resource acquisition and
// release. This is particularly useful when you want to reuse the same resource management
// pattern with different operations.
//
// The pattern is:
// 1. Create a resource manager with onCreate and onRelease
// 2. Apply it to different use functions as needed
// 3. Each application ensures proper resource cleanup
//
// This is useful for:
// - Creating reusable resource management patterns
// - Building resource pools or factories
// - Composing resource-dependent operations
// - Abstracting resource lifecycle management
//
// Type Parameters:
// - B: The type of the result produced by using the resource
// - A: The type of the acquired resource
// - ANY: The type returned by the release action (typically ignored)
//
// Parameters:
// - onCreate: Lazy computation that creates/acquires the resource
// - onRelease: Function that releases the resource (receives the resource)
//
// Returns:
// - A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B]
// with automatic resource management
//
// Example - Reusable database connection manager:
//
// import (
// "context"
// "database/sql"
// )
//
// // Create a reusable DB connection manager
// withDB := readerresult.WithResource(
// // onCreate: Acquire connection
// func() readerresult.ReaderResult[*sql.DB] {
// return func(ctx context.Context) (*sql.DB, error) {
// return sql.Open("postgres", connString)
// }
// },
// // onRelease: Close connection
// func(db *sql.DB) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, db.Close()
// }
// },
// )
//
// // Use the manager with different operations
// getUsers := withDB(func(db *sql.DB) readerresult.ReaderResult[[]User] {
// return func(ctx context.Context) ([]User, error) {
// return queryUsers(ctx, db)
// }
// })
//
// getOrders := withDB(func(db *sql.DB) readerresult.ReaderResult[[]Order] {
// return func(ctx context.Context) ([]Order, error) {
// return queryOrders(ctx, db)
// }
// })
//
// // Both operations automatically manage the connection
// users, err := getUsers(context.Background())
// orders, err := getOrders(context.Background())
//
// Example - File operations manager:
//
// withFile := readerresult.WithResource(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("config.json")
// }
// },
// func(file *os.File) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, file.Close()
// }
// },
// )
//
// // Different operations on the same file
// readConfig := withFile(func(file *os.File) readerresult.ReaderResult[Config] {
// return func(ctx context.Context) (Config, error) {
// return parseConfig(file)
// }
// })
//
// validateConfig := withFile(func(file *os.File) readerresult.ReaderResult[bool] {
// return func(ctx context.Context) (bool, error) {
// return validateConfigFile(file)
// }
// })
//
// Example - Composing with other operations:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Create a pipeline with automatic resource management
// processData := F.Pipe2(
// loadData,
// withDB(func(db *sql.DB) readerresult.ReaderResult[Result] {
// return saveToDatabase(db)
// }),
// readerresult.Map(formatResult),
// )
//
//go:inline
func WithResource[B, A, ANY any](
onCreate Lazy[ReaderResult[A]],
onRelease Kleisli[A, ANY],
) Kleisli[Kleisli[A, B], B] {
return WithContextK(RR.WithResource[B](onCreate, onRelease))
}
// onClose is a helper function that creates a ReaderResult that closes an io.Closer.
// This is used internally by WithCloser to provide automatic cleanup for resources
// that implement the io.Closer interface.
func onClose[A io.Closer](a A) ReaderResult[struct{}] {
return func(_ context.Context) (struct{}, error) {
return struct{}{}, a.Close()
}
}
// WithCloser creates a higher-order function for managing resources that implement io.Closer.
//
// This is a specialized version of WithResource that automatically handles cleanup for any
// resource implementing the io.Closer interface (such as files, network connections, HTTP
// response bodies, etc.). It eliminates the need to manually specify the release function,
// making it more convenient for common Go resources.
//
// The function automatically calls Close() on the resource when the operation completes,
// regardless of success or failure. This ensures proper resource cleanup following Go's
// standard io.Closer pattern.
//
// Type Parameters:
// - B: The type of the result produced by using the resource
// - A: The type of the resource, which must implement io.Closer
//
// Parameters:
// - onCreate: Lazy computation that creates/acquires the io.Closer resource
//
// Returns:
// - A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B]
// with automatic Close() cleanup
//
// Example - File operations:
//
// import (
// "context"
// "os"
// "io"
// )
//
// // Create a reusable file manager
// withFile := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("data.txt")
// }
// },
// )
//
// // Use with different operations - Close() is automatic
// readContent := withFile(func(file *os.File) readerresult.ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// data, err := io.ReadAll(file)
// return string(data), err
// }
// })
//
// getSize := withFile(func(file *os.File) readerresult.ReaderResult[int64] {
// return func(ctx context.Context) (int64, error) {
// info, err := file.Stat()
// if err != nil {
// return 0, err
// }
// return info.Size(), nil
// }
// })
//
// content, err := readContent(context.Background())
// size, err := getSize(context.Background())
//
// Example - HTTP response body:
//
// import "net/http"
//
// withResponse := readerresult.WithCloser(
// func() readerresult.ReaderResult[*http.Response] {
// return func(ctx context.Context) (*http.Response, error) {
// return http.Get("https://api.example.com/data")
// }
// },
// )
//
// // Body is automatically closed after use
// parseJSON := withResponse(func(resp *http.Response) readerresult.ReaderResult[Data] {
// return func(ctx context.Context) (Data, error) {
// var data Data
// err := json.NewDecoder(resp.Body).Decode(&data)
// return data, err
// }
// })
//
// Example - Multiple file operations:
//
// // Read from one file, write to another
// copyFile := func(src, dst string) readerresult.ReaderResult[int64] {
// withSrc := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open(src)
// }
// },
// )
//
// withDst := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Create(dst)
// }
// },
// )
//
// return withSrc(func(srcFile *os.File) readerresult.ReaderResult[int64] {
// return withDst(func(dstFile *os.File) readerresult.ReaderResult[int64] {
// return func(ctx context.Context) (int64, error) {
// return io.Copy(dstFile, srcFile)
// }
// })
// })
// }
//
// Example - Network connection:
//
// import "net"
//
// withConn := readerresult.WithCloser(
// func() readerresult.ReaderResult[net.Conn] {
// return func(ctx context.Context) (net.Conn, error) {
// return net.Dial("tcp", "localhost:8080")
// }
// },
// )
//
// sendData := withConn(func(conn net.Conn) readerresult.ReaderResult[int] {
// return func(ctx context.Context) (int, error) {
// return conn.Write([]byte("Hello, World!"))
// }
// })
//
// Note: WithCloser is a convenience wrapper around WithResource that automatically
// provides the Close() cleanup function. For resources that don't implement io.Closer
// or require custom cleanup logic, use WithResource or Bracket instead.
//
//go:inline
func WithCloser[B any, A io.Closer](onCreate Lazy[ReaderResult[A]]) Kleisli[Kleisli[A, B], B] {
return WithResource[B](onCreate, onClose[A])
}

View File

@@ -0,0 +1,630 @@
// 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 readerresult
import (
"context"
"errors"
"io"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
// mockResource simulates a resource that needs cleanup
type mockResource struct {
id int
closed bool
closeMu sync.Mutex
closeErr error
}
func (m *mockResource) Close() error {
m.closeMu.Lock()
defer m.closeMu.Unlock()
m.closed = true
return m.closeErr
}
func (m *mockResource) IsClosed() bool {
m.closeMu.Lock()
defer m.closeMu.Unlock()
return m.closed
}
// mockCloser implements io.Closer for testing WithCloser
type mockCloser struct {
*mockResource
}
func TestBracketExtended(t *testing.T) {
t.Run("successful acquire, use, and release with real resource", func(t *testing.T) {
resource := &mockResource{id: 1}
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 2, nil
}
},
// Release
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
assert.Equal(t, 2, result)
assert.NoError(t, err)
return nil, r.Close()
}
},
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 2, value)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("acquire fails - release not called", func(t *testing.T) {
acquireErr := errors.New("acquire failed")
released := false
result := Bracket(
// Acquire fails
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return nil, acquireErr
}
},
// Use (should not be called)
func(r *mockResource) ReaderResult[int] {
t.Fatal("use should not be called when acquire fails")
return func(ctx context.Context) (int, error) {
return 0, nil
}
},
// Release (should not be called)
func(r *mockResource, result int, err error) ReaderResult[any] {
released = true
return func(ctx context.Context) (any, error) {
return nil, nil
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, acquireErr, err)
assert.False(t, released)
})
t.Run("use fails - release still called", func(t *testing.T) {
resource := &mockResource{id: 1}
useErr := errors.New("use failed")
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use fails
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, useErr
}
},
// Release (should still be called)
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
assert.Equal(t, 0, result)
assert.Equal(t, useErr, err)
return nil, r.Close()
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, useErr, err)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("release fails - error propagated", func(t *testing.T) {
resource := &mockResource{id: 1, closeErr: errors.New("close failed")}
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use succeeds
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 2, nil
}
},
// Release fails
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
return nil, r.Close()
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, "close failed", err.Error())
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("both use and release fail - use error takes precedence", func(t *testing.T) {
resource := &mockResource{id: 1, closeErr: errors.New("close failed")}
useErr := errors.New("use failed")
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use fails
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, useErr
}
},
// Release also fails
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
assert.Equal(t, useErr, err)
return nil, r.Close()
}
},
)
_, err := result(context.Background())
assert.Error(t, err)
// The use error should be returned
assert.Equal(t, useErr, err)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
t.Run("context cancellation during use", func(t *testing.T) {
resource := &mockResource{id: 1}
released := false
result := Bracket(
// Acquire
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
// Use checks context
func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
return r.id * 2, nil
}
}
},
// Release
func(r *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
released = true
return nil, r.Close()
}
},
)
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := result(ctx)
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
assert.True(t, released)
assert.True(t, resource.IsClosed())
})
}
func TestWithResource(t *testing.T) {
t.Run("reusable resource manager - successful operations", func(t *testing.T) {
resource := &mockResource{id: 42}
createCount := 0
releaseCount := 0
withResource := WithResource[int](
// onCreate
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
createCount++
return resource, nil
}
},
// onRelease
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
releaseCount++
return nil, r.Close()
}
},
)
// First operation
operation1 := withResource(func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 2, nil
}
})
result1, err1 := operation1(context.Background())
assert.NoError(t, err1)
assert.Equal(t, 84, result1)
assert.Equal(t, 1, createCount)
assert.Equal(t, 1, releaseCount)
// Reset for second operation
resource.closed = false
// Second operation with same resource manager
operation2 := withResource(func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id + 10, nil
}
})
result2, err2 := operation2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, 52, result2)
assert.Equal(t, 2, createCount)
assert.Equal(t, 2, releaseCount)
})
t.Run("resource manager with failing operation", func(t *testing.T) {
resource := &mockResource{id: 42}
releaseCount := 0
opErr := errors.New("operation failed")
withResource := WithResource[int](
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource, nil
}
},
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
releaseCount++
return nil, r.Close()
}
},
)
operation := withResource(func(r *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return 0, opErr
}
})
_, err := operation(context.Background())
assert.Error(t, err)
assert.Equal(t, opErr, err)
assert.Equal(t, 1, releaseCount)
assert.True(t, resource.IsClosed())
})
t.Run("nested resource managers", func(t *testing.T) {
resource1 := &mockResource{id: 1}
resource2 := &mockResource{id: 2}
withResource1 := WithResource[int](
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource1, nil
}
},
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, r.Close()
}
},
)
withResource2 := WithResource[int](
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return resource2, nil
}
},
func(r *mockResource) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, r.Close()
}
},
)
// Nest the resource managers
operation := withResource1(func(r1 *mockResource) ReaderResult[int] {
return withResource2(func(r2 *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r1.id + r2.id, nil
}
})
})
result, err := operation(context.Background())
assert.NoError(t, err)
assert.Equal(t, 3, result)
assert.True(t, resource1.IsClosed())
assert.True(t, resource2.IsClosed())
})
}
func TestWithCloser(t *testing.T) {
t.Run("successful operation with io.Closer", func(t *testing.T) {
resource := &mockCloser{mockResource: &mockResource{id: 100}}
withCloser := WithCloser[string](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
return resource, nil
}
},
)
operation := withCloser(func(r *mockCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "success", nil
}
})
result, err := operation(context.Background())
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.True(t, resource.IsClosed())
})
t.Run("operation fails but closer still called", func(t *testing.T) {
resource := &mockCloser{mockResource: &mockResource{id: 100}}
opErr := errors.New("operation failed")
withCloser := WithCloser[string](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
return resource, nil
}
},
)
operation := withCloser(func(r *mockCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "", opErr
}
})
_, err := operation(context.Background())
assert.Error(t, err)
assert.Equal(t, opErr, err)
assert.True(t, resource.IsClosed())
})
t.Run("closer fails", func(t *testing.T) {
closeErr := errors.New("close failed")
resource := &mockCloser{mockResource: &mockResource{id: 100, closeErr: closeErr}}
withCloser := WithCloser[string](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
return resource, nil
}
},
)
operation := withCloser(func(r *mockCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "success", nil
}
})
_, err := operation(context.Background())
assert.Error(t, err)
assert.Equal(t, closeErr, err)
assert.True(t, resource.IsClosed())
})
t.Run("with strings.Reader (real io.Closer)", func(t *testing.T) {
content := "Hello, World!"
withReader := WithCloser[string](
func() ReaderResult[io.ReadCloser] {
return func(ctx context.Context) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(content)), nil
}
},
)
operation := withReader(func(r io.ReadCloser) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
data, err := io.ReadAll(r)
return string(data), err
}
})
result, err := operation(context.Background())
assert.NoError(t, err)
assert.Equal(t, content, result)
})
t.Run("multiple operations with same closer", func(t *testing.T) {
createCount := 0
withCloser := WithCloser[int](
func() ReaderResult[*mockCloser] {
return func(ctx context.Context) (*mockCloser, error) {
createCount++
return &mockCloser{mockResource: &mockResource{id: createCount}}, nil
}
},
)
// First operation
op1 := withCloser(func(r *mockCloser) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 10, nil
}
})
result1, err1 := op1(context.Background())
assert.NoError(t, err1)
assert.Equal(t, 10, result1)
// Second operation
op2 := withCloser(func(r *mockCloser) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return r.id * 20, nil
}
})
result2, err2 := op2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, 40, result2)
assert.Equal(t, 2, createCount)
})
}
func TestOnClose(t *testing.T) {
t.Run("onClose helper function", func(t *testing.T) {
resource := &mockCloser{mockResource: &mockResource{id: 1}}
closeFunc := onClose(resource)
_, err := closeFunc(context.Background())
assert.NoError(t, err)
assert.True(t, resource.IsClosed())
})
t.Run("onClose with error", func(t *testing.T) {
closeErr := errors.New("close error")
resource := &mockCloser{mockResource: &mockResource{id: 1, closeErr: closeErr}}
closeFunc := onClose(resource)
_, err := closeFunc(context.Background())
assert.Error(t, err)
assert.Equal(t, closeErr, err)
assert.True(t, resource.IsClosed())
})
}
// Integration test combining multiple bracket patterns
func TestBracketIntegration(t *testing.T) {
t.Run("complex resource management scenario", func(t *testing.T) {
// Simulate a scenario with multiple resources
db := &mockResource{id: 1}
cache := &mockResource{id: 2}
logger := &mockResource{id: 3}
result := Bracket(
// Acquire DB
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return db, nil
}
},
// Use DB to get cache and logger
func(dbRes *mockResource) ReaderResult[int] {
return Bracket(
// Acquire cache
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return cache, nil
}
},
// Use cache to get logger
func(cacheRes *mockResource) ReaderResult[int] {
return Bracket(
// Acquire logger
func() ReaderResult[*mockResource] {
return func(ctx context.Context) (*mockResource, error) {
return logger, nil
}
},
// Use all resources
func(logRes *mockResource) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
return dbRes.id + cacheRes.id + logRes.id, nil
}
},
// Release logger
func(logRes *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, logRes.Close()
}
},
)
},
// Release cache
func(cacheRes *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, cacheRes.Close()
}
},
)
},
// Release DB
func(dbRes *mockResource, result int, err error) ReaderResult[any] {
return func(ctx context.Context) (any, error) {
return nil, dbRes.Close()
}
},
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 6, value) // 1 + 2 + 3
assert.True(t, db.IsClosed())
assert.True(t, cache.IsClosed())
assert.True(t, logger.IsClosed())
})
}

View File

@@ -0,0 +1,113 @@
// 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 readerresult
import (
"context"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/result"
)
// WithContext wraps an existing ReaderResult and performs a context check for cancellation before delegating
// to the underlying computation.
//
// If the context has been cancelled (ctx.Err() != nil), it immediately returns an error containing the
// cancellation cause without executing the wrapped computation. Otherwise, it delegates to the original
// ReaderResult.
//
// This is useful for adding cancellation checks to computations that may not check the context themselves,
// ensuring that cancelled operations fail fast.
//
// Example:
//
// // A computation that might take a long time
// slowComputation := func(ctx context.Context) (int, error) {
// time.Sleep(5 * time.Second)
// return 42, nil
// }
//
// // Wrap it to check for cancellation before execution
// safeSlow := readerresult.WithContext(slowComputation)
//
// // If context is already cancelled, this returns immediately
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
// result, err := safeSlow(ctx) // Returns error immediately without sleeping
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) (A, error) {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
return ma(ctx)
}
}
// WithContextK wraps a Kleisli arrow (a function that returns a ReaderResult) with context cancellation checking.
//
// This is the Kleisli arrow version of WithContext. It takes a function A -> ReaderResult[B] and returns
// a new function that performs the same transformation but with an added context cancellation check before
// executing the resulting ReaderResult.
//
// A Kleisli arrow is a function that takes a value and returns a monadic computation. In this case,
// Kleisli[A, B] = func(A) ReaderResult[B], which represents a function from A to a context-dependent
// computation that may fail.
//
// WithContextK is particularly useful when composing operations with Chain/Bind, as it ensures that
// each step in the composition checks for cancellation before proceeding.
//
// Parameters:
// - f: A Kleisli arrow (function from A to ReaderResult[B])
//
// Returns:
// - A new Kleisli arrow that wraps the result of f with context cancellation checking
//
// Example:
//
// // A function that fetches user details
// fetchUserDetails := func(userID int) readerresult.ReaderResult[UserDetails] {
// return func(ctx context.Context) (UserDetails, error) {
// // Fetch from database...
// return details, nil
// }
// }
//
// // Wrap it to ensure cancellation is checked before each execution
// safeFetchDetails := readerresult.WithContextK(fetchUserDetails)
//
// // Use in a composition chain
// pipeline := F.Pipe2(
// getUser(42),
// readerresult.Chain(safeFetchDetails), // Checks cancellation before fetching details
// )
//
// // If context is cancelled between getUser and fetchUserDetails,
// // the details fetch will not execute
// ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
// defer cancel()
// result, err := pipeline(ctx)
//
// Use Cases:
// - Adding cancellation checks to composed operations
// - Ensuring long-running pipelines respect context cancellation
// - Wrapping third-party functions that don't check context themselves
// - Creating fail-fast behavior in complex operation chains
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
return F.Flow2(
f,
WithContext,
)
}

View File

@@ -0,0 +1,210 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// Curry0 converts a function that takes context.Context and returns (A, error) into a ReaderResult[A].
//
// This is useful for lifting existing functions that follow Go's context-first convention
// into the ReaderResult monad.
//
// Type Parameters:
// - A: The return value type
//
// Parameters:
// - f: A function that takes context.Context and returns (A, error)
//
// Returns:
// - A ReaderResult[A] that wraps the function
//
// Example:
//
// func getConfig(ctx context.Context) (Config, error) {
// // ... implementation
// return config, nil
// }
// rr := readerresult.Curry0(getConfig)
// config, err := rr(ctx)
//
//go:inline
func Curry0[A any](f func(context.Context) (A, error)) ReaderResult[A] {
return RR.Curry0(f)
}
// Curry1 converts a function with one parameter into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameter before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1) and returns (A, error)
//
// Returns:
// - A curried function that takes T1 and returns ReaderResult[A]
//
// Example:
//
// func getUser(ctx context.Context, id int) (User, error) {
// // ... implementation
// return user, nil
// }
// getUserRR := readerresult.Curry1(getUser)
// rr := getUserRR(42) // Partially applied
// user, err := rr(ctx) // Execute with context
//
//go:inline
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A] {
return RR.Curry1(f)
}
// Curry2 converts a function with two parameters into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameters before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2) and returns (A, error)
//
// Returns:
// - A curried function that takes T1, then T2, and returns ReaderResult[A]
//
// Example:
//
// func updateUser(ctx context.Context, id int, name string) (User, error) {
// // ... implementation
// return user, nil
// }
// updateUserRR := readerresult.Curry2(updateUser)
// rr := updateUserRR(42)("Alice") // Partially applied
// user, err := rr(ctx) // Execute with context
//
//go:inline
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) func(T2) ReaderResult[A] {
return RR.Curry2(f)
}
// Curry3 converts a function with three parameters into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameters before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
// Returns:
// - A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]
//
// Example:
//
// func createPost(ctx context.Context, userID int, title string, body string) (Post, error) {
// // ... implementation
// return post, nil
// }
// createPostRR := readerresult.Curry3(createPost)
// rr := createPostRR(42)("Title")("Body") // Partially applied
// post, err := rr(ctx) // Execute with context
//
//go:inline
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderResult[A] {
return RR.Curry3(f)
}
// Uncurry1 converts a curried ReaderResult function back to a standard Go function.
//
// This is the inverse of Curry1, useful when you need to call curried functions
// in a traditional Go style.
//
// Type Parameters:
// - T1: The parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1 and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1) and returns (A, error)
//
// Example:
//
// curriedFn := func(id int) readerresult.ReaderResult[User] { ... }
// normalFn := readerresult.Uncurry1(curriedFn)
// user, err := normalFn(ctx, 42)
//
//go:inline
func Uncurry1[T1, A any](f func(T1) ReaderResult[A]) func(context.Context, T1) (A, error) {
return RR.Uncurry1(f)
}
// Uncurry2 converts a curried ReaderResult function with two parameters back to a standard Go function.
//
// This is the inverse of Curry2.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1, then T2, and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1, T2) and returns (A, error)
//
//go:inline
func Uncurry2[T1, T2, A any](f func(T1) func(T2) ReaderResult[A]) func(context.Context, T1, T2) (A, error) {
return RR.Uncurry2(f)
}
// Uncurry3 converts a curried ReaderResult function with three parameters back to a standard Go function.
//
// This is the inverse of Curry3.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
//go:inline
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderResult[A]) func(context.Context, T1, T2, T3) (A, error) {
return RR.Uncurry3(f)
}

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 readerresult provides a ReaderResult monad specialized for context.Context.
//
// A ReaderResult[A] represents an effectful computation that:
// - Takes a context.Context as input
// - May fail with an error (Result aspect, which is Either[error, A])
// - Returns a value of type A on success
//
// The type is defined as: ReaderResult[A any] = func(context.Context) (A, error)
//
// This is equivalent to Reader[context.Context, Result[A]] or Reader[context.Context, Either[error, A]],
// but specialized to always use context.Context as the environment type.
//
// # Effectful Computations with Context
//
// ReaderResult is particularly well-suited for representing effectful computations in Go. An effectful
// computation is one that:
//
// - Performs side effects (I/O, network calls, database operations, etc.)
// - May fail with an error
// - Requires contextual information (cancellation, deadlines, request-scoped values)
//
// By using context.Context as the fixed environment type, ReaderResult[A] provides:
//
// 1. Cancellation propagation - operations can be cancelled via context
// 2. Deadline/timeout handling - operations respect context deadlines
// 3. Request-scoped values - access to request metadata, trace IDs, etc.
// 4. Functional composition - chain effectful operations while maintaining context
// 5. Error handling - explicit error propagation through the Result type
//
// This pattern is idiomatic in Go, where functions performing I/O conventionally accept
// context.Context as their first parameter: func(ctx context.Context, ...) (Result, error).
// ReaderResult preserves this convention while enabling functional composition.
//
// Example of an effectful computation:
//
// // An effectful operation that queries a database
// func fetchUser(ctx context.Context, id int) (User, error) {
// // ctx provides cancellation, deadlines, and request context
// row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id)
// var user User
// err := row.Scan(&user.ID, &user.Name)
// return user, err
// }
//
// // Lift into ReaderResult for functional composition
// getUser := readerresult.Curry1(fetchUser)
//
// // Compose multiple effectful operations
// pipeline := F.Pipe2(
// getUser(42), // ReaderResult[User]
// readerresult.Chain(func(user User) readerresult.ReaderResult[[]Post] {
// return getPosts(user.ID) // Another effectful operation
// }),
// )
//
// // Execute with a context (e.g., from an HTTP request)
// ctx := r.Context() // HTTP request context
// posts, err := pipeline(ctx)
//
// # Use Cases
//
// ReaderResult is particularly useful for:
//
// 1. Effectful computations with context - operations that perform I/O and need cancellation/deadlines
// 2. Functional error handling - compose operations that depend on context and may error
// 3. Testing - easily mock context-dependent operations
// 4. HTTP handlers - chain request processing operations with proper context propagation
//
// # Composition
//
// ReaderResult provides several ways to compose computations:
//
// 1. Map - transform successful values
// 2. Chain (FlatMap) - sequence dependent operations
// 3. Ap - combine independent computations
// 4. Do-notation - imperative-style composition with Bind
//
// # Do-Notation Example
//
// type State struct {
// User User
// Posts []Post
// }
//
// result := F.Pipe2(
// readerresult.Do(State{}),
// readerresult.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerresult.ReaderResult[User] {
// return getUser(42)
// },
// ),
// readerresult.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerresult.ReaderResult[[]Post] {
// return getPosts(s.User.ID)
// },
// ),
// )
//
// # Currying Functions with Context
//
// The Curry functions enable partial application of function parameters while deferring
// the context.Context parameter until execution time.
//
// When you curry a function like func(context.Context, T1, T2) (A, error), the context.Context
// becomes the last argument to be applied, even though it appears first in the original function
// signature. This is intentional and follows Go's context-first convention while enabling
// functional composition patterns.
//
// Why context.Context is the last curried argument:
//
// - In Go, context conventionally comes first: func(ctx context.Context, params...) (Result, error)
// - In curried form: Curry2(f)(param1)(param2) returns ReaderResult[A]
// - The ReaderResult is then applied to ctx: Curry2(f)(param1)(param2)(ctx)
// - This allows partial application of business parameters before providing the context
//
// Example with database operations:
//
// // Database operations following Go conventions (context first)
// func fetchUser(ctx context.Context, db *sql.DB, id int) (User, error) {
// row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id)
// var user User
// err := row.Scan(&user.ID, &user.Name)
// return user, err
// }
//
// func updateUser(ctx context.Context, db *sql.DB, id int, name string) (User, error) {
// _, err := db.ExecContext(ctx, "UPDATE users SET name = ? WHERE id = ?", name, id)
// if err != nil {
// return User{}, err
// }
// return fetchUser(ctx, db, id)
// }
//
// // Curry these into composable operations
// getUser := readerresult.Curry2(fetchUser)
// updateUserName := readerresult.Curry3(updateUser)
//
// // Compose operations with partial application
// pipeline := F.Pipe2(
// getUser(db)(42), // ReaderResult[User] - db and id applied, waiting for ctx
// readerresult.Chain(func(user User) readerresult.ReaderResult[User] {
// newName := user.Name + " (updated)"
// return updateUserName(db)(user.ID)(newName) // Waiting for ctx
// }),
// )
//
// // Execute by providing the context
// ctx := context.Background()
// updatedUser, err := pipeline(ctx)
//
// The key insight is that currying creates a chain where:
// 1. Business parameters are applied first: getUser(db)(42)
// 2. This returns a ReaderResult[User] that waits for the context
// 3. Multiple operations can be composed before providing the context
// 4. Finally, the context is provided to execute everything: pipeline(ctx)
//
// This pattern is particularly useful for:
// - Creating reusable operation pipelines independent of specific contexts
// - Testing with different contexts (with timeouts, cancellation, etc.)
// - Composing operations that share the same context
// - Deferring context creation until execution time
//
// # Error Handling
//
// ReaderResult provides several functions for error handling:
//
// - Left/Right - create failed/successful values
// - GetOrElse - provide a default value for errors
// - OrElse - recover from errors with an alternative computation
// - Fold - handle both success and failure cases
// - ChainEitherK - lift result.Result computations into ReaderResult
//
// # Relationship to Other Monads
//
// ReaderResult is related to several other monads in this library:
//
// - Reader[context.Context, A] - ReaderResult without error handling
// - Result[A] (Either[error, A]) - error handling without context dependency
// - IOResult[A] - similar to ReaderResult but without explicit context parameter
// - ReaderIOResult[R, A] - generic version that allows custom environment type R
//
// # Performance Note
//
// ReaderResult is a zero-cost abstraction - it compiles to a simple function type
// with no runtime overhead beyond the underlying computation.
package readerresult

View File

@@ -0,0 +1,738 @@
// 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 readerresult
import (
"context"
"fmt"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/optics/lens"
RES "github.com/IBM/fp-go/v2/result"
)
// Post represents a blog post
// fp-go:Lens
type Post struct {
ID int
UserID int
Title string
}
// State represents accumulated state in do-notation
// fp-go:Lens
type State struct {
User User
Posts []Post
FullName string
Status string
}
// getUser simulates fetching a user by ID
func getUser(id int) ReaderResult[User] {
return func(ctx context.Context) (User, error) {
return User{ID: id, Name: "Alice"}, nil
}
}
// getPosts simulates fetching posts for a user
func getPosts(userID int) ReaderResult[[]Post] {
return func(ctx context.Context) ([]Post, error) {
return []Post{
{ID: 1, UserID: userID, Title: "First Post"},
{ID: 2, UserID: userID, Title: "Second Post"},
}, nil
}
}
// fp-go:Lens
type SimpleState struct {
Value int
}
// ExampleDo demonstrates initializing a do-notation context with an empty state.
// This is the starting point for do-notation style composition, which allows
// imperative-style sequencing of ReaderResult computations while maintaining
// functional purity.
//
// Step-by-step breakdown:
//
// 1. Do(SimpleState{}) - Initialize the do-notation chain with an empty SimpleState.
// This creates a ReaderResult that, when executed, will return the initial state.
// The state acts as an accumulator that will be threaded through subsequent operations.
//
// 2. LetToL(simpleStateLenses.Value, 42) - Set the Value field to the constant 42.
// LetToL uses a lens to focus on a specific field in the state and assign a constant value.
// The "L" suffix indicates this is the lens-based version of LetTo.
// After this step, state.Value = 42.
//
// 3. LetL(simpleStateLenses.Value, N.Add(8)) - Transform the Value field by adding 8.
// LetL uses a lens to focus on a field and apply a transformation function to it.
// N.Add(8) creates a function that adds 8 to its input.
// After this step, state.Value = 42 + 8 = 50.
//
// 4. result(context.Background()) - Execute the composed ReaderResult computation.
// This runs the entire chain with the provided context and returns the final state
// and any error that occurred during execution.
//
// The key insight: Do-notation allows you to build complex stateful computations
// in a declarative, pipeline style while maintaining immutability and composability.
func ExampleDo() {
simpleStateLenses := MakeSimpleStateLenses()
result := F.Pipe2(
Do(SimpleState{}),
LetToL(
simpleStateLenses.Value,
42,
),
LetL(
simpleStateLenses.Value,
N.Add(8),
),
)
state, err := result(context.Background())
fmt.Printf("Value: %d, Error: %v\n", state.Value, err)
// Output: Value: 50, Error: <nil>
}
// ExampleBind demonstrates sequencing a ReaderResult computation and updating
// the state with its result. This is the core operation for do-notation,
// allowing you to chain computations where each step can depend on the
// accumulated state and update it with new values.
//
// Step-by-step breakdown:
//
// 1. Setup lenses for accessing nested state fields:
//
// - userLenses: Provides lenses for User fields (ID, Name)
//
// - stateLenses: Provides lenses for State fields (User, Posts, FullName, Status)
//
// - userIdLens: A composed lens that focuses on state.User.ID
// Created by composing stateLenses.User with userLenses.ID
//
// 2. Do(State{}) - Initialize the do-notation chain with an empty State.
// This creates the initial ReaderResult that will accumulate data through
// subsequent operations.
//
// 3. ApSL(stateLenses.User, getUser(42)) - Fetch user and store in state.User field.
// ApSL (Applicative Set Lens) executes the getUser(42) ReaderResult computation
// and uses the lens to set the result into state.User.
// After this step: state.User = User{ID: 42, Name: "Alice"}
//
// 4. Bind(stateLenses.Posts.Set, F.Flow2(userIdLens.Get, getPosts)) - Fetch posts
// based on the user ID from state and store them in state.Posts.
//
// Breaking down the Bind operation:
// a) First parameter: stateLenses.Posts.Set - A setter function that will update
// the Posts field in the state with the result of the computation.
//
// b) Second parameter: F.Flow2(userIdLens.Get, getPosts) - A composed function that:
//
// - Takes the current state as input
//
// - Extracts the user ID using userIdLens.Get (gets state.User.ID)
//
// - Passes the user ID to getPosts, which returns a ReaderResult[[]Post]
//
// - The result is then set into state.Posts using the setter
//
// After this step: state.Posts = [{ID: 1, UserID: 42, ...}, {ID: 2, UserID: 42, ...}]
//
// 5. result(context.Background()) - Execute the entire computation chain.
// This runs all the ReaderResult operations in sequence, threading the context
// through each step and accumulating the state.
//
// Key concepts demonstrated:
// - Lens composition: Building complex accessors from simple ones
// - Sequential effects: Each step can depend on previous results
// - State accumulation: Building up a complex state object step by step
// - Context threading: The context.Context flows through all operations
// - Error handling: Any error in the chain short-circuits execution
func ExampleBind() {
userLenses := MakeUserLenses()
stateLenses := MakeStateLenses()
userIdLens := F.Pipe1(
stateLenses.User,
lens.Compose[State](userLenses.ID),
)
result := F.Pipe2(
Do(State{}),
ApSL(
stateLenses.User,
getUser(42),
),
Bind(
stateLenses.Posts.Set,
F.Flow2(
userIdLens.Get,
getPosts,
),
),
)
state, err := result(context.Background())
fmt.Printf("User: %s, Posts: %d, Error: %v\n", state.User.Name, len(state.Posts), err)
// Output: User: Alice, Posts: 2, Error: <nil>
}
// fp-go:Lens
type NameState struct {
FirstName string
LastName string
FullName string
}
// ExampleLet demonstrates attaching the result of a pure computation to a state.
// Unlike Bind, Let works with pure functions (not ReaderResult computations).
// This is useful for deriving values from the current state without performing
// any effects.
//
// Step-by-step breakdown:
//
// 1. nameStateLenses := MakeNameStateLenses() - Create lenses for accessing NameState fields.
// Lenses provide a functional way to get and set nested fields in immutable data structures.
// This gives us lenses for FirstName, LastName, and FullName fields.
//
// 2. Do(NameState{FirstName: "John", LastName: "Doe"}) - Initialize the do-notation
// chain with a NameState containing first and last names.
// Initial state: {FirstName: "John", LastName: "Doe", FullName: ""}
//
// 3. Let(nameStateLenses.FullName.Set, func(s NameState) string {...}) - Compute a
// derived value from the current state and update the state with it.
//
// Let takes two parameters:
//
// a) First parameter: nameStateLenses.FullName.Set
// This is a setter function (from the lens) that takes a value and returns a
// function to update the FullName field in the state. The lens-based setter
// ensures immutable updates.
//
// b) Second parameter: func(s NameState) string
// This is a pure "getter" or "computation" function that derives a value from
// the current state. Here it concatenates FirstName and LastName with a space.
// This function has no side effects - it just computes a value.
//
// The Let operation flow:
// - Takes the current state: {FirstName: "John", LastName: "Doe", FullName: ""}
// - Calls the computation function: "John" + " " + "Doe" = "John Doe"
// - Passes "John Doe" to the setter (nameStateLenses.FullName.Set)
// - The setter creates a new state with FullName updated
// After this step: {FirstName: "John", LastName: "Doe", FullName: "John Doe"}
//
// 4. Map(nameStateLenses.FullName.Get) - Transform the final state to extract just
// the FullName field using the lens getter. This changes the result type from
// ReaderResult[NameState] to ReaderResult[string].
//
// 5. result(context.Background()) - Execute the computation chain and return the
// final extracted value ("John Doe") and any error.
//
// Key differences between Let and Bind:
// - Let: Works with pure functions (State -> Value), no effects or errors
// - Bind: Works with effectful computations (State -> ReaderResult[Value])
// - Let: Used for deriving/computing values from existing state
// - Bind: Used for operations that may fail, need context, or have side effects
//
// Use Let when you need to:
// - Compute derived values from existing state fields
// - Transform or combine state values without side effects
// - Add computed fields to your state for later use in the pipeline
// - Perform pure calculations that don't require context or error handling
func ExampleLet() {
nameStateLenses := MakeNameStateLenses()
result := F.Pipe2(
Do(NameState{FirstName: "John", LastName: "Doe"}),
Let(nameStateLenses.FullName.Set,
func(s NameState) string {
return s.FirstName + " " + s.LastName
},
),
Map(nameStateLenses.FullName.Get),
)
fullName, err := result(context.Background())
fmt.Printf("Full Name: %s, Error: %v\n", fullName, err)
// Output: Full Name: John Doe, Error: <nil>
}
// fp-go:Lens
type StatusState struct {
Status string
}
// ExampleLetTo demonstrates attaching a constant value to a state.
// This is a simplified version of Let for when you want to add a constant
// value to the state without computing it.
//
// Step-by-step breakdown:
//
// 1. statusStateLenses := MakeStatusStateLenses() - Create lenses for accessing
// StatusState fields. This provides functional accessors (getters and setters)
// for the Status field.
//
// 2. Do(StatusState{}) - Initialize the do-notation chain with an empty StatusState.
// Initial state: {Status: ""}
//
// 3. LetToL(statusStateLenses.Status, "active") - Set the Status field to the
// constant value "active".
//
// LetToL is the lens-based version of LetTo and takes two parameters:
//
// a) First parameter: statusStateLenses.Status
// This is a lens that focuses on the Status field. The lens provides both
// a getter and setter for the field, enabling immutable updates.
//
// b) Second parameter: "active"
// This is the constant value to assign to the Status field. Unlike Let,
// which takes a function to compute the value, LetToL directly takes the
// value itself.
//
// The LetToL operation:
// - Takes the constant value "active"
// - Uses the lens setter to create a new state with Status = "active"
// - Returns the updated state
// After this step: {Status: "active"}
//
// 4. Map(statusStateLenses.Status.Get) - Transform the final state to extract
// just the Status field using the lens getter. This changes the result type
// from ReaderResult[StatusState] to ReaderResult[string].
//
// 5. result(context.Background()) - Execute the computation chain and return
// the final extracted value ("active") and any error.
//
// Comparison of state-setting operations:
// - LetToL: Set a field to a constant value using a lens (simplest)
// - LetL: Transform a field using a function and a lens
// - Let: Compute a value from state and update using a custom setter
// - Bind: Execute an effectful computation and update state with the result
//
// Use LetToL when you need to:
// - Set a field to a known constant value
// - Initialize state fields with default values
// - Update configuration or status flags
// - Assign literal values without any computation
//
// LetToL is the most straightforward way to set a constant value in do-notation,
// combining the simplicity of LetTo with the power of lenses for type-safe,
// immutable field updates.
func ExampleLetTo() {
statusStateLenses := MakeStatusStateLenses()
result := F.Pipe2(
Do(StatusState{}),
LetToL(
statusStateLenses.Status,
"active",
),
Map(statusStateLenses.Status.Get),
)
status, err := result(context.Background())
fmt.Printf("Status: %s, Error: %v\n", status, err)
// Output: Status: active, Error: <nil>
}
// fp-go:Lens
type UserState struct {
User User
}
// ExampleBindTo demonstrates initializing do-notation by binding a value to a state.
// This is typically used as the first operation after a computation to
// start building up a state structure.
func ExampleBindTo() {
userStatePrisms := MakeUserStatePrisms()
result := F.Pipe1(
getUser(42),
BindToP(userStatePrisms.User),
)
state, err := result(context.Background())
fmt.Printf("User: %s, Error: %v\n", state.User.Name, err)
// Output: User: Alice, Error: <nil>
}
// fp-go:Lens
type ConfigState struct {
Config string
}
// ExampleBindReaderK demonstrates binding a Reader computation (context-dependent
// but error-free) into a ReaderResult do-notation chain.
func ExampleBindReaderK() {
configStateLenses := MakeConfigStateLenses()
// A Reader that extracts a value from context
getConfig := func(ctx context.Context) string {
if val := ctx.Value("config"); val != nil {
return val.(string)
}
return "default"
}
result := F.Pipe1(
Do(ConfigState{}),
BindReaderK(configStateLenses.Config.Set,
func(s ConfigState) Reader[context.Context, string] {
return getConfig
},
),
)
ctx := context.WithValue(context.Background(), "config", "production")
state, err := result(ctx)
fmt.Printf("Config: %s, Error: %v\n", state.Config, err)
// Output: Config: production, Error: <nil>
}
// fp-go:Lens
type NumberState struct {
Number int
}
// ExampleBindEitherK demonstrates binding a Result (Either) computation into
// a ReaderResult do-notation chain. This is useful for integrating pure
// error-handling logic that doesn't need context.
func ExampleBindEitherK() {
numberStateLenses := MakeNumberStateLenses()
// A pure function that returns a Result
parseNumber := func(s NumberState) RES.Result[int] {
return RES.Of(42)
}
result := F.Pipe1(
Do(NumberState{}),
BindEitherK(
numberStateLenses.Number.Set,
parseNumber,
),
)
state, err := result(context.Background())
fmt.Printf("Number: %d, Error: %v\n", state.Number, err)
// Output: Number: 42, Error: <nil>
}
// fp-go:Lens
type DataState struct {
Data string
}
// ExampleBindResultK demonstrates binding an idiomatic Go function (returning
// value and error) into a ReaderResult do-notation chain. This is particularly
// useful for integrating existing Go code that follows the standard (value, error)
// return pattern into functional pipelines.
//
// Step-by-step breakdown:
//
// 1. dataStateLenses := MakeDataStateLenses() - Create lenses for accessing
// DataState fields. This provides functional accessors (getters and setters)
// for the Data field, enabling type-safe, immutable field updates.
//
// 2. fetchData := func(s DataState) (string, error) - Define an idiomatic Go
// function that takes the current state and returns a tuple of (value, error).
//
// IMPORTANT: This function represents a PURE READER COMPOSITION - it reads from
// the state and performs computations that don't require a context.Context.
// This is suitable for:
// - Pure computations that may fail (parsing, validation, calculations)
// - Operations that only depend on the state, not external context
// - Stateless transformations with error handling
// - Synchronous operations that don't need cancellation or timeouts
//
// For EFFECTFUL COMPOSITION (operations that need context), use the full
// ReaderResult type instead: func(context.Context) (Value, error)
// Use ReaderResult when you need:
// - Context cancellation or timeouts
// - Context values (request IDs, trace IDs, etc.)
// - Operations that depend on external context state
// - Async operations that should respect context lifecycle
//
// In this example, fetchData always succeeds with "fetched data", but in real
// code it might perform pure operations like:
// - Parsing or validating data from the state
// - Performing calculations that could fail
// - Calling pure functions from external libraries
// - Data transformations that don't require context
//
// 3. Do(DataState{}) - Initialize the do-notation chain with an empty DataState.
// This creates the initial ReaderResult that will accumulate data through
// subsequent operations.
// Initial state: {Data: ""}
//
// 4. BindResultK(dataStateLenses.Data.Set, fetchData) - Bind the idiomatic Go
// function into the ReaderResult chain.
//
// BindResultK takes two parameters:
//
// a) First parameter: dataStateLenses.Data.Set
// This is a setter function from the lens that will update the Data field
// with the result of the computation. The lens ensures immutable updates.
//
// b) Second parameter: fetchData
// This is the idiomatic Go function (State -> (Value, error)) that will be
// lifted into the ReaderResult context.
//
// The BindResultK operation flow:
// - Takes the current state: {Data: ""}
// - Calls fetchData with the state: fetchData(DataState{})
// - Gets the result tuple: ("fetched data", nil)
// - If error is not nil, short-circuits the chain and returns the error
// - If error is nil, uses the setter to update state.Data with "fetched data"
// - Returns the updated state: {Data: "fetched data"}
// After this step: {Data: "fetched data"}
//
// 5. result(context.Background()) - Execute the computation chain with a context.
// Even though fetchData doesn't use the context, the ReaderResult still needs
// one to maintain the uniform interface. This runs all operations in sequence
// and returns the final state and any error.
//
// Key concepts demonstrated:
// - Integration of idiomatic Go code: BindResultK bridges functional and imperative styles
// - Error propagation: Errors from the Go function automatically propagate through the chain
// - State transformation: The result updates the state using lens-based setters
// - Context independence: The function doesn't need context but still works in ReaderResult
//
// Comparison with other bind operations:
// - BindResultK: For idiomatic Go functions (State -> (Value, error))
// - Bind: For full ReaderResult computations (State -> ReaderResult[Value])
// - BindEitherK: For pure Result/Either values (State -> Result[Value])
// - BindReaderK: For context-dependent functions (State -> Reader[Context, Value])
//
// Use BindResultK when you need to:
// - Integrate existing Go code that returns (value, error)
// - Call functions that may fail but don't need context
// - Perform stateful computations with standard Go error handling
// - Bridge between functional pipelines and imperative Go code
// - Work with libraries that follow Go conventions
//
// Real-world example scenarios:
// - Parsing JSON from a state field: func(s State) (ParsedData, error)
// - Validating user input: func(s State) (ValidatedInput, error)
// - Performing calculations: func(s State) (Result, error)
// - Calling third-party libraries: func(s State) (APIResponse, error)
func ExampleBindResultK() {
dataStateLenses := MakeDataStateLenses()
// An idiomatic Go function returning (value, error)
fetchData := func(s DataState) (string, error) {
return "fetched data", nil
}
result := F.Pipe1(
Do(DataState{}),
BindResultK(
dataStateLenses.Data.Set,
fetchData,
),
)
state, err := result(context.Background())
fmt.Printf("Data: %s, Error: %v\n", state.Data, err)
// Output: Data: fetched data, Error: <nil>
}
// fp-go:Lens
type RequestState struct {
RequestID string
}
// ExampleBindToReader demonstrates converting a Reader computation into a
// ReaderResult and binding it to create an initial state.
func ExampleBindToReader() {
// A Reader that extracts request ID from context
getRequestID := func(ctx context.Context) string {
if val := ctx.Value("requestID"); val != nil {
return val.(string)
}
return "unknown"
}
result := F.Pipe1(
getRequestID,
BindToReader(func(id string) RequestState {
return RequestState{RequestID: id}
}),
)
ctx := context.WithValue(context.Background(), "requestID", "req-123")
state, err := result(ctx)
fmt.Printf("Request ID: %s, Error: %v\n", state.RequestID, err)
// Output: Request ID: req-123, Error: <nil>
}
// fp-go:Lens
type ValueState struct {
Value int
}
// ExampleBindToEither demonstrates converting a Result (Either) into a
// ReaderResult and binding it to create an initial state.
func ExampleBindToEither() {
// A Result value
resultValue := RES.Of(100)
result := F.Pipe1(
resultValue,
BindToEither(func(v int) ValueState {
return ValueState{Value: v}
}),
)
state, err := result(context.Background())
fmt.Printf("Value: %d, Error: %v\n", state.Value, err)
// Output: Value: 100, Error: <nil>
}
// fp-go:Lens
type ResultState struct {
Result string
}
// ExampleBindToResult demonstrates converting an idiomatic Go tuple (value, error)
// into a ReaderResult and binding it to create an initial state.
func ExampleBindToResult() {
// Simulate an idiomatic Go function result
value, err := "success", error(nil)
result := F.Pipe1(
BindToResult(func(v string) ResultState {
return ResultState{Result: v}
}),
func(f func(string, error) ReaderResult[ResultState]) ReaderResult[ResultState] {
return f(value, err)
},
)
state, resultErr := result(context.Background())
fmt.Printf("Result: %s, Error: %v\n", state.Result, resultErr)
// Output: Result: success, Error: <nil>
}
// fp-go:Lens
type EnvState struct {
Environment string
}
// ExampleApReaderS demonstrates applying a Reader computation in applicative style,
// combining it with the current state in a do-notation chain.
func ExampleApReaderS() {
// A Reader that gets environment from context
getEnv := func(ctx context.Context) string {
if val := ctx.Value("env"); val != nil {
return val.(string)
}
return "development"
}
result := F.Pipe1(
Do(EnvState{}),
ApReaderS(
func(env string) Endomorphism[EnvState] {
return func(s EnvState) EnvState {
s.Environment = env
return s
}
},
getEnv,
),
)
ctx := context.WithValue(context.Background(), "env", "staging")
state, err := result(ctx)
fmt.Printf("Environment: %s, Error: %v\n", state.Environment, err)
// Output: Environment: staging, Error: <nil>
}
// fp-go:Lens
type ScoreState struct {
Score int
}
// ExampleApEitherS demonstrates applying a Result (Either) in applicative style,
// combining it with the current state in a do-notation chain.
func ExampleApEitherS() {
// A Result value
scoreResult := RES.Of(95)
result := F.Pipe1(
Do(ScoreState{}),
ApEitherS(
func(score int) Endomorphism[ScoreState] {
return func(s ScoreState) ScoreState {
s.Score = score
return s
}
},
scoreResult,
),
)
state, err := result(context.Background())
fmt.Printf("Score: %d, Error: %v\n", state.Score, err)
// Output: Score: 95, Error: <nil>
}
// fp-go:Lens
type MessageState struct {
Message string
}
// ExampleApResultS demonstrates applying an idiomatic Go tuple (value, error)
// in applicative style, combining it with the current state in a do-notation chain.
func ExampleApResultS() {
// Simulate an idiomatic Go function result
value, err := "Hello, World!", error(nil)
result := F.Pipe1(
Do(MessageState{}),
func(rr ReaderResult[MessageState]) ReaderResult[MessageState] {
return F.Pipe1(
rr,
ApResultS(
func(msg string) Endomorphism[MessageState] {
return func(s MessageState) MessageState {
s.Message = msg
return s
}
},
)(value, err),
)
},
)
state, resultErr := result(context.Background())
fmt.Printf("Message: %s, Error: %v\n", state.Message, resultErr)
// Output: Message: Hello, World!, Error: <nil>
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
"errors"
"fmt"
RES "github.com/IBM/fp-go/v2/result"
)
// ExampleFromEither demonstrates lifting a Result (Either) into a
// The resulting ReaderResult ignores the context and returns the Result value.
func ExampleFromEither() {
res := RES.Of(42)
rr := FromEither(res)
value, err := rr(context.Background())
fmt.Println(value, err)
// Output:
// 42 <nil>
}
// ExampleFromResult demonstrates creating a ReaderResult from a Go-style (value, error) tuple.
// This is useful for converting standard Go error handling into the ReaderResult monad.
func ExampleFromResult() {
rr := FromResult(42, nil)
value, err := rr(context.Background())
fmt.Println(value, err)
// Output:
// 42 <nil>
}
// ExampleFromResult_error demonstrates creating a ReaderResult from an error case.
// The resulting ReaderResult will propagate the error when executed.
func ExampleFromResult_error() {
rr := FromResult(0, errors.New("failed"))
value, err := rr(context.Background())
fmt.Println(value, err != nil)
// Output:
// 0 true
}
// ExampleLeft demonstrates creating a ReaderResult that always fails with an error.
// This is the error constructor for ReaderResult, analogous to Either's Left.
func ExampleLeft() {
rr := Left[int](errors.New("failed"))
value, err := rr(context.Background())
fmt.Println(value, err != nil)
// Output:
// 0 true
}
// ExampleRight demonstrates creating a ReaderResult that always succeeds with a value.
// This is the success constructor for ReaderResult, analogous to Either's Right.
func ExampleRight() {
rr := Right(42)
value, err := rr(context.Background())
fmt.Println(value, err)
// Output:
// 42 <nil>
}
// ExampleOf demonstrates the monadic return/pure operation for
// It creates a ReaderResult that always succeeds with the given value.
func ExampleOf() {
rr := Of(42)
value, err := rr(context.Background())
fmt.Println(value, err)
// Output:
// 42 <nil>
}
// ExampleAsk demonstrates getting the context.Context environment.
// This returns a ReaderResult that provides access to the context itself.
func ExampleAsk() {
rr := Ask()
ctx := context.Background()
value, err := rr(ctx)
fmt.Println(value == ctx, err)
// Output:
// true <nil>
}
// ExampleAsks demonstrates extracting a value from the context using a function.
// This is useful for accessing configuration or other data stored in the context.
func ExampleAsks() {
type Config struct {
Port int
}
getPort := Asks(func(ctx context.Context) int {
// In real code, extract config from context
return 8080
})
value, err := getPort(context.Background())
fmt.Println(value, err)
// Output:
// 8080 <nil>
}

View File

@@ -0,0 +1,107 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/reader"
)
// SequenceReader swaps the order of nested environment parameters when the inner type is a Reader.
//
// It transforms ReaderResult[Reader[R, A]] into a function that takes context.Context first,
// then R, and returns (A, error). This is useful when you have a ReaderResult computation
// that produces a Reader, and you want to sequence the environment dependencies.
//
// Type Parameters:
// - R: The inner Reader's environment type
// - A: The final result type
//
// Parameters:
// - ma: A ReaderResult that produces a Reader[R, A]
//
// Returns:
// - A Kleisli arrow that takes context.Context and R to produce (A, error)
//
// Example:
//
// type Config struct {
// DatabaseURL string
// }
//
// // Returns a ReaderResult that produces a Reader
// getDBReader := func(ctx context.Context) (reader.Reader[Config, string], error) {
// return func(cfg Config) string {
// return cfg.DatabaseURL
// }, nil
// }
//
// // Sequence the environments: context.Context -> Config -> string
// sequenced := readerresult.SequenceReader[Config, string](getDBReader)
// result, err := sequenced(ctx)(config)
//
//go:inline
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) Kleisli[R, A] {
return WithContextK(RR.SequenceReader(ma))
}
// TraverseReader combines SequenceReader with a Kleisli arrow transformation.
//
// It takes a Reader Kleisli arrow (a function from A to Reader[R, B]) and returns
// a function that transforms ReaderResult[A] into a Kleisli arrow from context.Context
// and R to B. This is useful for transforming values within a ReaderResult while
// introducing an additional Reader dependency.
//
// Type Parameters:
// - R: The Reader's environment type
// - A: The input type
// - B: The output type
//
// Parameters:
// - f: A Kleisli arrow that transforms A into Reader[R, B]
//
// Returns:
// - A function that transforms ReaderResult[A] into a Kleisli arrow from context.Context and R to B
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // A Kleisli arrow that uses Config to transform int to string
// formatWithConfig := func(n int) reader.Reader[Config, string] {
// return func(cfg Config) string {
// return fmt.Sprintf("Value: %d", n * cfg.Multiplier)
// }
// }
//
// // Create a ReaderResult[int]
// getValue := readerresult.Of[int](42)
//
// // Traverse: transform the int using the Reader Kleisli arrow
// traversed := readerresult.TraverseReader[Config](formatWithConfig)(getValue)
// result, err := traversed(ctx)(Config{Multiplier: 2})
// // result == "Value: 84"
//
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderResult[A]) Kleisli[R, B] {
return RR.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,134 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// From0 converts a context-taking function into a thunk that returns a ReaderResult.
//
// Unlike Curry0 which returns a ReaderResult directly, From0 returns a function
// that when called produces a ReaderResult. This is useful for lazy evaluation.
//
// Type Parameters:
// - A: The return value type
//
// Parameters:
// - f: A function that takes context.Context and returns (A, error)
//
// Returns:
// - A thunk (function with no parameters) that returns ReaderResult[A]
//
// Example:
//
// func getConfig(ctx context.Context) (Config, error) {
// return Config{Port: 8080}, nil
// }
// thunk := readerresult.From0(getConfig)
// rr := thunk() // Create the ReaderResult
// config, err := rr(ctx) // Execute it
//
//go:inline
func From0[A any](f func(context.Context) (A, error)) func() ReaderResult[A] {
return RR.From0(f)
}
// From1 converts a function with one parameter into an uncurried ReaderResult-returning function.
//
// Unlike Curry1 which returns a curried function, From1 returns a function that takes
// all parameters at once (except context). This is more convenient for direct calls.
//
// Type Parameters:
// - T1: The parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1) and returns (A, error)
//
// Returns:
// - A function that takes T1 and returns ReaderResult[A]
//
// Example:
//
// func getUser(ctx context.Context, id int) (User, error) {
// return User{ID: id}, nil
// }
// getUserRR := readerresult.From1(getUser)
// rr := getUserRR(42)
// user, err := rr(ctx)
//
//go:inline
func From1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A] {
return RR.From1(f)
}
// From2 converts a function with two parameters into an uncurried ReaderResult-returning function.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2) and returns (A, error)
//
// Returns:
// - A function that takes (T1, T2) and returns ReaderResult[A]
//
// Example:
//
// func updateUser(ctx context.Context, id int, name string) (User, error) {
// return User{ID: id, Name: name}, nil
// }
// updateUserRR := readerresult.From2(updateUser)
// rr := updateUserRR(42, "Alice")
// user, err := rr(ctx)
//
//go:inline
func From2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1, T2) ReaderResult[A] {
return RR.From2(f)
}
// From3 converts a function with three parameters into an uncurried ReaderResult-returning function.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
// Returns:
// - A function that takes (T1, T2, T3) and returns ReaderResult[A]
//
// Example:
//
// func createPost(ctx context.Context, userID int, title, body string) (Post, error) {
// return Post{UserID: userID, Title: title, Body: body}, nil
// }
// createPostRR := readerresult.From3(createPost)
// rr := createPostRR(42, "Title", "Body")
// post, err := rr(ctx)
//
//go:inline
func From3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1, T2, T3) ReaderResult[A] {
return RR.From3(f)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
package readerresult
//go:generate go run ../../../main.go lens --dir . --filename gen_lens.go

View File

@@ -0,0 +1,120 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
M "github.com/IBM/fp-go/v2/monoid"
)
// AlternativeMonoid creates a Monoid for ReaderResult using the Alternative semantics.
//
// The Alternative semantics means that the monoid operation tries the first computation,
// and if it fails, tries the second one. The empty element is a computation that always fails.
// The inner values are combined using the provided monoid when both computations succeed.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - m: A Monoid[A] for combining successful values
//
// Returns:
// - A Monoid[ReaderResult[A]] with Alternative semantics
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Monoid for integers with addition
// intMonoid := monoid.MonoidSum[int]()
// rrMonoid := readerresult.AlternativeMonoid(intMonoid)
//
// rr1 := readerresult.Right(10)
// rr2 := readerresult.Right(20)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (30, nil)
//
//go:inline
func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
return RR.AlternativeMonoid[context.Context](m)
}
// AltMonoid creates a Monoid for ReaderResult using Alt semantics with a custom zero.
//
// The Alt semantics means that the monoid operation tries the first computation,
// and if it fails, tries the second one. The provided zero is used as the empty element.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - zero: A lazy ReaderResult[A] to use as the empty element
//
// Returns:
// - A Monoid[ReaderResult[A]] with Alt semantics
//
// Example:
//
// zero := func() readerresult.ReaderResult[int] {
// return readerresult.Left[int](errors.New("empty"))
// }
// rrMonoid := readerresult.AltMonoid(zero)
//
// rr1 := readerresult.Left[int](errors.New("failed"))
// rr2 := readerresult.Right(42)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (42, nil) - uses second on first failure
//
//go:inline
func AltMonoid[A any](zero Lazy[ReaderResult[A]]) Monoid[A] {
return RR.AltMonoid(zero)
}
// ApplicativeMonoid creates a Monoid for ReaderResult using Applicative semantics.
//
// The Applicative semantics means that both computations are executed independently,
// and their results are combined using the provided monoid. If either fails, the
// entire operation fails.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - m: A Monoid[A] for combining successful values
//
// Returns:
// - A Monoid[ReaderResult[A]] with Applicative semantics
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Monoid for integers with addition
// intMonoid := monoid.MonoidSum[int]()
// rrMonoid := readerresult.ApplicativeMonoid(intMonoid)
//
// rr1 := readerresult.Right(10)
// rr2 := readerresult.Right(20)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (30, nil)
//
//go:inline
func ApplicativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
return RR.ApplicativeMonoid[context.Context](m)
}

View File

@@ -0,0 +1,692 @@
// 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 readerresult
import (
"context"
"sync"
"time"
RS "github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/option"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/idiomatic/result"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
// FromEither lifts a Result (Either[error, A]) into a ReaderResult.
//
// The resulting ReaderResult ignores the context.Context environment and simply
// returns the Result value. This is useful for converting existing Result values
// into the ReaderResult monad for composition with other ReaderResult operations.
//
// Type Parameters:
// - A: The success value type
//
// Parameters:
// - e: A Result[A] (Either[error, A]) to lift
//
// Returns:
// - A ReaderResult[A] that ignores the context and returns the Result
//
//go:inline
func FromEither[A any](e Result[A]) ReaderResult[A] {
return RR.FromEither[context.Context](e)
}
// FromResult creates a ReaderResult from a Go-style (value, error) tuple.
//
// This is a convenience function for converting standard Go error handling
// into the ReaderResult monad. The resulting ReaderResult ignores the context.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value
// - err: The error (nil for success)
//
// Returns:
// - A ReaderResult[A] that returns the given value and error
//
//go:inline
func FromResult[A any](a A, err error) ReaderResult[A] {
return RR.FromResult[context.Context](a, err)
}
//go:inline
func RightReader[A any](rdr Reader[context.Context, A]) ReaderResult[A] {
return RR.RightReader(rdr)
}
//go:inline
func LeftReader[A, R any](l Reader[context.Context, error]) ReaderResult[A] {
return RR.LeftReader[A](l)
}
// Left creates a ReaderResult that always fails with the given error.
//
// This is the error constructor for ReaderResult, analogous to Either's Left.
// The resulting computation ignores the context and immediately returns the error.
//
// Type Parameters:
// - A: The success type (for type inference)
//
// Parameters:
// - err: The error to return
//
// Returns:
// - A ReaderResult[A] that always fails with the given error
//
//go:inline
func Left[A any](err error) ReaderResult[A] {
return RR.Left[context.Context, A](err)
}
// Right creates a ReaderResult that always succeeds with the given value.
//
// This is the success constructor for ReaderResult, analogous to Either's Right.
// The resulting computation ignores the context and immediately returns the value.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value to return
//
// Returns:
// - A ReaderResult[A] that always succeeds with the given value
//
//go:inline
func Right[A any](a A) ReaderResult[A] {
return RR.Right[context.Context](a)
}
// FromReader lifts a Reader into a ReaderResult that always succeeds.
//
// The Reader computation is executed and its result is wrapped in a successful Result.
// This is useful for incorporating Reader computations into ReaderResult pipelines.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - r: A Reader[context.Context, A] to lift
//
// Returns:
// - A ReaderResult[A] that executes the Reader and always succeeds
//
//go:inline
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
return RR.FromReader(r)
}
//go:inline
func FromReaderResult[A any](r RS.ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return either.Unwrap(r(ctx))
}
}
//go:inline
func ToReaderResult[A any](r ReaderResult[A]) RS.ReaderResult[A] {
return func(ctx context.Context) Result[A] {
return either.TryCatchError(r(ctx))
}
}
// MonadMap transforms the success value of a ReaderResult using the given function.
//
// If the ReaderResult fails, the error is propagated unchanged. This is the
// Functor's map operation for ReaderResult.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - fa: The ReaderResult to transform
// - f: The transformation function
//
// Returns:
// - A ReaderResult[B] with the transformed value
//
//go:inline
func MonadMap[A, B any](fa ReaderResult[A], f func(A) B) ReaderResult[B] {
return RR.MonadMap(fa, f)
}
// Map is the curried version of MonadMap, useful for function composition.
//
// It returns an Operator that can be used in pipelines with F.Pipe.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: The transformation function
//
// Returns:
// - An Operator that transforms ReaderResult[A] to ReaderResult[B]
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return RR.Map[context.Context](f)
}
// MonadChain sequences two ReaderResult computations where the second depends on the first.
//
// This is the monadic bind operation (flatMap). If the first computation fails,
// the error is propagated and the second computation is not executed. Both
// computations share the same context.Context environment.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - ma: The first ReaderResult computation
// - f: A Kleisli arrow that produces the second computation based on the first's result
//
// Returns:
// - A ReaderResult[B] representing the sequenced computation
//
//go:inline
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChain(ma, WithContextK(f))
}
// Chain is the curried version of MonadChain, useful for function composition.
//
// It returns an Operator that can be used in pipelines with F.Pipe.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Kleisli arrow for the second computation
//
// Returns:
// - An Operator that chains ReaderResult computations
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return RR.Chain(WithContextK(f))
}
// Of creates a ReaderResult that always succeeds with the given value.
//
// This is an alias for Right and represents the Applicative's pure/return operation.
// The resulting computation ignores the context and immediately returns the value.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value to wrap
//
// Returns:
// - A ReaderResult[A] that always succeeds with the given value
//
//go:inline
func Of[A any](a A) ReaderResult[A] {
return RR.Of[context.Context](a)
}
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
//
// This is the Applicative's ap operation. Both computations are executed concurrently
// using goroutines, and the context is shared between them. If either computation fails,
// the entire operation fails. If the context is cancelled, the operation is aborted.
//
// The concurrent execution allows for parallel independent computations, which can
// improve performance when both operations involve I/O or other blocking operations.
//
// Type Parameters:
// - B: The result type after applying the function
// - A: The input type to the function
//
// Parameters:
// - fab: A ReaderResult containing a function from A to B
// - fa: A ReaderResult containing a value of type A
//
// Returns:
// - A ReaderResult[B] that applies the function to the value
//
// Example:
//
// // Create a function wrapped in ReaderResult
// addTen := readerresult.Right(func(n int) int {
// return n + 10
// })
//
// // Create a value wrapped in ReaderResult
// value := readerresult.Right(32)
//
// // Apply the function to the value
// result := readerresult.MonadAp(addTen, value)
// output, err := result(ctx) // Returns (42, nil)
//
// Error Handling:
//
// // If the function fails
// failedFn := readerresult.Left[func(int) int](errors.New("function error"))
// result := readerresult.MonadAp(failedFn, value)
// _, err := result(ctx) // Returns function error
//
// // If the value fails
// failedValue := readerresult.Left[int](errors.New("value error"))
// result := readerresult.MonadAp(addTen, failedValue)
// _, err := result(ctx) // Returns value error
//
// Context Cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
// result := readerresult.MonadAp(addTen, value)
// _, err := result(ctx) // Returns context cancellation error
func MonadAp[B, A any](fab ReaderResult[func(A) B], fa ReaderResult[A]) ReaderResult[B] {
return func(ctx context.Context) (B, error) {
if ctx.Err() != nil {
return result.Left[B](context.Cause(ctx))
}
var wg sync.WaitGroup
wg.Add(1)
cancelCtx, cancelFct := context.WithCancel(ctx)
defer cancelFct()
var a A
var aerr error
go func() {
defer wg.Done()
a, aerr = fa(cancelCtx)
if aerr != nil {
cancelFct()
}
}()
ab, aberr := fab(cancelCtx)
if aberr != nil {
cancelFct()
wg.Wait()
return result.Left[B](aberr)
}
wg.Wait()
if aerr != nil {
return result.Left[B](aerr)
}
return result.Of(ab(a))
}
}
// Ap is the curried version of MonadAp, useful for function composition.
//
// It fixes the value argument and returns an Operator that can be applied
// to a ReaderResult containing a function. This is particularly useful in
// pipelines where you want to apply a fixed value to various functions.
//
// Type Parameters:
// - B: The result type after applying the function
// - A: The input type to the function
//
// Parameters:
// - fa: A ReaderResult containing a value of type A
//
// Returns:
// - An Operator that applies the value to a function wrapped in ReaderResult
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// value := readerresult.Right(32)
// addTen := readerresult.Right(N.Add(10))
//
// result := F.Pipe1(
// addTen,
// readerresult.Ap[int](value),
// )
// output, err := result(ctx) // Returns (42, nil)
//
//go:inline
func Ap[B, A any](fa ReaderResult[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadAp[B, A], fa)
}
//go:inline
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
return WithContextK(RR.FromPredicate[context.Context](pred, onFalse))
}
//go:inline
func Fold[A, B any](onLeft reader.Kleisli[context.Context, error, B], onRight reader.Kleisli[context.Context, A, B]) func(ReaderResult[A]) Reader[context.Context, B] {
return RR.Fold(onLeft, onRight)
}
//go:inline
func GetOrElse[A any](onLeft reader.Kleisli[context.Context, error, A]) func(ReaderResult[A]) Reader[context.Context, A] {
return RR.GetOrElse(onLeft)
}
//go:inline
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
return RR.OrElse(WithContextK(onLeft))
}
//go:inline
func OrLeft[A any](onLeft reader.Kleisli[context.Context, error, error]) Operator[A, A] {
return RR.OrLeft[A](onLeft)
}
// Ask retrieves the current context.Context environment.
//
// This is the Reader's ask operation, which provides access to the environment.
// It always succeeds and returns the context that was passed in.
//
// Returns:
// - A ReaderResult[context.Context] that returns the environment
//
//go:inline
func Ask() ReaderResult[context.Context] {
return RR.Ask[context.Context]()
}
// Asks extracts a value from the context.Context environment using a Reader function.
//
// This is useful for accessing specific parts of the environment. The Reader
// function is applied to the context, and the result is wrapped in a successful ReaderResult.
//
// Type Parameters:
// - A: The extracted value type
//
// Parameters:
// - r: A Reader function that extracts a value from the context
//
// Returns:
// - A ReaderResult[A] that extracts and returns the value
//
//go:inline
func Asks[A any](r Reader[context.Context, A]) ReaderResult[A] {
return RR.Asks(r)
}
//go:inline
func MonadChainEitherK[A, B any](ma ReaderResult[A], f RES.Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChainEitherK(ma, f)
}
//go:inline
func ChainEitherK[A, B any](f RES.Kleisli[A, B]) Operator[A, B] {
return RR.ChainEitherK[context.Context](f)
}
//go:inline
func MonadChainReaderK[A, B any](ma ReaderResult[A], f result.Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChainReaderK(ma, f)
}
//go:inline
func ChainReaderK[A, B any](f result.Kleisli[A, B]) Operator[A, B] {
return RR.ChainReaderK[context.Context](f)
}
//go:inline
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
return RR.ChainOptionK[context.Context, A, B](onNone)
}
// Flatten removes one level of ReaderResult nesting.
//
// This is equivalent to Chain with the identity function. It's useful when you have
// a ReaderResult that produces another ReaderResult and want to collapse them into one.
//
// Type Parameters:
// - A: The inner value type
//
// Parameters:
// - mma: A nested ReaderResult[ReaderResult[A]]
//
// Returns:
// - A flattened ReaderResult[A]
//
//go:inline
func Flatten[A any](mma ReaderResult[ReaderResult[A]]) ReaderResult[A] {
return RR.Flatten(mma)
}
//go:inline
func MonadBiMap[A, B any](fa ReaderResult[A], f Endomorphism[error], g func(A) B) ReaderResult[B] {
return RR.MonadBiMap(fa, f, g)
}
//go:inline
func BiMap[A, B any](f Endomorphism[error], g func(A) B) Operator[A, B] {
return RR.BiMap[context.Context](f, g)
}
// Read executes a ReaderResult by providing it with a context.Context.
//
// This is the elimination form for ReaderResult - it "runs" the computation
// by supplying the required environment, producing a (value, error) tuple.
//
// Type Parameters:
// - A: The result value type
//
// Parameters:
// - ctx: The context.Context environment to provide
//
// Returns:
// - A function that executes a ReaderResult[A] and returns (A, error)
//
//go:inline
func Read[A any](ctx context.Context) func(ReaderResult[A]) (A, error) {
return RR.Read[A](ctx)
}
//go:inline
func MonadFlap[A, B any](fab ReaderResult[func(A) B], a A) ReaderResult[B] {
return RR.MonadFlap(fab, a)
}
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return RR.Flap[context.Context, B](a)
}
//go:inline
func MonadMapLeft[A any](fa ReaderResult[A], f Endomorphism[error]) ReaderResult[A] {
return RR.MonadMapLeft(fa, f)
}
//go:inline
func MapLeft[A any](f Endomorphism[error]) Operator[A, A] {
return RR.MapLeft[context.Context, A](f)
}
//go:inline
func MonadAlt[A any](first ReaderResult[A], second Lazy[ReaderResult[A]]) ReaderResult[A] {
return RR.MonadAlt(first, second)
}
//go:inline
func Alt[A any](second Lazy[ReaderResult[A]]) Operator[A, A] {
return RR.Alt(second)
}
// Local transforms the context.Context environment before passing it to a ReaderResult computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerresult.Asks(func(ctx context.Context) string {
// return ctx.Value(userKey).(string)
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// user, err := result(context.Background()) // Returns ("Alice", nil)
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) (A, error) {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)
}
}
}
// WithTimeout adds a timeout to the context for a ReaderResult computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderResult is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerresult.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerresult.WithTimeout[Data](5*time.Second),
// )
// _, err := result(context.Background()) // Returns context.DeadlineExceeded after 5s
//
// Successful Example:
//
// quickFetch := readerresult.Right(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerresult.WithTimeout[Data](5*time.Second),
// )
// data, err := result(context.Background()) // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderResult computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 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 readerresult
import (
RS "github.com/IBM/fp-go/v2/context/readerresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
R "github.com/IBM/fp-go/v2/retry"
)
// Retrying retries a ReaderResult computation according to a retry policy with context awareness.
//
// This is the idiomatic wrapper around the functional [github.com/IBM/fp-go/v2/context/readerresult.Retrying]
// function. It provides a more Go-friendly API by working with (value, error) tuples instead of Result types.
//
// The function implements a retry mechanism for operations that depend on a [context.Context] and can fail.
// It respects context cancellation, meaning that if the context is cancelled during retry delays, the
// operation will stop immediately and return the cancellation error.
//
// The retry loop will continue until one of the following occurs:
// - The action succeeds and the check function returns false (no retry needed)
// - The retry policy returns None (retry limit reached)
// - The check function returns false (indicating success or a non-retryable failure)
// - The context is cancelled (returns context.Canceled or context.DeadlineExceeded)
//
// Parameters:
//
// - policy: A RetryPolicy that determines when and how long to wait between retries.
// The policy receives a RetryStatus on each iteration and returns an optional delay.
// If it returns None, retrying stops. Common policies include LimitRetries,
// ExponentialBackoff, and CapDelay from the retry package.
//
// - action: A Kleisli arrow that takes a RetryStatus and returns a ReaderResult[A].
// This function is called on each retry attempt and receives information about the
// current retry state (iteration number, cumulative delay, etc.). The action depends
// on a context.Context and produces (A, error). The context passed to the action
// will be the same context used for retry delays, so cancellation is properly propagated.
//
// - check: A predicate function that examines the result value and error, returning true
// if the operation should be retried, or false if it should stop. This allows you to
// distinguish between retryable failures (e.g., network timeouts) and permanent
// failures (e.g., invalid input). The function receives both the value and error from
// the action's result. Note that context cancellation errors will automatically stop
// retrying regardless of this function's return value.
//
// Returns:
//
// A ReaderResult[A] that, when executed with a context, will perform the retry
// logic with context cancellation support and return the final (value, error) tuple.
//
// Type Parameters:
// - A: The type of the success value
//
// Context Cancellation:
//
// The retry mechanism respects context cancellation in two ways:
// 1. During retry delays: If the context is cancelled while waiting between retries,
// the operation stops immediately and returns the context error.
// 2. During action execution: If the action itself checks the context and returns
// an error due to cancellation, the retry loop will stop (assuming the check
// function doesn't force a retry on context errors).
//
// Implementation Details:
//
// This function wraps the functional [github.com/IBM/fp-go/v2/context/readerresult.Retrying]
// by converting between the idiomatic (value, error) tuple representation and the functional
// Result[A] representation. The conversion is handled by ToReaderResult and FromReaderResult,
// ensuring seamless integration with the underlying retry mechanism that uses delayWithCancel
// to properly handle context cancellation during delays.
//
// Example:
//
// // Create a retry policy: exponential backoff with a cap, limited to 5 retries
// policy := retry.Monoid.Concat(
// retry.LimitRetries(5),
// retry.CapDelay(10*time.Second, retry.ExponentialBackoff(100*time.Millisecond)),
// )
//
// // Action that fetches data, with retry status information
// fetchData := func(status retry.RetryStatus) ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// // Check if context is cancelled
// if ctx.Err() != nil {
// return "", ctx.Err()
// }
// // Simulate an HTTP request that might fail
// if status.IterNumber < 3 {
// return "", fmt.Errorf("temporary error")
// }
// return "success", nil
// }
// }
//
// // Check function: retry on any error except context cancellation
// shouldRetry := func(val string, err error) bool {
// return err != nil && !errors.Is(err, context.Canceled)
// }
//
// // Create the retrying computation
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// result, err := retryingFetch(ctx)
//
// See also:
// - retry.RetryPolicy for available retry policies
// - retry.RetryStatus for information passed to the action
// - context.Context for context cancellation semantics
// - github.com/IBM/fp-go/v2/context/readerresult.Retrying for the underlying functional implementation
//
//go:inline
func Retrying[A any](
policy R.RetryPolicy,
action Kleisli[R.RetryStatus, A],
check func(A, error) bool,
) ReaderResult[A] {
return F.Pipe1(
RS.Retrying(
policy,
F.Flow2(
action,
ToReaderResult,
),
func(a Result[A]) bool {
return check(result.Unwrap(a))
},
),
FromReaderResult,
)
}

View File

@@ -0,0 +1,482 @@
// 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 readerresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
R "github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
// Helper function to create a test retry policy
func testRetryPolicy() R.RetryPolicy {
return R.Monoid.Concat(
R.LimitRetries(5),
R.CapDelay(1*time.Second, R.ExponentialBackoff(10*time.Millisecond)),
)
}
// TestRetrying_SuccessOnFirstAttempt tests that Retrying succeeds immediately
// when the action succeeds on the first attempt.
func TestRetrying_SuccessOnFirstAttempt(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "success", nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, "success", result)
}
// TestRetrying_SuccessAfterRetries tests that Retrying eventually succeeds
// after a few failed attempts.
func TestRetrying_SuccessAfterRetries(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
// Fail on first 3 attempts, succeed on 4th
if status.IterNumber < 3 {
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
return fmt.Sprintf("success on attempt %d", status.IterNumber), nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, "success on attempt 3", result)
}
// TestRetrying_ExhaustsRetries tests that Retrying stops after the retry limit
// is reached and returns the last error.
func TestRetrying_ExhaustsRetries(t *testing.T) {
policy := R.LimitRetries(3)
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.Error(t, err)
assert.Equal(t, "", result)
assert.Equal(t, "attempt 3 failed", err.Error())
}
// TestRetrying_ActionChecksContextCancellation tests that actions can check
// the context and return early if it's cancelled.
func TestRetrying_ActionChecksContextCancellation(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Check context at the start of the action
if ctx.Err() != nil {
return "", ctx.Err()
}
// Simulate work that might take time
time.Sleep(10 * time.Millisecond)
// Check context again after work
if ctx.Err() != nil {
return "", ctx.Err()
}
// Always fail to trigger retries
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
// Don't retry on context errors
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return err != nil
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel after a short time
ctx, cancel := context.WithCancel(context.Background())
// Start the retry operation in a goroutine
type result struct {
val string
err error
}
resultChan := make(chan result, 1)
go func() {
val, err := retrying(ctx)
resultChan <- result{val, err}
}()
// Cancel the context after allowing a couple attempts
time.Sleep(50 * time.Millisecond)
cancel()
// Wait for the result
res := <-resultChan
// Should have stopped due to context cancellation
assert.Error(t, res.err)
// Should have stopped early (not all 10 attempts)
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context cancellation")
}
// TestRetrying_ContextCancelledBeforeStart tests that if the context is already
// cancelled before starting, the operation fails immediately.
func TestRetrying_ContextCancelledBeforeStart(t *testing.T) {
policy := testRetryPolicy()
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return "", ctx.Err()
}
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
// Don't retry on context errors
if err != nil && errors.Is(err, context.Canceled) {
return false
}
return err != nil
}
retrying := Retrying(policy, action, check)
// Create an already-cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := retrying(ctx)
assert.Error(t, err)
assert.True(t, errors.Is(err, context.Canceled))
// Should have attempted at most once
assert.LessOrEqual(t, attemptCount, 1)
}
// TestRetrying_ContextTimeoutInAction tests that actions respect context deadlines.
func TestRetrying_ContextTimeoutInAction(t *testing.T) {
policy := R.LimitRetries(10)
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Check context before doing work
if ctx.Err() != nil {
return "", ctx.Err()
}
// Simulate some work
time.Sleep(50 * time.Millisecond)
// Check context after work
if ctx.Err() != nil {
return "", ctx.Err()
}
// Always fail to trigger retries
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
check := func(val string, err error) bool {
// Don't retry on context errors
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
return false
}
return err != nil
}
retrying := Retrying(policy, action, check)
// Create a context with a short timeout
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
startTime := time.Now()
_, err := retrying(ctx)
elapsed := time.Since(startTime)
assert.Error(t, err)
// Should have stopped before completing all 10 retries
assert.Less(t, attemptCount, 10, "Should stop retrying when action detects context timeout")
// Should have stopped around the timeout duration
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop soon after timeout")
}
// TestRetrying_CheckFunctionStopsRetry tests that the check function can
// stop retrying even when errors occur.
func TestRetrying_CheckFunctionStopsRetry(t *testing.T) {
policy := testRetryPolicy()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
if status.IterNumber == 0 {
return "", fmt.Errorf("retryable error")
}
return "", fmt.Errorf("permanent error")
}
}
// Only retry on "retryable error"
check := func(val string, err error) bool {
return err != nil && err.Error() == "retryable error"
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
_, err := retrying(ctx)
assert.Error(t, err)
assert.Equal(t, "permanent error", err.Error())
}
// TestRetrying_ExponentialBackoff tests that exponential backoff is applied.
func TestRetrying_ExponentialBackoff(t *testing.T) {
// Use a policy with measurable delays
policy := R.Monoid.Concat(
R.LimitRetries(3),
R.ExponentialBackoff(50*time.Millisecond),
)
startTime := time.Now()
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
if status.IterNumber < 2 {
return "", fmt.Errorf("retry")
}
return "success", nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
elapsed := time.Since(startTime)
assert.NoError(t, err)
assert.Equal(t, "success", result)
// With exponential backoff starting at 50ms:
// Iteration 0: no delay
// Iteration 1: 50ms delay
// Iteration 2: 100ms delay
// Total should be at least 150ms
assert.GreaterOrEqual(t, elapsed, 150*time.Millisecond)
}
// TestRetrying_ContextValuePropagation tests that context values are properly
// propagated through the retry mechanism.
func TestRetrying_ContextValuePropagation(t *testing.T) {
policy := R.LimitRetries(2)
type contextKey string
const requestIDKey contextKey = "requestID"
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
// Extract value from context
requestID, ok := ctx.Value(requestIDKey).(string)
if !ok {
return "", fmt.Errorf("missing request ID")
}
if status.IterNumber < 1 {
return "", fmt.Errorf("retry needed")
}
return fmt.Sprintf("processed request %s", requestID), nil
}
}
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
// Create context with a value
ctx := context.WithValue(context.Background(), requestIDKey, "12345")
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, "processed request 12345", result)
}
// TestRetrying_RetryStatusProgression tests that the RetryStatus is properly
// updated on each iteration.
func TestRetrying_RetryStatusProgression(t *testing.T) {
policy := testRetryPolicy()
var iterations []uint
action := func(status R.RetryStatus) ReaderResult[int] {
return func(ctx context.Context) (int, error) {
iterations = append(iterations, status.IterNumber)
if status.IterNumber < 3 {
return 0, fmt.Errorf("retry")
}
return int(status.IterNumber), nil
}
}
check := func(val int, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
ctx := context.Background()
result, err := retrying(ctx)
assert.NoError(t, err)
assert.Equal(t, 3, result)
// Should have attempted iterations 0, 1, 2, 3
assert.Equal(t, []uint{0, 1, 2, 3}, iterations)
}
// TestRetrying_ContextCancelledDuringDelay tests that the retry operation
// stops immediately when the context is cancelled during a retry delay,
// even if there are still retries remaining according to the policy.
func TestRetrying_ContextCancelledDuringDelay(t *testing.T) {
// Use a policy with significant delays to ensure we can cancel during the delay
policy := R.Monoid.Concat(
R.LimitRetries(10),
R.ConstantDelay(200*time.Millisecond),
)
attemptCount := 0
action := func(status R.RetryStatus) ReaderResult[string] {
return func(ctx context.Context) (string, error) {
attemptCount++
// Always fail to trigger retries
return "", fmt.Errorf("attempt %d failed", status.IterNumber)
}
}
// Always retry on errors (don't check for context cancellation in check function)
check := func(val string, err error) bool {
return err != nil
}
retrying := Retrying(policy, action, check)
// Create a context that we'll cancel during the retry delay
ctx, cancel := context.WithCancel(context.Background())
// Start the retry operation in a goroutine
type result struct {
val string
err error
}
resultChan := make(chan result, 1)
startTime := time.Now()
go func() {
val, err := retrying(ctx)
resultChan <- result{val, err}
}()
// Wait for the first attempt to complete and the delay to start
time.Sleep(50 * time.Millisecond)
// Cancel the context during the retry delay
cancel()
// Wait for the result
res := <-resultChan
elapsed := time.Since(startTime)
// Should have stopped due to context cancellation
assert.Error(t, res.err)
// Should have attempted only once or twice (not all 10 attempts)
// because the context was cancelled during the delay
assert.LessOrEqual(t, attemptCount, 2, "Should stop retrying when context is cancelled during delay")
// Should have stopped quickly after cancellation, not waiting for all delays
// With 10 retries and 200ms delays, it would take ~2 seconds without cancellation
// With cancellation during first delay, it should complete in well under 500ms
assert.Less(t, elapsed, 500*time.Millisecond, "Should stop immediately when context is cancelled during delay")
// When context is cancelled during the delay, the retry mechanism
// detects the cancellation and returns a context error
assert.True(t, errors.Is(res.err, context.Canceled), "Should return context.Canceled when cancelled during delay")
}

View File

@@ -0,0 +1,133 @@
// 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 readerresult
import (
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
T "github.com/IBM/fp-go/v2/tuple"
)
// SequenceT1 wraps a single ReaderResult in a Tuple1.
//
// This is mainly for consistency with the other SequenceT functions.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: A ReaderResult[A]
//
// Returns:
// - A ReaderResult[Tuple1[A]]
//
// Example:
//
// rr := readerresult.Right(42)
// result := readerresult.SequenceT1(rr)
// tuple, err := result(ctx) // Returns (Tuple1{42}, nil)
//
//go:inline
func SequenceT1[A any](a ReaderResult[A]) ReaderResult[T.Tuple1[A]] {
return RR.SequenceT1(a)
}
// SequenceT2 combines two independent ReaderResult computations into a tuple.
//
// Both computations are executed with the same context. If either fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
//
// Returns:
// - A ReaderResult[Tuple2[A, B]] containing both results
//
// Example:
//
// getUser := readerresult.Right(User{ID: 1})
// getConfig := readerresult.Right(Config{Port: 8080})
// result := readerresult.SequenceT2(getUser, getConfig)
// tuple, err := result(ctx) // Returns (Tuple2{User, Config}, nil)
//
//go:inline
func SequenceT2[A, B any](
a ReaderResult[A],
b ReaderResult[B],
) ReaderResult[T.Tuple2[A, B]] {
return RR.SequenceT2(a, b)
}
// SequenceT3 combines three independent ReaderResult computations into a tuple.
//
// All computations are executed with the same context. If any fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
// - C: The third value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
//
// Returns:
// - A ReaderResult[Tuple3[A, B, C]] containing all three results
//
//go:inline
func SequenceT3[A, B, C any](
a ReaderResult[A],
b ReaderResult[B],
c ReaderResult[C],
) ReaderResult[T.Tuple3[A, B, C]] {
return RR.SequenceT3(a, b, c)
}
// SequenceT4 combines four independent ReaderResult computations into a tuple.
//
// All computations are executed with the same context. If any fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
// - C: The third value type
// - D: The fourth value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
// - d: The fourth ReaderResult
//
// Returns:
// - A ReaderResult[Tuple4[A, B, C, D]] containing all four results
//
//go:inline
func SequenceT4[A, B, C, D any](
a ReaderResult[A],
b ReaderResult[B],
c ReaderResult[C],
d ReaderResult[D],
) ReaderResult[T.Tuple4[A, B, C, D]] {
return RR.SequenceT4(a, b, c, d)
}

View File

@@ -0,0 +1,71 @@
// 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 readerresult
import (
"context"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
// Endomorphism represents a function from type A to type A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lazy represents a deferred computation that produces a value of type A when evaluated.
Lazy[A any] = lazy.Lazy[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// Either represents a value that can be one of two types: Left (E) or Right (A).
Either[E, A any] = either.Either[E, A]
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
Result[A any] = result.Result[A]
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
// ReaderResult represents a computation that depends on a context.Context and produces either a value of type A or an error.
// It combines the Reader pattern with Result (error handling), making it suitable for context-aware operations that may fail.
ReaderResult[A any] = func(context.Context) (A, error)
// Monoid represents a monoid structure for ReaderResult values.
Monoid[A any] = monoid.Monoid[ReaderResult[A]]
// Kleisli represents a Kleisli arrow from A to ReaderResult[B].
// It's a function that takes a value of type A and returns a computation that produces B or an error in a context.
Kleisli[A, B any] = Reader[A, ReaderResult[B]]
// Operator represents a Kleisli arrow that operates on ReaderResult values.
// It transforms a ReaderResult[A] into a ReaderResult[B], useful for composing context-aware operations.
Operator[A, B any] = Kleisli[ReaderResult[A], B]
// Lens represents an optic that focuses on a field of type A within a structure of type S.
Lens[S, A any] = lens.Lens[S, A]
// Prism represents an optic that focuses on a case of type A within a sum type S.
Prism[S, A any] = prism.Prism[S, A]
)

View File

@@ -28,7 +28,7 @@ func TestMkdir(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "testdir") newDir := filepath.Join(tmpDir, "testdir")
result := Mkdir(newDir, 0755) result := Mkdir(newDir, 0o755)
path, err := result() path, err := result()
assert.NoError(t, err) assert.NoError(t, err)
@@ -43,14 +43,14 @@ func TestMkdir(t *testing.T) {
t.Run("mkdir with existing directory", func(t *testing.T) { t.Run("mkdir with existing directory", func(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
result := Mkdir(tmpDir, 0755) result := Mkdir(tmpDir, 0o755)
_, err := result() _, err := result()
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("mkdir with parent directory not existing", func(t *testing.T) { t.Run("mkdir with parent directory not existing", func(t *testing.T) {
result := Mkdir("/non/existent/parent/child", 0755) result := Mkdir("/non/existent/parent/child", 0o755)
_, err := result() _, err := result()
assert.Error(t, err) assert.Error(t, err)
@@ -62,7 +62,7 @@ func TestMkdirAll(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3") nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
result := MkdirAll(nestedDir, 0755) result := MkdirAll(nestedDir, 0o755)
path, err := result() path, err := result()
assert.NoError(t, err) assert.NoError(t, err)
@@ -88,7 +88,7 @@ func TestMkdirAll(t *testing.T) {
t.Run("mkdirall with existing directory", func(t *testing.T) { t.Run("mkdirall with existing directory", func(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
result := MkdirAll(tmpDir, 0755) result := MkdirAll(tmpDir, 0o755)
path, err := result() path, err := result()
// MkdirAll should succeed even if directory exists // MkdirAll should succeed even if directory exists
@@ -100,7 +100,7 @@ func TestMkdirAll(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "single") newDir := filepath.Join(tmpDir, "single")
result := MkdirAll(newDir, 0755) result := MkdirAll(newDir, 0o755)
path, err := result() path, err := result()
assert.NoError(t, err) assert.NoError(t, err)
@@ -116,11 +116,11 @@ func TestMkdirAll(t *testing.T) {
filePath := filepath.Join(tmpDir, "file.txt") filePath := filepath.Join(tmpDir, "file.txt")
// Create a file // Create a file
err := os.WriteFile(filePath, []byte("content"), 0644) err := os.WriteFile(filePath, []byte("content"), 0o644)
assert.NoError(t, err) assert.NoError(t, err)
// Try to create a directory where file exists // Try to create a directory where file exists
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755) result := MkdirAll(filepath.Join(filePath, "subdir"), 0o755)
_, err = result() _, err = result()
assert.Error(t, err) assert.Error(t, err)

View File

@@ -34,7 +34,7 @@ func TestOpen(t *testing.T) {
defer os.Remove(tmpPath) defer os.Remove(tmpPath)
// Write some content // Write some content
err = os.WriteFile(tmpPath, []byte("test content"), 0644) err = os.WriteFile(tmpPath, []byte("test content"), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Test Open // Test Open
@@ -127,7 +127,7 @@ func TestWriteFile(t *testing.T) {
testPath := filepath.Join(tmpDir, "write-test.txt") testPath := filepath.Join(tmpDir, "write-test.txt")
testData := []byte("test data") testData := []byte("test data")
result := WriteFile(testPath, 0644)(testData) result := WriteFile(testPath, 0o644)(testData)
returnedData, err := result() returnedData, err := result()
assert.NoError(t, err) assert.NoError(t, err)
@@ -141,7 +141,7 @@ func TestWriteFile(t *testing.T) {
t.Run("write to invalid path", func(t *testing.T) { t.Run("write to invalid path", func(t *testing.T) {
testData := []byte("test data") testData := []byte("test data")
result := WriteFile("/non/existent/dir/file.txt", 0644)(testData) result := WriteFile("/non/existent/dir/file.txt", 0o644)(testData)
_, err := result() _, err := result()
assert.Error(t, err) assert.Error(t, err)
@@ -155,12 +155,12 @@ func TestWriteFile(t *testing.T) {
defer os.Remove(tmpPath) defer os.Remove(tmpPath)
// Write initial content // Write initial content
err = os.WriteFile(tmpPath, []byte("initial"), 0644) err = os.WriteFile(tmpPath, []byte("initial"), 0o644)
require.NoError(t, err) require.NoError(t, err)
// Overwrite with new content // Overwrite with new content
newData := []byte("overwritten") newData := []byte("overwritten")
result := WriteFile(tmpPath, 0644)(newData) result := WriteFile(tmpPath, 0o644)(newData)
returnedData, err := result() returnedData, err := result()
assert.NoError(t, err) assert.NoError(t, err)
@@ -212,7 +212,7 @@ func TestClose(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Verify file is closed by attempting to write // Verify file is closed by attempting to write
_, writeErr := tmpFile.Write([]byte("test")) _, writeErr := tmpFile.WriteString("test")
assert.Error(t, writeErr) assert.Error(t, writeErr)
}) })

View File

@@ -105,7 +105,7 @@ func TestReadAll(t *testing.T) {
largeContent[i] = byte('A' + (i % 26)) largeContent[i] = byte('A' + (i % 26))
} }
err := os.WriteFile(testPath, largeContent, 0644) err := os.WriteFile(testPath, largeContent, 0o644)
require.NoError(t, err) require.NoError(t, err)
result := ReadAll(Open(testPath)) result := ReadAll(Open(testPath))

View File

@@ -70,7 +70,7 @@ func TestWriteAll(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Verify file is closed by trying to write to it // Verify file is closed by trying to write to it
_, writeErr := capturedFile.Write([]byte("more")) _, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr) assert.Error(t, writeErr)
}) })
@@ -147,7 +147,7 @@ func TestWrite(t *testing.T) {
useFile := func(f *os.File) IOResult[string] { useFile := func(f *os.File) IOResult[string] {
return func() (string, error) { return func() (string, error) {
_, err := f.Write([]byte("data")) _, err := f.WriteString("data")
return "success", err return "success", err
} }
} }
@@ -158,7 +158,7 @@ func TestWrite(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Verify file is closed // Verify file is closed
_, writeErr := capturedFile.Write([]byte("more")) _, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr) assert.Error(t, writeErr)
}) })
@@ -183,7 +183,7 @@ func TestWrite(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
// Verify file is still closed even on error // Verify file is still closed even on error
_, writeErr := capturedFile.Write([]byte("more")) _, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr) assert.Error(t, writeErr)
}) })

View File

@@ -45,7 +45,7 @@ func Requester(builder *R.Builder) IOEH.Requester {
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] { withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
return func() (*http.Request, error) { return func() (*http.Request, error) {
req, err := http.NewRequest(method, url, nil) req, err := http.NewRequest(method, url, http.NoBody)
if err == nil { if err == nil {
H.Monoid.Concat(req.Header, builder.GetHeaders()) H.Monoid.Concat(req.Header, builder.GetHeaders())
} }

View File

@@ -23,6 +23,7 @@ import (
F "github.com/IBM/fp-go/v2/function" F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils" "github.com/IBM/fp-go/v2/internal/utils"
"github.com/IBM/fp-go/v2/io" "github.com/IBM/fp-go/v2/io"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -111,7 +112,7 @@ func TestChainWithIO(t *testing.T) {
Of("test"), Of("test"),
ChainIOK(func(s string) IO[bool] { ChainIOK(func(s string) IO[bool] {
return func() bool { return func() bool {
return len(s) > 0 return S.IsNonEmpty(s)
} }
}), }),
) )

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