mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-17 23:37:41 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20398e67a9 | ||
|
|
fceda15701 | ||
|
|
4ebfcadabe | ||
|
|
acb601fc01 | ||
|
|
d17663f016 | ||
|
|
829365fc24 | ||
|
|
64b5660b4e | ||
|
|
16e82d6a65 | ||
|
|
0d40fdcebb | ||
|
|
6a4dfa2c93 | ||
|
|
a37f379a3c | ||
|
|
ece0cd135d | ||
|
|
739b6a284c | ||
|
|
ba10d8d314 | ||
|
|
3d6c419185 | ||
|
|
3f4b6292e4 |
212
v2/EXAMPLE_TESTS_PROGRESS.md
Normal file
212
v2/EXAMPLE_TESTS_PROGRESS.md
Normal 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
|
||||
@@ -61,6 +61,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -145,6 +146,8 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
|
||||
### From V1 to V2
|
||||
|
||||
#### 1. Generic Type Aliases
|
||||
|
||||
@@ -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] {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
@@ -214,3 +215,262 @@ func ExampleFoldMap() {
|
||||
// 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])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
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) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -140,22 +140,27 @@ func Empty[GA ~[]A, A any]() GA {
|
||||
return array.Empty[GA]()
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func UpsertAt[GA ~[]A, A any](a A) func(GA) GA {
|
||||
return array.UpsertAt[GA](a)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
|
||||
return array.Prepend[ENDO](head)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
return array.Reverse(as)
|
||||
}
|
||||
|
||||
@@ -18,14 +18,11 @@ package nonempty
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
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
|
||||
func Of[A any](first A) NonEmptyArray[A] {
|
||||
return G.Of[NonEmptyArray[A]](first)
|
||||
@@ -44,20 +41,24 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
|
||||
return buffer
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
|
||||
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
|
||||
}
|
||||
|
||||
func Map[A, B any](f func(a A) B) func(NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
return F.Bind2nd(MonadMap[A, B], f)
|
||||
//go:inline
|
||||
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 {
|
||||
@@ -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 {
|
||||
return as[1:]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Head[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func First[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Last[A any](as NonEmptyArray[A]) A {
|
||||
return as[len(as)-1]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Size[A any](as NonEmptyArray[A]) int {
|
||||
return G.Size(as)
|
||||
}
|
||||
@@ -96,11 +102,11 @@ func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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]] {
|
||||
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))
|
||||
}
|
||||
|
||||
370
v2/array/nonempty/array_test.go
Normal file
370
v2/array/nonempty/array_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
15
v2/array/nonempty/types.go
Normal file
15
v2/array/nonempty/types.go
Normal 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]
|
||||
)
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
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) {
|
||||
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)
|
||||
if !result {
|
||||
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) {
|
||||
// Create multiple focused assertions
|
||||
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 })(
|
||||
That(func(age int) bool { return age >= 18 && age <= 100 }),
|
||||
|
||||
169
v2/cli/lens.go
169
v2/cli/lens.go
@@ -27,13 +27,15 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
C "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLensDir = "dir"
|
||||
keyVerbose = "verbose"
|
||||
lensAnnotation = "fp-go:Lens"
|
||||
keyLensDir = "dir"
|
||||
keyVerbose = "verbose"
|
||||
keyIncludeTestFile = "include-test-files"
|
||||
lensAnnotation = "fp-go:Lens"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,6 +51,13 @@ var (
|
||||
Value: false,
|
||||
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
|
||||
@@ -67,6 +76,7 @@ type fieldInfo struct {
|
||||
BaseType string // TypeName without leading * for pointer types
|
||||
IsOptional bool // true if field is a pointer or has json omitempty tag
|
||||
IsComparable bool // true if the type is comparable (can use ==)
|
||||
IsEmbedded bool // true if this field comes from an embedded struct
|
||||
}
|
||||
|
||||
// templateData holds data for template rendering
|
||||
@@ -80,12 +90,12 @@ const lensStructTemplate = `
|
||||
type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}} __lens.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
// optional fields
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}}O __lens_option.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
@@ -94,13 +104,24 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}} __lens.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
// optional fields
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
// prisms
|
||||
{{- range .Fields}}
|
||||
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
|
||||
type {{.Name}}Prisms{{.TypeParams}} struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
`
|
||||
@@ -110,15 +131,16 @@ const lensConstructorTemplate = `
|
||||
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
lens{{.Name}} := L.MakeLens(
|
||||
lens{{.Name}} := __lens.MakeLensWithName(
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- 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}}
|
||||
return {{.Name}}Lenses{{.TypeParamNames}}{
|
||||
@@ -140,21 +162,23 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
lens{{.Name}} := L.MakeLensStrict(
|
||||
lens{{.Name}} := __lens.MakeLensStrictWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
|
||||
)
|
||||
{{- else}}
|
||||
lens{{.Name}} := L.MakeLensRef(
|
||||
lens{{.Name}} := __lens.MakeLensRefWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- 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}}
|
||||
return {{.Name}}RefLenses{{.TypeParamNames}}{
|
||||
@@ -170,6 +194,47 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
||||
{{- 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 (
|
||||
@@ -439,7 +504,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
||||
return results
|
||||
}
|
||||
|
||||
if typeName == "" || typeIdent == nil {
|
||||
if S.IsEmpty(typeName) || typeIdent == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -494,6 +559,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
||||
BaseType: baseType,
|
||||
IsOptional: isOptional,
|
||||
IsComparable: isComparable,
|
||||
IsEmbedded: true,
|
||||
},
|
||||
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
|
||||
func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
func generateLensHelpers(dir, filename string, verbose, includeTestFiles bool) error {
|
||||
// Get absolute path
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
@@ -716,21 +782,34 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
log.Printf("Found %d Go files", len(files))
|
||||
}
|
||||
|
||||
// Parse all files and collect structs
|
||||
var allStructs []structInfo
|
||||
// Parse all files and collect structs, separating test and non-test files
|
||||
var regularStructs []structInfo
|
||||
var testStructs []structInfo
|
||||
var packageName string
|
||||
|
||||
for _, file := range files {
|
||||
// Skip generated files and test files
|
||||
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
|
||||
baseName := filepath.Base(file)
|
||||
|
||||
// Skip generated lens files (both regular and test)
|
||||
if strings.HasPrefix(baseName, "gen_lens") && strings.HasSuffix(baseName, ".go") {
|
||||
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
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Parsing file: %s", filepath.Base(file))
|
||||
log.Printf("Parsing file: %s", baseName)
|
||||
}
|
||||
|
||||
structs, pkg, err := parseFile(file)
|
||||
@@ -740,27 +819,52 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
|
||||
}
|
||||
}
|
||||
|
||||
if packageName == "" {
|
||||
if S.IsEmpty(packageName) {
|
||||
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)
|
||||
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
|
||||
allImports := make(map[string]string) // import path -> alias
|
||||
for _, s := range allStructs {
|
||||
for _, s := range structs {
|
||||
for importPath, alias := range s.Imports {
|
||||
allImports[importPath] = alias
|
||||
}
|
||||
@@ -774,7 +878,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
}
|
||||
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
|
||||
writePackage(f, packageName)
|
||||
@@ -782,10 +886,11 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
// Write imports
|
||||
f.WriteString("import (\n")
|
||||
// Standard fp-go imports always needed
|
||||
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
||||
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
|
||||
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
|
||||
f.WriteString("\t__lens \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||
f.WriteString("\t__option \"github.com/IBM/fp-go/v2/option\"\n")
|
||||
f.WriteString("\t__prism \"github.com/IBM/fp-go/v2/optics/prism\"\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
|
||||
for importPath, alias := range allImports {
|
||||
@@ -795,7 +900,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
f.WriteString(")\n")
|
||||
|
||||
// Generate lens code for each struct using templates
|
||||
for _, s := range allStructs {
|
||||
for _, s := range structs {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Generate struct type
|
||||
@@ -827,12 +932,14 @@ func LensCommand() *C.Command {
|
||||
flagLensDir,
|
||||
flagFilename,
|
||||
flagVerbose,
|
||||
flagIncludeTestFiles,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
ctx.Bool(keyIncludeTestFile),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -60,7 +61,7 @@ func TestHasLensAnnotation(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var doc *ast.CommentGroup
|
||||
if tt.comment != "" {
|
||||
if S.IsNonEmpty(tt.comment) {
|
||||
doc = &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: tt.comment},
|
||||
@@ -289,7 +290,7 @@ func TestHasOmitEmpty(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var tag *ast.BasicLit
|
||||
if tt.tag != "" {
|
||||
if S.IsNonEmpty(tt.tag) {
|
||||
tag = &ast.BasicLit{
|
||||
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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// Parse the file
|
||||
@@ -514,16 +515,16 @@ func TestLensRefTemplatesWithComparable(t *testing.T) {
|
||||
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
|
||||
|
||||
// Name field - comparable, should use MakeLensStrict
|
||||
assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(",
|
||||
"comparable field Name should use MakeLensStrict in RefLenses")
|
||||
assert.Contains(t, constructorStr, "lensName := __lens.MakeLensStrictWithName(",
|
||||
"comparable field Name should use MakeLensStrictWithName in RefLenses")
|
||||
|
||||
// Age field - comparable, should use MakeLensStrict
|
||||
assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(",
|
||||
"comparable field Age should use MakeLensStrict in RefLenses")
|
||||
assert.Contains(t, constructorStr, "lensAge := __lens.MakeLensStrictWithName(",
|
||||
"comparable field Age should use MakeLensStrictWithName in RefLenses")
|
||||
|
||||
// Data field - not comparable, should use MakeLensRef
|
||||
assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(",
|
||||
"non-comparable field Data should use MakeLensRef in RefLenses")
|
||||
assert.Contains(t, constructorStr, "lensData := __lens.MakeLensRefWithName(",
|
||||
"non-comparable field Data should use MakeLensRefWithName in RefLenses")
|
||||
|
||||
}
|
||||
|
||||
@@ -542,12 +543,12 @@ type TestStruct struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
@@ -564,23 +565,23 @@ type TestStruct struct {
|
||||
// Check for expected content in RefLenses
|
||||
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
|
||||
|
||||
// Name and Count are comparable, should use MakeLensStrict
|
||||
assert.Contains(t, contentStr, "L.MakeLensStrict",
|
||||
"comparable fields should use MakeLensStrict in RefLenses")
|
||||
// Name and Count are comparable, should use MakeLensStrictWithName
|
||||
assert.Contains(t, contentStr, "__lens.MakeLensStrictWithName",
|
||||
"comparable fields should use MakeLensStrictWithName in RefLenses")
|
||||
|
||||
// Data is not comparable (slice), should use MakeLensRef
|
||||
assert.Contains(t, contentStr, "L.MakeLensRef",
|
||||
"non-comparable fields should use MakeLensRef in RefLenses")
|
||||
// Data is not comparable (slice), should use MakeLensRefWithName
|
||||
assert.Contains(t, contentStr, "__lens.MakeLensRefWithName",
|
||||
"non-comparable fields should use MakeLensRefWithName in RefLenses")
|
||||
|
||||
// Verify the pattern appears for Name field (comparable)
|
||||
namePattern := "lensName := L.MakeLensStrict("
|
||||
namePattern := "lensName := __lens.MakeLensStrictWithName("
|
||||
assert.Contains(t, contentStr, namePattern,
|
||||
"Name field should use MakeLensStrict")
|
||||
"Name field should use MakeLensStrictWithName")
|
||||
|
||||
// Verify the pattern appears for Data field (not comparable)
|
||||
dataPattern := "lensData := L.MakeLensRef("
|
||||
dataPattern := "lensData := __lens.MakeLensRefWithName("
|
||||
assert.Contains(t, contentStr, dataPattern,
|
||||
"Data field should use MakeLensRef")
|
||||
"Data field should use MakeLensRefWithName")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpers(t *testing.T) {
|
||||
@@ -597,12 +598,12 @@ type TestStruct struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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, "TestStructLenses")
|
||||
assert.Contains(t, contentStr, "MakeTestStructLenses")
|
||||
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, contentStr, "IO.FromZero")
|
||||
assert.Contains(t, contentStr, "__lens.Lens[TestStruct, string]")
|
||||
assert.Contains(t, contentStr, "__lens_option.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, contentStr, "__iso_option.FromZero")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
|
||||
@@ -639,12 +640,12 @@ type TestStruct struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code (should not create file)
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file does not exist
|
||||
@@ -669,10 +670,10 @@ func TestLensTemplates(t *testing.T) {
|
||||
|
||||
structStr := structBuf.String()
|
||||
assert.Contains(t, structStr, "type TestStructLenses struct")
|
||||
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]")
|
||||
assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, structStr, "Name __lens.Lens[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO __lens_option.LensO[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "Value __lens.Lens[TestStruct, *int]")
|
||||
assert.Contains(t, structStr, "ValueO __lens_option.LensO[TestStruct, *int]")
|
||||
|
||||
// Test constructor template
|
||||
var constructorBuf bytes.Buffer
|
||||
@@ -686,7 +687,7 @@ func TestLensTemplates(t *testing.T) {
|
||||
assert.Contains(t, constructorStr, "NameO: lensNameO,")
|
||||
assert.Contains(t, constructorStr, "Value: lensValue,")
|
||||
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero")
|
||||
}
|
||||
|
||||
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||
@@ -707,14 +708,14 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||
|
||||
structStr := structBuf.String()
|
||||
assert.Contains(t, structStr, "type ConfigStructLenses struct")
|
||||
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]")
|
||||
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]")
|
||||
assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]")
|
||||
assert.Contains(t, structStr, "Name __lens.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO __lens_option.LensO[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "Value __lens.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "ValueO __lens_option.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Count __lens.Lens[ConfigStruct, int]")
|
||||
assert.Contains(t, structStr, "CountO __lens_option.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Pointer __lens.Lens[ConfigStruct, *string]")
|
||||
assert.Contains(t, structStr, "PointerO __lens_option.LensO[ConfigStruct, *string]")
|
||||
|
||||
// Test constructor template
|
||||
var constructorBuf bytes.Buffer
|
||||
@@ -723,9 +724,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||
|
||||
constructorStr := constructorBuf.String()
|
||||
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero[string]()")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero[int]()")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero[string]()")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero[int]()")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero[*string]()")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// 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 {
|
||||
switch flag.Names()[0] {
|
||||
case "dir":
|
||||
@@ -748,12 +749,15 @@ func TestLensCommandFlags(t *testing.T) {
|
||||
hasFilename = true
|
||||
case "verbose":
|
||||
hasVerbose = true
|
||||
case "include-test-files":
|
||||
hasIncludeTestFiles = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasDir, "should have dir flag")
|
||||
assert.True(t, hasFilename, "should have filename flag")
|
||||
assert.True(t, hasVerbose, "should have verbose flag")
|
||||
assert.True(t, hasIncludeTestFiles, "should have include-test-files flag")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Parse the file
|
||||
@@ -824,12 +828,12 @@ type Person struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
@@ -849,14 +853,14 @@ type Person struct {
|
||||
assert.Contains(t, contentStr, "MakePersonLenses")
|
||||
|
||||
// Check that embedded fields are included
|
||||
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
|
||||
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
|
||||
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
|
||||
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")
|
||||
assert.Contains(t, contentStr, "Street __lens.Lens[Person, string]", "Should have lens for embedded Street field")
|
||||
assert.Contains(t, contentStr, "City __lens.Lens[Person, string]", "Should have lens for embedded City field")
|
||||
assert.Contains(t, contentStr, "Name __lens.Lens[Person, string]", "Should have lens for Name 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
|
||||
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
|
||||
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
|
||||
assert.Contains(t, contentStr, "StreetO __lens_option.LensO[Person, string]")
|
||||
assert.Contains(t, contentStr, "CityO __lens_option.LensO[Person, string]")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
|
||||
// Parse the file
|
||||
@@ -998,12 +1002,12 @@ type Box[T any] struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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")
|
||||
|
||||
// Check that fields use the generic type parameter
|
||||
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
|
||||
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")
|
||||
assert.Contains(t, contentStr, "Content __lens.Lens[Box[T], T]", "Should have lens for generic Content field")
|
||||
assert.Contains(t, contentStr, "Label __lens.Lens[Box[T], string]", "Should have lens for Label field")
|
||||
|
||||
// Check optional lenses - only for comparable types
|
||||
// T any is not comparable, so ContentO should NOT be generated
|
||||
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
|
||||
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
|
||||
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) {
|
||||
@@ -1049,12 +1053,12 @@ type ComparableBox[T comparable] struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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")
|
||||
|
||||
// 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
|
||||
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)
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
// 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)))
|
||||
}
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
// types
|
||||
@@ -209,7 +211,7 @@ func generateTraverseTuple1(
|
||||
fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i)
|
||||
// map
|
||||
fmt.Fprintf(f, " Map[")
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s, T1,", infix)
|
||||
} else {
|
||||
fmt.Fprintf(f, "T1,")
|
||||
@@ -231,7 +233,7 @@ func generateTraverseTuple1(
|
||||
fmt.Fprintf(f, " ")
|
||||
}
|
||||
fmt.Fprintf(f, "%s", tuple)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
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, "func SequenceTuple%d[", i)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s", infix)
|
||||
}
|
||||
for j := 0; j < i; j++ {
|
||||
if infix != "" || j > 0 {
|
||||
if S.IsNonEmpty(infix) || j > 0 {
|
||||
fmt.Fprintf(f, ", ")
|
||||
}
|
||||
fmt.Fprintf(f, "T%d", j+1)
|
||||
@@ -276,7 +278,7 @@ func generateSequenceTuple1(
|
||||
fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i)
|
||||
// map
|
||||
fmt.Fprintf(f, " Map[")
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s, T1,", infix)
|
||||
} else {
|
||||
fmt.Fprintf(f, "T1,")
|
||||
@@ -298,7 +300,7 @@ func generateSequenceTuple1(
|
||||
fmt.Fprintf(f, " ")
|
||||
}
|
||||
fmt.Fprintf(f, "%s", tuple)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
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, "func SequenceT%d[", i)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s", infix)
|
||||
}
|
||||
for j := 0; j < i; j++ {
|
||||
if infix != "" || j > 0 {
|
||||
if S.IsNonEmpty(infix) || j > 0 {
|
||||
fmt.Fprintf(f, ", ")
|
||||
}
|
||||
fmt.Fprintf(f, "T%d", j+1)
|
||||
@@ -339,7 +341,7 @@ func generateSequenceT1(
|
||||
fmt.Fprintf(f, " return A.SequenceT%d(\n", i)
|
||||
// map
|
||||
fmt.Fprintf(f, " Map[")
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s, T1,", infix)
|
||||
} else {
|
||||
fmt.Fprintf(f, "T1,")
|
||||
@@ -361,7 +363,7 @@ func generateSequenceT1(
|
||||
fmt.Fprintf(f, " ")
|
||||
}
|
||||
fmt.Fprintf(f, "%s", tuple)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
fmt.Fprintf(f, ", T%d],\n", j+1)
|
||||
|
||||
177
v2/consumer/consumer.go
Normal file
177
v2/consumer/consumer.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
383
v2/consumer/consumer_test.go
Normal file
383
v2/consumer/consumer_test.go
Normal 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
56
v2/consumer/types.go
Normal 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]
|
||||
)
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
// 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] {
|
||||
return func() Result[A] {
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return result.Left[A](err)
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[A](context.Cause(ctx))
|
||||
}
|
||||
return ma()
|
||||
}
|
||||
|
||||
16
v2/context/readerio/bracket.go
Normal file
16
v2/context/readerio/bracket.go
Normal 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)
|
||||
}
|
||||
13
v2/context/readerio/consumer.go
Normal file
13
v2/context/readerio/consumer.go
Normal 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))
|
||||
}
|
||||
@@ -16,5 +16,5 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderIO[A]) Kleisli[R, B] {
|
||||
return RIO.TraverseReader[context.Context, R](f)
|
||||
return RIO.TraverseReader[context.Context](f)
|
||||
}
|
||||
|
||||
29
v2/context/readerio/logging.go
Normal file
29
v2/context/readerio/logging.go
Normal 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)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package readerio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"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] {
|
||||
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)
|
||||
}
|
||||
|
||||
25
v2/context/readerio/rec.go
Normal file
25
v2/context/readerio/rec.go
Normal 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)
|
||||
}
|
||||
41
v2/context/readerio/retry.go
Normal file
41
v2/context/readerio/retry.go
Normal 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,
|
||||
)
|
||||
}
|
||||
@@ -18,6 +18,8 @@ package readerio
|
||||
import (
|
||||
"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/lazy"
|
||||
"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 any] = Kleisli[ReaderIO[A], B]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
|
||||
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
|
||||
5. [Practical Benefits](#practical-benefits)
|
||||
6. [Examples](#examples)
|
||||
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
||||
5. [TraverseReader: Introducing Dependencies](#traversereader-introducing-dependencies)
|
||||
6. [Practical Benefits](#practical-benefits)
|
||||
7. [Examples](#examples)
|
||||
8. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
||||
|
||||
## What is Point-Free Style?
|
||||
|
||||
@@ -25,10 +26,7 @@ func double(x int) int {
|
||||
|
||||
**Point-free style (without points):**
|
||||
```go
|
||||
var double = F.Flow2(
|
||||
N.Mul(2),
|
||||
identity,
|
||||
)
|
||||
var double = N.Mul(2)
|
||||
```
|
||||
|
||||
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
|
||||
func SequenceReader[R, A any](
|
||||
ma ReaderIOResult[Reader[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
) Kleisli[R, A]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
@@ -115,7 +113,7 @@ Now `R` (the Reader's environment) comes **first**, before `context.Context`!
|
||||
```go
|
||||
func SequenceReaderIO[R, A any](
|
||||
ma ReaderIOResult[ReaderIO[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
) Kleisli[R, A]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
@@ -129,7 +127,7 @@ To: func(R) func(context.Context) func() Either[error, A]
|
||||
```go
|
||||
func SequenceReaderResult[R, A any](
|
||||
ma ReaderIOResult[ReaderResult[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
) Kleisli[R, A]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
@@ -222,6 +220,186 @@ authInfo := authService(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
|
||||
|
||||
### 1. **Improved Testability**
|
||||
|
||||
@@ -18,14 +18,13 @@ package readerioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -96,7 +95,7 @@ func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) 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]
|
||||
@@ -128,6 +127,13 @@ func BindTo[S1, T any](
|
||||
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
|
||||
// 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.
|
||||
@@ -214,7 +220,7 @@ func ApS[S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa ReaderIOResult[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
@@ -253,10 +259,10 @@ func ApSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) 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.
|
||||
@@ -289,8 +295,8 @@ func BindL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[S, S] {
|
||||
return RIOR.LetL[context.Context](lens, f)
|
||||
}
|
||||
@@ -322,7 +328,7 @@ func LetL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return RIOR.LetToL[context.Context](lens, b)
|
||||
@@ -398,7 +404,7 @@ func BindReaderK[S1, S2, T any](
|
||||
//go:inline
|
||||
func BindReaderIOK[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f readerio.Kleisli[context.Context, S1, T],
|
||||
f readerio.Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return Bind(setter, F.Flow2(f, FromReaderIO[T]))
|
||||
}
|
||||
@@ -443,7 +449,7 @@ func BindResultK[S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindIOEitherKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f ioresult.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||
@@ -458,7 +464,7 @@ func BindIOEitherKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindIOResultKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f ioresult.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||
@@ -474,7 +480,7 @@ func BindIOResultKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindIOKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f io.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIO[T]))
|
||||
@@ -490,7 +496,7 @@ func BindIOKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f reader.Kleisli[context.Context, T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReader[T]))
|
||||
@@ -506,8 +512,8 @@ func BindReaderKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderIOKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f readerio.Kleisli[context.Context, T, T],
|
||||
lens Lens[S, T],
|
||||
f readerio.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
|
||||
}
|
||||
@@ -627,7 +633,7 @@ func ApResultS[S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApIOEitherSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa IOResult[T],
|
||||
) Operator[S, S] {
|
||||
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
|
||||
func ApIOResultSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa IOResult[T],
|
||||
) Operator[S, S] {
|
||||
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
|
||||
func ApIOSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromIO(fa))
|
||||
@@ -672,7 +678,7 @@ func ApIOSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa Reader[context.Context, T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromReader(fa))
|
||||
@@ -687,7 +693,7 @@ func ApReaderSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderIOSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa ReaderIO[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromReaderIO(fa))
|
||||
@@ -702,7 +708,7 @@ func ApReaderIOSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa Result[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromEither(fa))
|
||||
@@ -717,7 +723,7 @@ func ApEitherSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApResultSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa Result[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromResult(fa))
|
||||
|
||||
@@ -203,9 +203,7 @@ func TestApS_EmptyState(t *testing.T) {
|
||||
result := res(t.Context())()
|
||||
assert.True(t, E.IsRight(result))
|
||||
emptyOpt := E.ToOption(result)
|
||||
assert.True(t, O.IsSome(emptyOpt))
|
||||
empty, _ := O.Unwrap(emptyOpt)
|
||||
assert.Equal(t, Empty{}, empty)
|
||||
assert.Equal(t, O.Of(Empty{}), emptyOpt)
|
||||
}
|
||||
|
||||
func TestApS_ChainedWithBind(t *testing.T) {
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
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
|
||||
// whether the body action returns and error or not.
|
||||
//
|
||||
//go:inline
|
||||
func Bracket[
|
||||
A, B, ANY any](
|
||||
|
||||
@@ -28,5 +31,5 @@ func Bracket[
|
||||
use Kleisli[A, B],
|
||||
release func(A, Either[B]) ReaderIOResult[ANY],
|
||||
) ReaderIOResult[B] {
|
||||
return RIOR.Bracket(acquire, use, release)
|
||||
return RIOR.Bracket(acquire, F.Flow2(use, WithContext), release)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"context"
|
||||
|
||||
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
@@ -34,9 +35,17 @@ import (
|
||||
// Returns a ReaderIOResult that checks for cancellation before executing.
|
||||
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return func(ctx context.Context) IOEither[A] {
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return ioeither.Left[A](err)
|
||||
if ctx.Err() != nil {
|
||||
return ioeither.Left[A](context.Cause(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,
|
||||
)
|
||||
}
|
||||
|
||||
13
v2/context/readerioresult/consumer.go
Normal file
13
v2/context/readerioresult/consumer.go
Normal 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))
|
||||
}
|
||||
@@ -83,7 +83,7 @@ import (
|
||||
// )
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[co
|
||||
// )
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kl
|
||||
// )
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
|
||||
732
v2/context/readerioresult/logging.go
Normal file
732
v2/context/readerioresult/logging.go
Normal 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))
|
||||
}
|
||||
662
v2/context/readerioresult/logging_test.go
Normal file
662
v2/context/readerioresult/logging_test.go
Normal 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")
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerio"
|
||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"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/ioeither"
|
||||
"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/readerio"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -150,7 +152,7 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
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.
|
||||
@@ -163,7 +165,7 @@ func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[
|
||||
//
|
||||
//go:inline
|
||||
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.
|
||||
@@ -177,12 +179,12 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
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
|
||||
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.
|
||||
@@ -195,12 +197,12 @@ func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirst(f)
|
||||
return RIOR.ChainFirst(WithContextK(f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
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.
|
||||
@@ -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] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return ioeither.Left[B](err)
|
||||
if ctx.Err() != nil {
|
||||
return ioeither.Left[B](context.Cause(ctx))
|
||||
}
|
||||
|
||||
return func() Result[B] {
|
||||
// quick check for cancellation
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return either.Left[B](err)
|
||||
if ctx.Err() != nil {
|
||||
return either.Left[B](context.Cause(ctx))
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -527,7 +534,7 @@ func Never[A any]() ReaderIOResult[A] {
|
||||
// Returns a new ReaderIOResult with the chained IO computation.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -753,7 +760,7 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
//
|
||||
//go:inline
|
||||
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.
|
||||
@@ -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.
|
||||
//
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -858,32 +865,32 @@ func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
@@ -913,15 +920,15 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
|
||||
//
|
||||
//go:inline
|
||||
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].
|
||||
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
return RIOR.ChainLeft(f)
|
||||
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
|
||||
return RIOR.ChainLeft(WithContextK(f))
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
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].
|
||||
@@ -951,10 +958,212 @@ func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOR
|
||||
//
|
||||
//go:inline
|
||||
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
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -567,15 +567,13 @@ func TestMemoize(t *testing.T) {
|
||||
res1 := computation(context.Background())()
|
||||
assert.True(t, E.IsRight(res1))
|
||||
val1 := E.ToOption(res1)
|
||||
v1, _ := O.Unwrap(val1)
|
||||
assert.Equal(t, 1, v1)
|
||||
assert.Equal(t, O.Of(1), val1)
|
||||
|
||||
// Second execution should return cached value
|
||||
res2 := computation(context.Background())()
|
||||
assert.True(t, E.IsRight(res2))
|
||||
val2 := E.ToOption(res2)
|
||||
v2, _ := O.Unwrap(val2)
|
||||
assert.Equal(t, 1, v2)
|
||||
assert.Equal(t, O.Of(1), val2)
|
||||
|
||||
// Counter should only be incremented once
|
||||
assert.Equal(t, 1, counter)
|
||||
@@ -739,9 +737,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
res := result(context.Background())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(arrOpt))
|
||||
resultArr, _ := O.Unwrap(arrOpt)
|
||||
assert.Equal(t, []int{2, 4, 6}, resultArr)
|
||||
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
|
||||
})
|
||||
|
||||
t.Run("TraverseArray with error", func(t *testing.T) {
|
||||
@@ -765,9 +761,7 @@ func TestSequenceArray(t *testing.T) {
|
||||
res := result(context.Background())()
|
||||
assert.True(t, E.IsRight(res))
|
||||
arrOpt := E.ToOption(res)
|
||||
assert.True(t, O.IsSome(arrOpt))
|
||||
resultArr, _ := O.Unwrap(arrOpt)
|
||||
assert.Equal(t, []int{1, 2, 3}, resultArr)
|
||||
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
|
||||
}
|
||||
|
||||
func TestTraverseRecord(t *testing.T) {
|
||||
|
||||
184
v2/context/readerioresult/rec.go
Normal file
184
v2/context/readerioresult/rec.go
Normal 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))
|
||||
}
|
||||
433
v2/context/readerioresult/rec_test.go
Normal file
433
v2/context/readerioresult/rec_test.go
Normal 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)
|
||||
}
|
||||
@@ -16,7 +16,11 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource.
|
||||
@@ -55,3 +59,111 @@ import (
|
||||
func WithResource[A, R, ANY any](onCreate ReaderIOResult[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
|
||||
return RIOR.WithResource[A](onCreate, onRelease)
|
||||
}
|
||||
|
||||
// onClose is a helper function that creates a ReaderIOResult for closing an io.Closer resource.
|
||||
// It safely calls the Close() method and handles any errors that may occur during closing.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: Must implement io.Closer interface
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The resource to close
|
||||
//
|
||||
// Returns:
|
||||
// - ReaderIOResult[any]: A computation that closes the resource and returns nil on success
|
||||
//
|
||||
// The function ignores the context parameter since closing operations typically don't need context.
|
||||
// Any error from Close() is captured and returned as a Result error.
|
||||
func onClose[A io.Closer](a A) ReaderIOResult[any] {
|
||||
return func(_ context.Context) IOResult[any] {
|
||||
return func() Result[any] {
|
||||
return result.TryCatchError[any](nil, a.Close())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithCloser creates a resource management function specifically for io.Closer resources.
|
||||
// This is a specialized version of WithResource that automatically handles closing of resources
|
||||
// that implement the io.Closer interface.
|
||||
//
|
||||
// The function ensures that:
|
||||
// - The resource is created using the onCreate function
|
||||
// - The resource is automatically closed when the operation completes (success or failure)
|
||||
// - Any errors during closing are properly handled
|
||||
// - The resource is closed even if the main operation fails or the context is canceled
|
||||
//
|
||||
// Type Parameters:
|
||||
// - B: The type of value returned by the resource-using function
|
||||
// - A: The type of resource that implements io.Closer
|
||||
//
|
||||
// Parameters:
|
||||
// - onCreate: ReaderIOResult that creates the io.Closer resource
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a resource-using function and returns a ReaderIOResult[B]
|
||||
//
|
||||
// Example with file operations:
|
||||
//
|
||||
// openFile := func(filename string) ReaderIOResult[*os.File] {
|
||||
// return TryCatch(func(ctx context.Context) func() (*os.File, error) {
|
||||
// return func() (*os.File, error) {
|
||||
// return os.Open(filename)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// fileReader := WithCloser(openFile("data.txt"))
|
||||
// result := fileReader(func(f *os.File) ReaderIOResult[string] {
|
||||
// return TryCatch(func(ctx context.Context) func() (string, error) {
|
||||
// return func() (string, error) {
|
||||
// data, err := io.ReadAll(f)
|
||||
// return string(data), err
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// Example with HTTP response:
|
||||
//
|
||||
// httpGet := func(url string) ReaderIOResult[*http.Response] {
|
||||
// return TryCatch(func(ctx context.Context) func() (*http.Response, error) {
|
||||
// return func() (*http.Response, error) {
|
||||
// return http.Get(url)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// responseReader := WithCloser(httpGet("https://api.example.com/data"))
|
||||
// result := responseReader(func(resp *http.Response) ReaderIOResult[[]byte] {
|
||||
// return TryCatch(func(ctx context.Context) func() ([]byte, error) {
|
||||
// return func() ([]byte, error) {
|
||||
// return io.ReadAll(resp.Body)
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// Example with database connection:
|
||||
//
|
||||
// openDB := func(dsn string) ReaderIOResult[*sql.DB] {
|
||||
// return TryCatch(func(ctx context.Context) func() (*sql.DB, error) {
|
||||
// return func() (*sql.DB, error) {
|
||||
// return sql.Open("postgres", dsn)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// dbQuery := WithCloser(openDB("postgres://..."))
|
||||
// result := dbQuery(func(db *sql.DB) ReaderIOResult[[]User] {
|
||||
// return TryCatch(func(ctx context.Context) func() ([]User, error) {
|
||||
// return func() ([]User, error) {
|
||||
// rows, err := db.QueryContext(ctx, "SELECT * FROM users")
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// defer rows.Close()
|
||||
// return scanUsers(rows)
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
func WithCloser[B any, A io.Closer](onCreate ReaderIOResult[A]) Kleisli[Kleisli[A, B], B] {
|
||||
return WithResource[B](onCreate, onClose[A])
|
||||
}
|
||||
|
||||
179
v2/context/readerioresult/retry.go
Normal file
179
v2/context/readerioresult/retry.go
Normal 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,
|
||||
)
|
||||
|
||||
}
|
||||
511
v2/context/readerioresult/retry_test.go
Normal file
511
v2/context/readerioresult/retry_test.go
Normal 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")
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package readerioresult
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"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],
|
||||
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],
|
||||
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],
|
||||
ApSeq[[]B, B],
|
||||
as,
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,7 +140,7 @@ func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
Of[[]B],
|
||||
Map[[]B, func(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],
|
||||
ApSeq[map[K]B, B],
|
||||
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],
|
||||
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],
|
||||
ApPar[[]B, B],
|
||||
as,
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -232,7 +233,7 @@ func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
Of[[]B],
|
||||
Map[[]B, func(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],
|
||||
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],
|
||||
ApPar[map[K]B, B],
|
||||
as,
|
||||
f,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,16 @@ package readerioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"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/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
@@ -126,4 +130,11 @@ type (
|
||||
ReaderResult[A any] = readerresult.ReaderResult[A]
|
||||
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, 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]
|
||||
)
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -17,7 +17,6 @@ package readerresult
|
||||
|
||||
import (
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -31,16 +30,26 @@ import (
|
||||
// TenantID string
|
||||
// }
|
||||
// result := readereither.Do(State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderResult[S] {
|
||||
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
|
||||
// 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
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
@@ -78,14 +87,27 @@ func Do[S any](
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) 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](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -93,7 +115,10 @@ func Let[S1, S2, T any](
|
||||
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](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
@@ -102,15 +127,27 @@ func LetTo[S1, S2, T any](
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Kleisli[ReaderResult[T], S1] {
|
||||
) Operator[T, S1] {
|
||||
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
|
||||
// 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
|
||||
// and can conceptually run in parallel.
|
||||
@@ -145,6 +182,8 @@ func BindTo[S1, T any](
|
||||
// getTenantID,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[T],
|
||||
@@ -183,17 +222,24 @@ func ApS[S1, S2, T any](
|
||||
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
||||
// readereither.ApSL(ageLens, getAge),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa ReaderResult[T],
|
||||
) Kleisli[ReaderResult[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.
|
||||
// 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
|
||||
// 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
|
||||
//
|
||||
// Parameters:
|
||||
@@ -227,15 +273,20 @@ func ApSL[S, T any](
|
||||
// readereither.Of[error](Counter{Value: 42}),
|
||||
// readereither.BindL(valueLens, increment),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) 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.
|
||||
// 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:
|
||||
// - 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),
|
||||
// )
|
||||
// // result when executed will be Right(Counter{Value: 42})
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
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.
|
||||
// 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:
|
||||
// - 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),
|
||||
// )
|
||||
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return LetTo(lens.Set, b)
|
||||
|
||||
@@ -19,14 +19,23 @@ import (
|
||||
"context"
|
||||
|
||||
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
|
||||
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
||||
return func(ctx context.Context) E.Either[error, A] {
|
||||
if err := context.Cause(ctx); err != nil {
|
||||
return E.Left[A](err)
|
||||
if ctx.Err() != nil {
|
||||
return E.Left[A](context.Cause(ctx))
|
||||
}
|
||||
return ma(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
|
||||
return F.Flow2(
|
||||
f,
|
||||
WithContext,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
import (
|
||||
@@ -7,11 +22,131 @@ import (
|
||||
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
|
||||
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
|
||||
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](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderResult[A]) Kleisli[R, B] {
|
||||
|
||||
215
v2/context/readerresult/logging.go
Normal file
215
v2/context/readerresult/logging.go
Normal 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))
|
||||
}
|
||||
302
v2/context/readerresult/logging_test.go
Normal file
302
v2/context/readerresult/logging_test.go
Normal 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")
|
||||
}
|
||||
@@ -18,9 +18,17 @@ package readerresult
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
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] {
|
||||
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] {
|
||||
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] {
|
||||
return readereither.Chain(f)
|
||||
return readereither.Chain(F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
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] {
|
||||
return readereither.OrElse(onLeft)
|
||||
return readereither.OrElse(F.Flow2(onLeft, WithContext))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
315
v2/context/readerresult/reader_test.go
Normal file
315
v2/context/readerresult/reader_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
106
v2/context/readerresult/rec.go
Normal file
106
v2/context/readerresult/rec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
498
v2/context/readerresult/rec_test.go
Normal file
498
v2/context/readerresult/rec_test.go
Normal 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)
|
||||
}
|
||||
84
v2/context/readerresult/retry.go
Normal file
84
v2/context/readerresult/retry.go
Normal 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,
|
||||
)
|
||||
|
||||
}
|
||||
@@ -13,13 +13,40 @@
|
||||
// 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 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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/reader"
|
||||
"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[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[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]
|
||||
)
|
||||
|
||||
9917
v2/coverage.out
9917
v2/coverage.out
File diff suppressed because it is too large
Load Diff
@@ -103,11 +103,11 @@ func (t *token[T]) Unerase(val any) Result[T] {
|
||||
func (t *token[T]) ProviderFactory() Option[DIE.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}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
|
||||
// Example:
|
||||
//
|
||||
// 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.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:
|
||||
//
|
||||
// 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.Left[string](fmt.Errorf("empty at index %d", i))
|
||||
|
||||
@@ -34,7 +34,7 @@ func Curry0[R any](f func() (R, error)) func() Either[error, R] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
||||
// parse := strconv.Atoi
|
||||
// curried := either.Curry1(parse)
|
||||
// result := curried("42") // Right(42)
|
||||
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] {
|
||||
|
||||
@@ -19,6 +19,21 @@
|
||||
// - Left represents an error or failure case (type E)
|
||||
// - 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
|
||||
//
|
||||
// The Either type is a discriminated union that can hold either a Left value (typically an error)
|
||||
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
"testing"
|
||||
|
||||
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"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -305,7 +306,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
// Test TraverseArrayWithIndex
|
||||
func TestTraverseArrayWithIndex(t *testing.T) {
|
||||
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 Left[string](fmt.Errorf("empty at index %d", i))
|
||||
@@ -334,7 +335,7 @@ func TestTraverseRecord(t *testing.T) {
|
||||
// Test TraverseRecordWithIndex
|
||||
func TestTraverseRecordWithIndex(t *testing.T) {
|
||||
validate := func(k string, v string) Either[error, string] {
|
||||
if len(v) > 0 {
|
||||
if S.IsNonEmpty(v) {
|
||||
return Right[error](k + ":" + v)
|
||||
}
|
||||
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) {
|
||||
parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
||||
parse := strconv.Atoi
|
||||
curried := Curry1(parse)
|
||||
result := curried("42")
|
||||
assert.Equal(t, Right[error](42), result)
|
||||
@@ -645,7 +646,7 @@ func TestAltSemigroup(t *testing.T) {
|
||||
|
||||
// Test AlternativeMonoid
|
||||
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)
|
||||
|
||||
result := m.Concat(Right[error](1), Right[error](2))
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
IO "github.com/IBM/fp-go/v2/io"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -120,10 +119,3 @@ func TestStringer(t *testing.T) {
|
||||
var s fmt.Stringer = &e
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,19 @@ package either
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
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] {
|
||||
return Fold(
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
package either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -35,3 +38,139 @@ func TestLogger(t *testing.T) {
|
||||
|
||||
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
34
v2/either/rec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ import (
|
||||
// increment := N.Add(1)
|
||||
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
|
||||
// // result(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
|
||||
func MonadAp[A any](fab, fa Endomorphism[A]) Endomorphism[A] {
|
||||
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:
|
||||
// composed := endomorphism.MonadCompose(increment, double)
|
||||
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
|
||||
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
||||
func MonadChain[A any](ma, f Endomorphism[A]) Endomorphism[A] {
|
||||
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 }
|
||||
// chained := endomorphism.MonadChainFirst(double, log)
|
||||
// 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 {
|
||||
result := ma(a)
|
||||
f(result) // Apply f for its effect
|
||||
|
||||
@@ -72,9 +72,7 @@ func TestFromStrictEquals(t *testing.T) {
|
||||
|
||||
func TestFromEquals(t *testing.T) {
|
||||
t.Run("case-insensitive string equality", func(t *testing.T) {
|
||||
caseInsensitiveEq := FromEquals(func(a, b string) bool {
|
||||
return strings.EqualFold(a, b)
|
||||
})
|
||||
caseInsensitiveEq := FromEquals(strings.EqualFold)
|
||||
|
||||
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) {
|
||||
caseInsensitiveEq := FromEquals(func(a, b string) bool {
|
||||
return strings.EqualFold(a, b)
|
||||
})
|
||||
caseInsensitiveEq := FromEquals(strings.EqualFold)
|
||||
|
||||
personEqByNameCI := Contramap(func(p Person) string {
|
||||
return p.Name
|
||||
|
||||
@@ -53,7 +53,10 @@ func Identity[A any](a A) A {
|
||||
//
|
||||
// getMessage := Constant("Hello")
|
||||
// msg := getMessage() // "Hello"
|
||||
//
|
||||
//go:inline
|
||||
func Constant[A any](a A) func() A {
|
||||
//go:inline
|
||||
return func() A {
|
||||
return a
|
||||
}
|
||||
@@ -81,7 +84,10 @@ func Constant[A any](a A) func() A {
|
||||
//
|
||||
// defaultName := Constant1[int, string]("Unknown")
|
||||
// name := defaultName(42) // "Unknown"
|
||||
//
|
||||
//go:inline
|
||||
func Constant1[B, A any](a A) func(B) A {
|
||||
//go:inline
|
||||
return func(_ B) A {
|
||||
return a
|
||||
}
|
||||
@@ -107,7 +113,10 @@ func Constant1[B, A any](a A) func(B) A {
|
||||
//
|
||||
// alwaysTrue := Constant2[int, string, bool](true)
|
||||
// result := alwaysTrue(42, "test") // true
|
||||
//
|
||||
//go:inline
|
||||
func Constant2[B, C, A any](a A) func(B, C) A {
|
||||
//go:inline
|
||||
return func(_ B, _ C) A {
|
||||
return a
|
||||
}
|
||||
@@ -128,6 +137,8 @@ func Constant2[B, C, A any](a A) func(B, C) A {
|
||||
//
|
||||
// value := 42
|
||||
// IsNil(&value) // false
|
||||
//
|
||||
//go:inline
|
||||
func IsNil[A any](a *A) bool {
|
||||
return a == nil
|
||||
}
|
||||
@@ -149,6 +160,8 @@ func IsNil[A any](a *A) bool {
|
||||
//
|
||||
// value := 42
|
||||
// IsNonNil(&value) // true
|
||||
//
|
||||
//go:inline
|
||||
func IsNonNil[A any](a *A) bool {
|
||||
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(true, 100) // true
|
||||
//
|
||||
//go:inline
|
||||
func First[T1, T2 any](t1 T1, _ T2) T1 {
|
||||
return t1
|
||||
}
|
||||
@@ -231,6 +246,14 @@ func First[T1, T2 any](t1 T1, _ T2) T1 {
|
||||
//
|
||||
// result := Second(42, "hello") // "hello"
|
||||
// result := Second(true, 100) // 100
|
||||
//
|
||||
//go:inline
|
||||
func Second[T1, T2 any](_ T1, t2 T2) T2 {
|
||||
return t2
|
||||
}
|
||||
|
||||
// Zero returns the zero value of the given type.
|
||||
func Zero[A comparable]() A {
|
||||
var zero A
|
||||
return zero
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// 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 {
|
||||
//go:inline
|
||||
return func(t0 T0) func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return f(t0, t1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ package function
|
||||
// )
|
||||
// result := classify(5) // "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 {
|
||||
if pred(a) {
|
||||
return onTrue(a)
|
||||
|
||||
@@ -246,7 +246,7 @@ func (builder *Builder) GetTargetURL() Result[string] {
|
||||
parseQuery,
|
||||
result.Map(F.Flow2(
|
||||
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)),
|
||||
)
|
||||
|
||||
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()
|
||||
return F.Pipe1(
|
||||
value,
|
||||
O.Fold(del(cpy), set(cpy)),
|
||||
)
|
||||
})
|
||||
}, fmt.Sprintf("HttpHeader[%s]", name))
|
||||
}
|
||||
|
||||
// WithHeader creates a [Endomorphism] for a certain header
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
/*
|
||||
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
|
||||
|
||||
The Identity monad is a trivial monad that simply wraps a value without adding
|
||||
|
||||
1033
v2/idiomatic/context/readerresult/README.md
Normal file
1033
v2/idiomatic/context/readerresult/README.md
Normal file
File diff suppressed because it is too large
Load Diff
75
v2/idiomatic/context/readerresult/array.go
Normal file
75
v2/idiomatic/context/readerresult/array.go
Normal 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)
|
||||
}
|
||||
456
v2/idiomatic/context/readerresult/bind.go
Normal file
456
v2/idiomatic/context/readerresult/bind.go
Normal 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)
|
||||
}
|
||||
627
v2/idiomatic/context/readerresult/bind_test.go
Normal file
627
v2/idiomatic/context/readerresult/bind_test.go
Normal 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)
|
||||
}
|
||||
409
v2/idiomatic/context/readerresult/bracket.go
Normal file
409
v2/idiomatic/context/readerresult/bracket.go
Normal 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])
|
||||
}
|
||||
630
v2/idiomatic/context/readerresult/bracket_test.go
Normal file
630
v2/idiomatic/context/readerresult/bracket_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
113
v2/idiomatic/context/readerresult/cancel.go
Normal file
113
v2/idiomatic/context/readerresult/cancel.go
Normal 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,
|
||||
)
|
||||
}
|
||||
210
v2/idiomatic/context/readerresult/curry.go
Normal file
210
v2/idiomatic/context/readerresult/curry.go
Normal 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)
|
||||
}
|
||||
207
v2/idiomatic/context/readerresult/doc.go
Normal file
207
v2/idiomatic/context/readerresult/doc.go
Normal 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
|
||||
738
v2/idiomatic/context/readerresult/examples_bind_test.go
Normal file
738
v2/idiomatic/context/readerresult/examples_bind_test.go
Normal 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>
|
||||
}
|
||||
114
v2/idiomatic/context/readerresult/examples_reader_test.go
Normal file
114
v2/idiomatic/context/readerresult/examples_reader_test.go
Normal 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>
|
||||
}
|
||||
107
v2/idiomatic/context/readerresult/flip.go
Normal file
107
v2/idiomatic/context/readerresult/flip.go
Normal 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)
|
||||
}
|
||||
134
v2/idiomatic/context/readerresult/from.go
Normal file
134
v2/idiomatic/context/readerresult/from.go
Normal 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)
|
||||
}
|
||||
1491
v2/idiomatic/context/readerresult/gen_lens_test.go
Normal file
1491
v2/idiomatic/context/readerresult/gen_lens_test.go
Normal file
File diff suppressed because it is too large
Load Diff
3
v2/idiomatic/context/readerresult/lens.go
Normal file
3
v2/idiomatic/context/readerresult/lens.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package readerresult
|
||||
|
||||
//go:generate go run ../../../main.go lens --dir . --filename gen_lens.go
|
||||
120
v2/idiomatic/context/readerresult/monoid.go
Normal file
120
v2/idiomatic/context/readerresult/monoid.go
Normal 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)
|
||||
}
|
||||
692
v2/idiomatic/context/readerresult/reader.go
Normal file
692
v2/idiomatic/context/readerresult/reader.go
Normal 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)
|
||||
})
|
||||
}
|
||||
1374
v2/idiomatic/context/readerresult/reader_test.go
Normal file
1374
v2/idiomatic/context/readerresult/reader_test.go
Normal file
File diff suppressed because it is too large
Load Diff
146
v2/idiomatic/context/readerresult/retry.go
Normal file
146
v2/idiomatic/context/readerresult/retry.go
Normal 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,
|
||||
)
|
||||
}
|
||||
482
v2/idiomatic/context/readerresult/retry_test.go
Normal file
482
v2/idiomatic/context/readerresult/retry_test.go
Normal 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")
|
||||
}
|
||||
133
v2/idiomatic/context/readerresult/sequence.go
Normal file
133
v2/idiomatic/context/readerresult/sequence.go
Normal 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)
|
||||
}
|
||||
71
v2/idiomatic/context/readerresult/types.go
Normal file
71
v2/idiomatic/context/readerresult/types.go
Normal 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]
|
||||
)
|
||||
@@ -28,7 +28,7 @@ func TestMkdir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newDir := filepath.Join(tmpDir, "testdir")
|
||||
|
||||
result := Mkdir(newDir, 0755)
|
||||
result := Mkdir(newDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -43,14 +43,14 @@ func TestMkdir(t *testing.T) {
|
||||
t.Run("mkdir with existing directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
result := Mkdir(tmpDir, 0755)
|
||||
result := Mkdir(tmpDir, 0o755)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -62,7 +62,7 @@ func TestMkdirAll(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
|
||||
|
||||
result := MkdirAll(nestedDir, 0755)
|
||||
result := MkdirAll(nestedDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -88,7 +88,7 @@ func TestMkdirAll(t *testing.T) {
|
||||
t.Run("mkdirall with existing directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
result := MkdirAll(tmpDir, 0755)
|
||||
result := MkdirAll(tmpDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
// MkdirAll should succeed even if directory exists
|
||||
@@ -100,7 +100,7 @@ func TestMkdirAll(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newDir := filepath.Join(tmpDir, "single")
|
||||
|
||||
result := MkdirAll(newDir, 0755)
|
||||
result := MkdirAll(newDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -116,11 +116,11 @@ func TestMkdirAll(t *testing.T) {
|
||||
filePath := filepath.Join(tmpDir, "file.txt")
|
||||
|
||||
// Create a file
|
||||
err := os.WriteFile(filePath, []byte("content"), 0644)
|
||||
err := os.WriteFile(filePath, []byte("content"), 0o644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to create a directory where file exists
|
||||
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755)
|
||||
result := MkdirAll(filepath.Join(filePath, "subdir"), 0o755)
|
||||
_, err = result()
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestOpen(t *testing.T) {
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write some content
|
||||
err = os.WriteFile(tmpPath, []byte("test content"), 0644)
|
||||
err = os.WriteFile(tmpPath, []byte("test content"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test Open
|
||||
@@ -127,7 +127,7 @@ func TestWriteFile(t *testing.T) {
|
||||
testPath := filepath.Join(tmpDir, "write-test.txt")
|
||||
testData := []byte("test data")
|
||||
|
||||
result := WriteFile(testPath, 0644)(testData)
|
||||
result := WriteFile(testPath, 0o644)(testData)
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -141,7 +141,7 @@ func TestWriteFile(t *testing.T) {
|
||||
|
||||
t.Run("write to invalid path", func(t *testing.T) {
|
||||
testData := []byte("test data")
|
||||
result := WriteFile("/non/existent/dir/file.txt", 0644)(testData)
|
||||
result := WriteFile("/non/existent/dir/file.txt", 0o644)(testData)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -155,12 +155,12 @@ func TestWriteFile(t *testing.T) {
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write initial content
|
||||
err = os.WriteFile(tmpPath, []byte("initial"), 0644)
|
||||
err = os.WriteFile(tmpPath, []byte("initial"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overwrite with new content
|
||||
newData := []byte("overwritten")
|
||||
result := WriteFile(tmpPath, 0644)(newData)
|
||||
result := WriteFile(tmpPath, 0o644)(newData)
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -212,7 +212,7 @@ func TestClose(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed by attempting to write
|
||||
_, writeErr := tmpFile.Write([]byte("test"))
|
||||
_, writeErr := tmpFile.WriteString("test")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestReadAll(t *testing.T) {
|
||||
largeContent[i] = byte('A' + (i % 26))
|
||||
}
|
||||
|
||||
err := os.WriteFile(testPath, largeContent, 0644)
|
||||
err := os.WriteFile(testPath, largeContent, 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := ReadAll(Open(testPath))
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestWriteAll(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed by trying to write to it
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
_, writeErr := capturedFile.WriteString("more")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestWrite(t *testing.T) {
|
||||
|
||||
useFile := func(f *os.File) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
_, err := f.Write([]byte("data"))
|
||||
_, err := f.WriteString("data")
|
||||
return "success", err
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,7 @@ func TestWrite(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
_, writeErr := capturedFile.WriteString("more")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
@@ -183,7 +183,7 @@ func TestWrite(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify file is still closed even on error
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
_, writeErr := capturedFile.WriteString("more")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ func Requester(builder *R.Builder) IOEH.Requester {
|
||||
|
||||
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
req, err := http.NewRequest(method, url, http.NoBody)
|
||||
if err == nil {
|
||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -111,7 +112,7 @@ func TestChainWithIO(t *testing.T) {
|
||||
Of("test"),
|
||||
ChainIOK(func(s string) IO[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
Reference in New Issue
Block a user