mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-19 23:42:05 +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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/IBM/fp-go/v2/option"
|
"github.com/IBM/fp-go/v2/option"
|
||||||
|
N "github.com/IBM/fp-go/v2/number"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -145,6 +146,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## ⚠️ Breaking Changes
|
||||||
|
|
||||||
### From V1 to V2
|
### From V1 to V2
|
||||||
|
|
||||||
#### 1. Generic Type Aliases
|
#### 1. Generic Type Aliases
|
||||||
|
|||||||
@@ -536,3 +536,89 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
|||||||
func Prepend[A any](head A) Operator[A, A] {
|
func Prepend[A any](head A) Operator[A, A] {
|
||||||
return G.Prepend[Operator[A, A]](head)
|
return G.Prepend[Operator[A, A]](head)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reverse returns a new slice with elements in reverse order.
|
||||||
|
// This function creates a new slice containing all elements from the input slice
|
||||||
|
// in reverse order, without modifying the original slice.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The type of elements in the slice
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - as: The input slice to reverse
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A new slice with elements in reverse order
|
||||||
|
//
|
||||||
|
// Behavior:
|
||||||
|
// - Creates a new slice with the same length as the input
|
||||||
|
// - Copies elements from the input slice in reverse order
|
||||||
|
// - Does not modify the original slice
|
||||||
|
// - Returns an empty slice if the input is empty
|
||||||
|
// - Returns a single-element slice unchanged if input has one element
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// numbers := []int{1, 2, 3, 4, 5}
|
||||||
|
// reversed := array.Reverse(numbers)
|
||||||
|
// // reversed: []int{5, 4, 3, 2, 1}
|
||||||
|
// // numbers: []int{1, 2, 3, 4, 5} (unchanged)
|
||||||
|
//
|
||||||
|
// Example with strings:
|
||||||
|
//
|
||||||
|
// words := []string{"hello", "world", "foo", "bar"}
|
||||||
|
// reversed := array.Reverse(words)
|
||||||
|
// // reversed: []string{"bar", "foo", "world", "hello"}
|
||||||
|
//
|
||||||
|
// Example with empty slice:
|
||||||
|
//
|
||||||
|
// empty := []int{}
|
||||||
|
// reversed := array.Reverse(empty)
|
||||||
|
// // reversed: []int{} (empty slice)
|
||||||
|
//
|
||||||
|
// Example with single element:
|
||||||
|
//
|
||||||
|
// single := []string{"only"}
|
||||||
|
// reversed := array.Reverse(single)
|
||||||
|
// // reversed: []string{"only"}
|
||||||
|
//
|
||||||
|
// Use cases:
|
||||||
|
// - Reversing the order of elements for display or processing
|
||||||
|
// - Implementing stack-like behavior (LIFO)
|
||||||
|
// - Processing data in reverse chronological order
|
||||||
|
// - Reversing transformation pipelines
|
||||||
|
// - Creating palindrome checks
|
||||||
|
// - Implementing undo/redo functionality
|
||||||
|
//
|
||||||
|
// Example with processing in reverse:
|
||||||
|
//
|
||||||
|
// events := []string{"start", "middle", "end"}
|
||||||
|
// reversed := array.Reverse(events)
|
||||||
|
// // Process events in reverse order
|
||||||
|
// for _, event := range reversed {
|
||||||
|
// fmt.Println(event) // Prints: "end", "middle", "start"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Example with functional composition:
|
||||||
|
//
|
||||||
|
// numbers := []int{1, 2, 3, 4, 5}
|
||||||
|
// result := F.Pipe2(
|
||||||
|
// numbers,
|
||||||
|
// array.Map(N.Mul(2)),
|
||||||
|
// array.Reverse,
|
||||||
|
// )
|
||||||
|
// // result: []int{10, 8, 6, 4, 2}
|
||||||
|
//
|
||||||
|
// Performance:
|
||||||
|
// - Time complexity: O(n) where n is the length of the slice
|
||||||
|
// - Space complexity: O(n) for the new slice
|
||||||
|
// - Does not allocate if the input slice is empty
|
||||||
|
//
|
||||||
|
// Note: This function is immutable - it does not modify the original slice.
|
||||||
|
// If you need to reverse a slice in-place, consider using a different approach
|
||||||
|
// or modifying the slice directly.
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
|
func Reverse[A any](as []A) []A {
|
||||||
|
return G.Reverse(as)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
"github.com/IBM/fp-go/v2/internal/utils"
|
"github.com/IBM/fp-go/v2/internal/utils"
|
||||||
|
N "github.com/IBM/fp-go/v2/number"
|
||||||
O "github.com/IBM/fp-go/v2/option"
|
O "github.com/IBM/fp-go/v2/option"
|
||||||
S "github.com/IBM/fp-go/v2/string"
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
T "github.com/IBM/fp-go/v2/tuple"
|
T "github.com/IBM/fp-go/v2/tuple"
|
||||||
@@ -214,3 +215,262 @@ func ExampleFoldMap() {
|
|||||||
// Output: ABC
|
// Output: ABC
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReverse tests the Reverse function
|
||||||
|
func TestReverse(t *testing.T) {
|
||||||
|
t.Run("Reverse integers", func(t *testing.T) {
|
||||||
|
input := []int{1, 2, 3, 4, 5}
|
||||||
|
result := Reverse(input)
|
||||||
|
expected := []int{5, 4, 3, 2, 1}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse strings", func(t *testing.T) {
|
||||||
|
input := []string{"hello", "world", "foo", "bar"}
|
||||||
|
result := Reverse(input)
|
||||||
|
expected := []string{"bar", "foo", "world", "hello"}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse empty slice", func(t *testing.T) {
|
||||||
|
input := []int{}
|
||||||
|
result := Reverse(input)
|
||||||
|
assert.Equal(t, []int{}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse single element", func(t *testing.T) {
|
||||||
|
input := []string{"only"}
|
||||||
|
result := Reverse(input)
|
||||||
|
assert.Equal(t, []string{"only"}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse two elements", func(t *testing.T) {
|
||||||
|
input := []int{1, 2}
|
||||||
|
result := Reverse(input)
|
||||||
|
assert.Equal(t, []int{2, 1}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Does not modify original slice", func(t *testing.T) {
|
||||||
|
original := []int{1, 2, 3, 4, 5}
|
||||||
|
originalCopy := []int{1, 2, 3, 4, 5}
|
||||||
|
_ = Reverse(original)
|
||||||
|
assert.Equal(t, originalCopy, original)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse with floats", func(t *testing.T) {
|
||||||
|
input := []float64{1.1, 2.2, 3.3}
|
||||||
|
result := Reverse(input)
|
||||||
|
expected := []float64{3.3, 2.2, 1.1}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse with structs", func(t *testing.T) {
|
||||||
|
type Person struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
}
|
||||||
|
input := []Person{
|
||||||
|
{"Alice", 30},
|
||||||
|
{"Bob", 25},
|
||||||
|
{"Charlie", 35},
|
||||||
|
}
|
||||||
|
result := Reverse(input)
|
||||||
|
expected := []Person{
|
||||||
|
{"Charlie", 35},
|
||||||
|
{"Bob", 25},
|
||||||
|
{"Alice", 30},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse with pointers", func(t *testing.T) {
|
||||||
|
a, b, c := 1, 2, 3
|
||||||
|
input := []*int{&a, &b, &c}
|
||||||
|
result := Reverse(input)
|
||||||
|
assert.Equal(t, []*int{&c, &b, &a}, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Double reverse returns original order", func(t *testing.T) {
|
||||||
|
original := []int{1, 2, 3, 4, 5}
|
||||||
|
reversed := Reverse(original)
|
||||||
|
doubleReversed := Reverse(reversed)
|
||||||
|
assert.Equal(t, original, doubleReversed)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse with large slice", func(t *testing.T) {
|
||||||
|
input := MakeBy(1000, F.Identity[int])
|
||||||
|
result := Reverse(input)
|
||||||
|
|
||||||
|
// Check first and last elements
|
||||||
|
assert.Equal(t, 999, result[0])
|
||||||
|
assert.Equal(t, 0, result[999])
|
||||||
|
|
||||||
|
// Check length
|
||||||
|
assert.Equal(t, 1000, len(result))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse palindrome", func(t *testing.T) {
|
||||||
|
input := []int{1, 2, 3, 2, 1}
|
||||||
|
result := Reverse(input)
|
||||||
|
assert.Equal(t, input, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReverseComposition tests Reverse with other array operations
|
||||||
|
func TestReverseComposition(t *testing.T) {
|
||||||
|
t.Run("Reverse after Map", func(t *testing.T) {
|
||||||
|
input := []int{1, 2, 3, 4, 5}
|
||||||
|
result := F.Pipe2(
|
||||||
|
input,
|
||||||
|
Map(N.Mul(2)),
|
||||||
|
Reverse[int],
|
||||||
|
)
|
||||||
|
expected := []int{10, 8, 6, 4, 2}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Map after Reverse", func(t *testing.T) {
|
||||||
|
input := []int{1, 2, 3, 4, 5}
|
||||||
|
result := F.Pipe2(
|
||||||
|
input,
|
||||||
|
Reverse[int],
|
||||||
|
Map(N.Mul(2)),
|
||||||
|
)
|
||||||
|
expected := []int{10, 8, 6, 4, 2}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse with Filter", func(t *testing.T) {
|
||||||
|
input := []int{1, 2, 3, 4, 5, 6}
|
||||||
|
result := F.Pipe2(
|
||||||
|
input,
|
||||||
|
Filter(func(n int) bool { return n%2 == 0 }),
|
||||||
|
Reverse[int],
|
||||||
|
)
|
||||||
|
expected := []int{6, 4, 2}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse with Reduce", func(t *testing.T) {
|
||||||
|
input := []string{"a", "b", "c"}
|
||||||
|
reversed := Reverse(input)
|
||||||
|
result := Reduce(func(acc, val string) string {
|
||||||
|
return acc + val
|
||||||
|
}, "")(reversed)
|
||||||
|
assert.Equal(t, "cba", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse with Flatten", func(t *testing.T) {
|
||||||
|
input := [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||||||
|
result := F.Pipe2(
|
||||||
|
input,
|
||||||
|
Reverse[[]int],
|
||||||
|
Flatten[int],
|
||||||
|
)
|
||||||
|
expected := []int{5, 6, 3, 4, 1, 2}
|
||||||
|
assert.Equal(t, expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReverseUseCases demonstrates practical use cases for Reverse
|
||||||
|
func TestReverseUseCases(t *testing.T) {
|
||||||
|
t.Run("Process events in reverse chronological order", func(t *testing.T) {
|
||||||
|
events := []string{"2024-01-01", "2024-01-02", "2024-01-03"}
|
||||||
|
reversed := Reverse(events)
|
||||||
|
|
||||||
|
// Most recent first
|
||||||
|
assert.Equal(t, "2024-01-03", reversed[0])
|
||||||
|
assert.Equal(t, "2024-01-01", reversed[2])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Implement stack behavior (LIFO)", func(t *testing.T) {
|
||||||
|
stack := []int{1, 2, 3, 4, 5}
|
||||||
|
reversed := Reverse(stack)
|
||||||
|
|
||||||
|
// Pop from reversed (LIFO)
|
||||||
|
assert.Equal(t, 5, reversed[0])
|
||||||
|
assert.Equal(t, 4, reversed[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse string characters", func(t *testing.T) {
|
||||||
|
chars := []rune("hello")
|
||||||
|
reversed := Reverse(chars)
|
||||||
|
result := string(reversed)
|
||||||
|
assert.Equal(t, "olleh", result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Check palindrome", func(t *testing.T) {
|
||||||
|
word := []rune("racecar")
|
||||||
|
reversed := Reverse(word)
|
||||||
|
assert.Equal(t, word, reversed)
|
||||||
|
|
||||||
|
notPalindrome := []rune("hello")
|
||||||
|
reversedNot := Reverse(notPalindrome)
|
||||||
|
assert.NotEqual(t, notPalindrome, reversedNot)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Reverse transformation pipeline", func(t *testing.T) {
|
||||||
|
// Apply transformations in reverse order
|
||||||
|
numbers := []int{1, 2, 3}
|
||||||
|
|
||||||
|
// Normal: add 10, then multiply by 2
|
||||||
|
normal := F.Pipe2(
|
||||||
|
numbers,
|
||||||
|
Map(N.Add(10)),
|
||||||
|
Map(N.Mul(2)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reversed order of operations
|
||||||
|
reversed := F.Pipe2(
|
||||||
|
numbers,
|
||||||
|
Map(N.Mul(2)),
|
||||||
|
Map(N.Add(10)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NotEqual(t, normal, reversed)
|
||||||
|
assert.Equal(t, []int{22, 24, 26}, normal)
|
||||||
|
assert.Equal(t, []int{12, 14, 16}, reversed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReverseProperties tests mathematical properties of Reverse
|
||||||
|
func TestReverseProperties(t *testing.T) {
|
||||||
|
t.Run("Involution property: Reverse(Reverse(x)) == x", func(t *testing.T) {
|
||||||
|
testCases := [][]int{
|
||||||
|
{1, 2, 3, 4, 5},
|
||||||
|
{1},
|
||||||
|
{},
|
||||||
|
{1, 2},
|
||||||
|
{5, 4, 3, 2, 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, original := range testCases {
|
||||||
|
result := Reverse(Reverse(original))
|
||||||
|
assert.Equal(t, original, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Length preservation: len(Reverse(x)) == len(x)", func(t *testing.T) {
|
||||||
|
testCases := [][]int{
|
||||||
|
{1, 2, 3, 4, 5},
|
||||||
|
{1},
|
||||||
|
{},
|
||||||
|
MakeBy(100, F.Identity[int]),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, input := range testCases {
|
||||||
|
result := Reverse(input)
|
||||||
|
assert.Equal(t, len(input), len(result))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("First element becomes last", func(t *testing.T) {
|
||||||
|
input := []int{1, 2, 3, 4, 5}
|
||||||
|
result := Reverse(input)
|
||||||
|
|
||||||
|
if len(input) > 0 {
|
||||||
|
assert.Equal(t, input[0], result[len(result)-1])
|
||||||
|
assert.Equal(t, input[len(input)-1], result[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
E "github.com/IBM/fp-go/v2/eq"
|
E "github.com/IBM/fp-go/v2/eq"
|
||||||
)
|
)
|
||||||
|
|
||||||
func equals[T any](left []T, right []T, eq func(T, T) bool) bool {
|
func equals[T any](left, right []T, eq func(T, T) bool) bool {
|
||||||
if len(left) != len(right) {
|
if len(left) != len(right) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,22 +140,27 @@ func Empty[GA ~[]A, A any]() GA {
|
|||||||
return array.Empty[GA]()
|
return array.Empty[GA]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func UpsertAt[GA ~[]A, A any](a A) func(GA) GA {
|
func UpsertAt[GA ~[]A, A any](a A) func(GA) GA {
|
||||||
return array.UpsertAt[GA](a)
|
return array.UpsertAt[GA](a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB {
|
func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB {
|
||||||
return array.MonadMap[GA, GB](as, f)
|
return array.MonadMap[GA, GB](as, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func Map[GA ~[]A, GB ~[]B, A, B any](f func(a A) B) func(GA) GB {
|
func Map[GA ~[]A, GB ~[]B, A, B any](f func(a A) B) func(GA) GB {
|
||||||
return array.Map[GA, GB](f)
|
return array.Map[GA, GB](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(int, A) B) GB {
|
func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(int, A) B) GB {
|
||||||
return array.MonadMapWithIndex[GA, GB](as, f)
|
return array.MonadMapWithIndex[GA, GB](as, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func MapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
|
func MapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
|
||||||
return F.Bind2nd(MonadMapWithIndex[GA, GB, A, B], f)
|
return F.Bind2nd(MonadMapWithIndex[GA, GB, A, B], f)
|
||||||
}
|
}
|
||||||
@@ -297,7 +302,7 @@ func MatchLeft[AS ~[]A, A, B any](onEmpty func() B, onNonEmpty func(A, AS) B) fu
|
|||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func Slice[AS ~[]A, A any](start int, end int) func(AS) AS {
|
func Slice[AS ~[]A, A any](start, end int) func(AS) AS {
|
||||||
return array.Slice[AS](start, end)
|
return array.Slice[AS](start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +366,12 @@ func Flap[FAB ~func(A) B, GFAB ~[]FAB, GB ~[]B, A, B any](a A) func(GFAB) GB {
|
|||||||
return FC.Flap(Map[GFAB, GB], a)
|
return FC.Flap(Map[GFAB, GB], a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
|
func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
|
||||||
return array.Prepend[ENDO](head)
|
return array.Prepend[ENDO](head)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func Reverse[GT ~[]T, T any](as GT) GT {
|
||||||
|
return array.Reverse(as)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,14 +18,11 @@ package nonempty
|
|||||||
import (
|
import (
|
||||||
G "github.com/IBM/fp-go/v2/array/generic"
|
G "github.com/IBM/fp-go/v2/array/generic"
|
||||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
|
||||||
"github.com/IBM/fp-go/v2/internal/array"
|
"github.com/IBM/fp-go/v2/internal/array"
|
||||||
|
"github.com/IBM/fp-go/v2/option"
|
||||||
S "github.com/IBM/fp-go/v2/semigroup"
|
S "github.com/IBM/fp-go/v2/semigroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NonEmptyArray represents an array with at least one element
|
|
||||||
type NonEmptyArray[A any] []A
|
|
||||||
|
|
||||||
// Of constructs a single element array
|
// Of constructs a single element array
|
||||||
func Of[A any](first A) NonEmptyArray[A] {
|
func Of[A any](first A) NonEmptyArray[A] {
|
||||||
return G.Of[NonEmptyArray[A]](first)
|
return G.Of[NonEmptyArray[A]](first)
|
||||||
@@ -44,20 +41,24 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
|
|||||||
return buffer
|
return buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
|
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
|
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
|
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
|
||||||
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
|
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Map[A, B any](f func(a A) B) func(NonEmptyArray[A]) NonEmptyArray[B] {
|
//go:inline
|
||||||
return F.Bind2nd(MonadMap[A, B], f)
|
func Map[A, B any](f func(a A) B) Operator[A, B] {
|
||||||
|
return G.Map[NonEmptyArray[A], NonEmptyArray[B]](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
|
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
|
||||||
@@ -72,22 +73,27 @@ func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func Tail[A any](as NonEmptyArray[A]) []A {
|
func Tail[A any](as NonEmptyArray[A]) []A {
|
||||||
return as[1:]
|
return as[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func Head[A any](as NonEmptyArray[A]) A {
|
func Head[A any](as NonEmptyArray[A]) A {
|
||||||
return as[0]
|
return as[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func First[A any](as NonEmptyArray[A]) A {
|
func First[A any](as NonEmptyArray[A]) A {
|
||||||
return as[0]
|
return as[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func Last[A any](as NonEmptyArray[A]) A {
|
func Last[A any](as NonEmptyArray[A]) A {
|
||||||
return as[len(as)-1]
|
return as[len(as)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
func Size[A any](as NonEmptyArray[A]) int {
|
func Size[A any](as NonEmptyArray[A]) int {
|
||||||
return G.Size(as)
|
return G.Size(as)
|
||||||
}
|
}
|
||||||
@@ -96,11 +102,11 @@ func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
|
|||||||
return G.Flatten(mma)
|
return G.Flatten(mma)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MonadChain[A, B any](fa NonEmptyArray[A], f func(a A) NonEmptyArray[B]) NonEmptyArray[B] {
|
func MonadChain[A, B any](fa NonEmptyArray[A], f Kleisli[A, B]) NonEmptyArray[B] {
|
||||||
return G.MonadChain(fa, f)
|
return G.MonadChain(fa, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Chain[A, B any](f func(A) NonEmptyArray[B]) func(NonEmptyArray[A]) NonEmptyArray[B] {
|
func Chain[A, B any](f func(A) NonEmptyArray[B]) Operator[A, B] {
|
||||||
return G.Chain[NonEmptyArray[A]](f)
|
return G.Chain[NonEmptyArray[A]](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,3 +140,89 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
|
|||||||
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
|
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
|
||||||
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
|
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToNonEmptyArray attempts to convert a regular slice into a NonEmptyArray.
|
||||||
|
// This function provides a safe way to create a NonEmptyArray from a slice that might be empty,
|
||||||
|
// returning an Option type to handle the case where the input slice is empty.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The element type of the array
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - as: A regular slice that may or may not be empty
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - Option[NonEmptyArray[A]]: Some(NonEmptyArray) if the input slice is non-empty, None if empty
|
||||||
|
//
|
||||||
|
// Behavior:
|
||||||
|
// - If the input slice is empty, returns None
|
||||||
|
// - If the input slice has at least one element, wraps it in Some and returns it as a NonEmptyArray
|
||||||
|
// - The conversion is a type cast, so no data is copied
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// // Convert non-empty slice
|
||||||
|
// numbers := []int{1, 2, 3}
|
||||||
|
// result := ToNonEmptyArray(numbers) // Some(NonEmptyArray[1, 2, 3])
|
||||||
|
//
|
||||||
|
// // Convert empty slice
|
||||||
|
// empty := []int{}
|
||||||
|
// result := ToNonEmptyArray(empty) // None
|
||||||
|
//
|
||||||
|
// // Use with Option methods
|
||||||
|
// numbers := []int{1, 2, 3}
|
||||||
|
// result := ToNonEmptyArray(numbers)
|
||||||
|
// if O.IsSome(result) {
|
||||||
|
// nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||||
|
// head := Head(nea) // 1
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Use cases:
|
||||||
|
// - Safely converting user input or external data to NonEmptyArray
|
||||||
|
// - Validating that a collection has at least one element before processing
|
||||||
|
// - Converting results from functions that return regular slices
|
||||||
|
// - Ensuring type safety when working with collections that must not be empty
|
||||||
|
//
|
||||||
|
// Example with validation:
|
||||||
|
//
|
||||||
|
// func processItems(items []string) Option[string] {
|
||||||
|
// return F.Pipe2(
|
||||||
|
// items,
|
||||||
|
// ToNonEmptyArray[string],
|
||||||
|
// O.Map(func(nea NonEmptyArray[string]) string {
|
||||||
|
// return Head(nea) // Safe to get head since we know it's non-empty
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Example with error handling:
|
||||||
|
//
|
||||||
|
// items := []int{1, 2, 3}
|
||||||
|
// result := ToNonEmptyArray(items)
|
||||||
|
// switch {
|
||||||
|
// case O.IsSome(result):
|
||||||
|
// nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||||
|
// fmt.Println("First item:", Head(nea))
|
||||||
|
// case O.IsNone(result):
|
||||||
|
// fmt.Println("Array is empty")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Example with chaining:
|
||||||
|
//
|
||||||
|
// // Process only if non-empty
|
||||||
|
// result := F.Pipe3(
|
||||||
|
// []int{1, 2, 3},
|
||||||
|
// ToNonEmptyArray[int],
|
||||||
|
// O.Map(Map(func(x int) int { return x * 2 })),
|
||||||
|
// O.Map(Head[int]),
|
||||||
|
// ) // Some(2)
|
||||||
|
//
|
||||||
|
// Note: This function is particularly useful when working with APIs or functions
|
||||||
|
// that return regular slices but you need the type-level guarantee that the
|
||||||
|
// collection is non-empty for subsequent operations.
|
||||||
|
func ToNonEmptyArray[A any](as []A) Option[NonEmptyArray[A]] {
|
||||||
|
if G.IsEmpty(as) {
|
||||||
|
return option.None[NonEmptyArray[A]]()
|
||||||
|
}
|
||||||
|
return option.Some(NonEmptyArray[A](as))
|
||||||
|
}
|
||||||
|
|||||||
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/optics/prism"
|
||||||
"github.com/IBM/fp-go/v2/option"
|
"github.com/IBM/fp-go/v2/option"
|
||||||
"github.com/IBM/fp-go/v2/result"
|
"github.com/IBM/fp-go/v2/result"
|
||||||
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEqual(t *testing.T) {
|
func TestEqual(t *testing.T) {
|
||||||
@@ -334,7 +335,7 @@ func TestThat(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("should work with string predicates", func(t *testing.T) {
|
t.Run("should work with string predicates", func(t *testing.T) {
|
||||||
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' }
|
startsWithH := func(s string) bool { return S.IsNonEmpty(s) && s[0] == 'h' }
|
||||||
result := That(startsWithH)("hello")(t)
|
result := That(startsWithH)("hello")(t)
|
||||||
if !result {
|
if !result {
|
||||||
t.Error("Expected That to pass for string predicate")
|
t.Error("Expected That to pass for string predicate")
|
||||||
@@ -484,7 +485,7 @@ func TestLocal(t *testing.T) {
|
|||||||
t.Run("should compose with other assertions", func(t *testing.T) {
|
t.Run("should compose with other assertions", func(t *testing.T) {
|
||||||
// Create multiple focused assertions
|
// Create multiple focused assertions
|
||||||
nameNotEmpty := Local(func(u User) string { return u.Name })(
|
nameNotEmpty := Local(func(u User) string { return u.Name })(
|
||||||
That(func(name string) bool { return len(name) > 0 }),
|
That(S.IsNonEmpty),
|
||||||
)
|
)
|
||||||
ageInRange := Local(func(u User) int { return u.Age })(
|
ageInRange := Local(func(u User) int { return u.Age })(
|
||||||
That(func(age int) bool { return age >= 18 && age <= 100 }),
|
That(func(age int) bool { return age >= 18 && age <= 100 }),
|
||||||
|
|||||||
169
v2/cli/lens.go
169
v2/cli/lens.go
@@ -27,13 +27,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
C "github.com/urfave/cli/v2"
|
C "github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
keyLensDir = "dir"
|
keyLensDir = "dir"
|
||||||
keyVerbose = "verbose"
|
keyVerbose = "verbose"
|
||||||
lensAnnotation = "fp-go:Lens"
|
keyIncludeTestFile = "include-test-files"
|
||||||
|
lensAnnotation = "fp-go:Lens"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -49,6 +51,13 @@ var (
|
|||||||
Value: false,
|
Value: false,
|
||||||
Usage: "Enable verbose output",
|
Usage: "Enable verbose output",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flagIncludeTestFiles = &C.BoolFlag{
|
||||||
|
Name: keyIncludeTestFile,
|
||||||
|
Aliases: []string{"t"},
|
||||||
|
Value: false,
|
||||||
|
Usage: "Include test files (*_test.go) when scanning for annotated types",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// structInfo holds information about a struct that needs lens generation
|
// structInfo holds information about a struct that needs lens generation
|
||||||
@@ -67,6 +76,7 @@ type fieldInfo struct {
|
|||||||
BaseType string // TypeName without leading * for pointer types
|
BaseType string // TypeName without leading * for pointer types
|
||||||
IsOptional bool // true if field is a pointer or has json omitempty tag
|
IsOptional bool // true if field is a pointer or has json omitempty tag
|
||||||
IsComparable bool // true if the type is comparable (can use ==)
|
IsComparable bool // true if the type is comparable (can use ==)
|
||||||
|
IsEmbedded bool // true if this field comes from an embedded struct
|
||||||
}
|
}
|
||||||
|
|
||||||
// templateData holds data for template rendering
|
// templateData holds data for template rendering
|
||||||
@@ -80,12 +90,12 @@ const lensStructTemplate = `
|
|||||||
type {{.Name}}Lenses{{.TypeParams}} struct {
|
type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||||
// mandatory fields
|
// mandatory fields
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
{{.Name}} __lens.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||||
{{- end}}
|
{{- end}}
|
||||||
// optional fields
|
// optional fields
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{- if .IsComparable}}
|
{{- if .IsComparable}}
|
||||||
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
{{.Name}}O __lens_option.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
}
|
}
|
||||||
@@ -94,13 +104,24 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
|
|||||||
type {{.Name}}RefLenses{{.TypeParams}} struct {
|
type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||||
// mandatory fields
|
// mandatory fields
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
{{.Name}} __lens.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||||
{{- end}}
|
{{- end}}
|
||||||
// optional fields
|
// optional fields
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{- if .IsComparable}}
|
{{- if .IsComparable}}
|
||||||
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||||
{{- end}}
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
// prisms
|
||||||
|
{{- range .Fields}}
|
||||||
|
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
|
||||||
|
type {{.Name}}Prisms{{.TypeParams}} struct {
|
||||||
|
{{- range .Fields}}
|
||||||
|
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||||
{{- end}}
|
{{- end}}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
@@ -110,15 +131,16 @@ const lensConstructorTemplate = `
|
|||||||
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||||
// mandatory lenses
|
// mandatory lenses
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
lens{{.Name}} := L.MakeLens(
|
lens{{.Name}} := __lens.MakeLensWithName(
|
||||||
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||||
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||||
|
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||||
)
|
)
|
||||||
{{- end}}
|
{{- end}}
|
||||||
// optional lenses
|
// optional lenses
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{- if .IsComparable}}
|
{{- if .IsComparable}}
|
||||||
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
lens{{.Name}}O := __lens_option.FromIso[{{$.Name}}{{$.TypeParamNames}}](__iso_option.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
return {{.Name}}Lenses{{.TypeParamNames}}{
|
return {{.Name}}Lenses{{.TypeParamNames}}{
|
||||||
@@ -140,21 +162,23 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
|||||||
// mandatory lenses
|
// mandatory lenses
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{- if .IsComparable}}
|
{{- if .IsComparable}}
|
||||||
lens{{.Name}} := L.MakeLensStrict(
|
lens{{.Name}} := __lens.MakeLensStrictWithName(
|
||||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||||
|
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
|
||||||
)
|
)
|
||||||
{{- else}}
|
{{- else}}
|
||||||
lens{{.Name}} := L.MakeLensRef(
|
lens{{.Name}} := __lens.MakeLensRefWithName(
|
||||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||||
|
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
|
||||||
)
|
)
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
// optional lenses
|
// optional lenses
|
||||||
{{- range .Fields}}
|
{{- range .Fields}}
|
||||||
{{- if .IsComparable}}
|
{{- if .IsComparable}}
|
||||||
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
lens{{.Name}}O := __lens_option.FromIso[*{{$.Name}}{{$.TypeParamNames}}](__iso_option.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
return {{.Name}}RefLenses{{.TypeParamNames}}{
|
return {{.Name}}RefLenses{{.TypeParamNames}}{
|
||||||
@@ -170,6 +194,47 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
|||||||
{{- end}}
|
{{- end}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
|
||||||
|
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
|
||||||
|
{{- range .Fields}}
|
||||||
|
{{- if .IsComparable}}
|
||||||
|
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
|
||||||
|
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||||
|
func(s {{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
|
||||||
|
func(v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} {
|
||||||
|
{{- if .IsEmbedded}}
|
||||||
|
var result {{$.Name}}{{$.TypeParamNames}}
|
||||||
|
result.{{.Name}} = v
|
||||||
|
return result
|
||||||
|
{{- else}}
|
||||||
|
return {{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||||
|
{{- end}}
|
||||||
|
},
|
||||||
|
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||||
|
)
|
||||||
|
{{- else}}
|
||||||
|
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||||
|
func(s {{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
|
||||||
|
func(v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} {
|
||||||
|
{{- if .IsEmbedded}}
|
||||||
|
var result {{$.Name}}{{$.TypeParamNames}}
|
||||||
|
result.{{.Name}} = v
|
||||||
|
return result
|
||||||
|
{{- else}}
|
||||||
|
return {{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||||
|
{{- end}}
|
||||||
|
},
|
||||||
|
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||||
|
)
|
||||||
|
{{- end}}
|
||||||
|
{{- end}}
|
||||||
|
return {{.Name}}Prisms{{.TypeParamNames}} {
|
||||||
|
{{- range .Fields}}
|
||||||
|
{{.Name}}: _prism{{.Name}},
|
||||||
|
{{- end}}
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -439,7 +504,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
if typeName == "" || typeIdent == nil {
|
if S.IsEmpty(typeName) || typeIdent == nil {
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +559,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
|||||||
BaseType: baseType,
|
BaseType: baseType,
|
||||||
IsOptional: isOptional,
|
IsOptional: isOptional,
|
||||||
IsComparable: isComparable,
|
IsComparable: isComparable,
|
||||||
|
IsEmbedded: true,
|
||||||
},
|
},
|
||||||
fieldType: field.Type,
|
fieldType: field.Type,
|
||||||
})
|
})
|
||||||
@@ -695,7 +761,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateLensHelpers scans a directory for Go files and generates lens code
|
// generateLensHelpers scans a directory for Go files and generates lens code
|
||||||
func generateLensHelpers(dir, filename string, verbose bool) error {
|
func generateLensHelpers(dir, filename string, verbose, includeTestFiles bool) error {
|
||||||
// Get absolute path
|
// Get absolute path
|
||||||
absDir, err := filepath.Abs(dir)
|
absDir, err := filepath.Abs(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -716,21 +782,34 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
|||||||
log.Printf("Found %d Go files", len(files))
|
log.Printf("Found %d Go files", len(files))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse all files and collect structs
|
// Parse all files and collect structs, separating test and non-test files
|
||||||
var allStructs []structInfo
|
var regularStructs []structInfo
|
||||||
|
var testStructs []structInfo
|
||||||
var packageName string
|
var packageName string
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
// Skip generated files and test files
|
baseName := filepath.Base(file)
|
||||||
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
|
|
||||||
|
// Skip generated lens files (both regular and test)
|
||||||
|
if strings.HasPrefix(baseName, "gen_lens") && strings.HasSuffix(baseName, ".go") {
|
||||||
if verbose {
|
if verbose {
|
||||||
log.Printf("Skipping file: %s", filepath.Base(file))
|
log.Printf("Skipping generated lens file: %s", baseName)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isTestFile := strings.HasSuffix(file, "_test.go")
|
||||||
|
|
||||||
|
// Skip test files unless includeTestFiles is true
|
||||||
|
if isTestFile && !includeTestFiles {
|
||||||
|
if verbose {
|
||||||
|
log.Printf("Skipping test file: %s", baseName)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if verbose {
|
if verbose {
|
||||||
log.Printf("Parsing file: %s", filepath.Base(file))
|
log.Printf("Parsing file: %s", baseName)
|
||||||
}
|
}
|
||||||
|
|
||||||
structs, pkg, err := parseFile(file)
|
structs, pkg, err := parseFile(file)
|
||||||
@@ -740,27 +819,52 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if verbose && len(structs) > 0 {
|
if verbose && len(structs) > 0 {
|
||||||
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file))
|
log.Printf("Found %d annotated struct(s) in %s", len(structs), baseName)
|
||||||
for _, s := range structs {
|
for _, s := range structs {
|
||||||
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
|
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if packageName == "" {
|
if S.IsEmpty(packageName) {
|
||||||
packageName = pkg
|
packageName = pkg
|
||||||
}
|
}
|
||||||
|
|
||||||
allStructs = append(allStructs, structs...)
|
// Separate structs based on source file type
|
||||||
|
if isTestFile {
|
||||||
|
testStructs = append(testStructs, structs...)
|
||||||
|
} else {
|
||||||
|
regularStructs = append(regularStructs, structs...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(allStructs) == 0 {
|
if len(regularStructs) == 0 && len(testStructs) == 0 {
|
||||||
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
|
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate regular lens file if there are regular structs
|
||||||
|
if len(regularStructs) > 0 {
|
||||||
|
if err := generateLensFile(absDir, filename, packageName, regularStructs, verbose); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate test lens file if there are test structs
|
||||||
|
if len(testStructs) > 0 {
|
||||||
|
testFilename := strings.TrimSuffix(filename, ".go") + "_test.go"
|
||||||
|
if err := generateLensFile(absDir, testFilename, packageName, testStructs, verbose); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateLensFile generates a lens file for the given structs
|
||||||
|
func generateLensFile(absDir, filename, packageName string, structs []structInfo, verbose bool) error {
|
||||||
// Collect all unique imports from all structs
|
// Collect all unique imports from all structs
|
||||||
allImports := make(map[string]string) // import path -> alias
|
allImports := make(map[string]string) // import path -> alias
|
||||||
for _, s := range allStructs {
|
for _, s := range structs {
|
||||||
for importPath, alias := range s.Imports {
|
for importPath, alias := range s.Imports {
|
||||||
allImports[importPath] = alias
|
allImports[importPath] = alias
|
||||||
}
|
}
|
||||||
@@ -774,7 +878,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs))
|
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(structs))
|
||||||
|
|
||||||
// Write header
|
// Write header
|
||||||
writePackage(f, packageName)
|
writePackage(f, packageName)
|
||||||
@@ -782,10 +886,11 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
|||||||
// Write imports
|
// Write imports
|
||||||
f.WriteString("import (\n")
|
f.WriteString("import (\n")
|
||||||
// Standard fp-go imports always needed
|
// Standard fp-go imports always needed
|
||||||
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
f.WriteString("\t__lens \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||||
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
f.WriteString("\t__option \"github.com/IBM/fp-go/v2/option\"\n")
|
||||||
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
|
f.WriteString("\t__prism \"github.com/IBM/fp-go/v2/optics/prism\"\n")
|
||||||
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
|
f.WriteString("\t__lens_option \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
||||||
|
f.WriteString("\t__iso_option \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
|
||||||
|
|
||||||
// Add additional imports collected from field types
|
// Add additional imports collected from field types
|
||||||
for importPath, alias := range allImports {
|
for importPath, alias := range allImports {
|
||||||
@@ -795,7 +900,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
|||||||
f.WriteString(")\n")
|
f.WriteString(")\n")
|
||||||
|
|
||||||
// Generate lens code for each struct using templates
|
// Generate lens code for each struct using templates
|
||||||
for _, s := range allStructs {
|
for _, s := range structs {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
// Generate struct type
|
// Generate struct type
|
||||||
@@ -827,12 +932,14 @@ func LensCommand() *C.Command {
|
|||||||
flagLensDir,
|
flagLensDir,
|
||||||
flagFilename,
|
flagFilename,
|
||||||
flagVerbose,
|
flagVerbose,
|
||||||
|
flagIncludeTestFiles,
|
||||||
},
|
},
|
||||||
Action: func(ctx *C.Context) error {
|
Action: func(ctx *C.Context) error {
|
||||||
return generateLensHelpers(
|
return generateLensHelpers(
|
||||||
ctx.String(keyLensDir),
|
ctx.String(keyLensDir),
|
||||||
ctx.String(keyFilename),
|
ctx.String(keyFilename),
|
||||||
ctx.Bool(keyVerbose),
|
ctx.Bool(keyVerbose),
|
||||||
|
ctx.Bool(keyIncludeTestFile),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -60,7 +61,7 @@ func TestHasLensAnnotation(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var doc *ast.CommentGroup
|
var doc *ast.CommentGroup
|
||||||
if tt.comment != "" {
|
if S.IsNonEmpty(tt.comment) {
|
||||||
doc = &ast.CommentGroup{
|
doc = &ast.CommentGroup{
|
||||||
List: []*ast.Comment{
|
List: []*ast.Comment{
|
||||||
{Text: tt.comment},
|
{Text: tt.comment},
|
||||||
@@ -289,7 +290,7 @@ func TestHasOmitEmpty(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var tag *ast.BasicLit
|
var tag *ast.BasicLit
|
||||||
if tt.tag != "" {
|
if S.IsNonEmpty(tt.tag) {
|
||||||
tag = &ast.BasicLit{
|
tag = &ast.BasicLit{
|
||||||
Value: tt.tag,
|
Value: tt.tag,
|
||||||
}
|
}
|
||||||
@@ -326,7 +327,7 @@ type Other struct {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse the file
|
// Parse the file
|
||||||
@@ -380,7 +381,7 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse the file
|
// Parse the file
|
||||||
@@ -440,7 +441,7 @@ type TypeTest struct {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse the file
|
// Parse the file
|
||||||
@@ -514,16 +515,16 @@ func TestLensRefTemplatesWithComparable(t *testing.T) {
|
|||||||
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
|
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
|
||||||
|
|
||||||
// Name field - comparable, should use MakeLensStrict
|
// Name field - comparable, should use MakeLensStrict
|
||||||
assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(",
|
assert.Contains(t, constructorStr, "lensName := __lens.MakeLensStrictWithName(",
|
||||||
"comparable field Name should use MakeLensStrict in RefLenses")
|
"comparable field Name should use MakeLensStrictWithName in RefLenses")
|
||||||
|
|
||||||
// Age field - comparable, should use MakeLensStrict
|
// Age field - comparable, should use MakeLensStrict
|
||||||
assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(",
|
assert.Contains(t, constructorStr, "lensAge := __lens.MakeLensStrictWithName(",
|
||||||
"comparable field Age should use MakeLensStrict in RefLenses")
|
"comparable field Age should use MakeLensStrictWithName in RefLenses")
|
||||||
|
|
||||||
// Data field - not comparable, should use MakeLensRef
|
// Data field - not comparable, should use MakeLensRef
|
||||||
assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(",
|
assert.Contains(t, constructorStr, "lensData := __lens.MakeLensRefWithName(",
|
||||||
"non-comparable field Data should use MakeLensRef in RefLenses")
|
"non-comparable field Data should use MakeLensRefWithName in RefLenses")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,12 +543,12 @@ type TestStruct struct {
|
|||||||
`
|
`
|
||||||
|
|
||||||
testFile := filepath.Join(tmpDir, "test.go")
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Generate lens code
|
// Generate lens code
|
||||||
outputFile := "gen.go"
|
outputFile := "gen.go"
|
||||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify the generated file exists
|
// Verify the generated file exists
|
||||||
@@ -564,23 +565,23 @@ type TestStruct struct {
|
|||||||
// Check for expected content in RefLenses
|
// Check for expected content in RefLenses
|
||||||
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
|
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
|
||||||
|
|
||||||
// Name and Count are comparable, should use MakeLensStrict
|
// Name and Count are comparable, should use MakeLensStrictWithName
|
||||||
assert.Contains(t, contentStr, "L.MakeLensStrict",
|
assert.Contains(t, contentStr, "__lens.MakeLensStrictWithName",
|
||||||
"comparable fields should use MakeLensStrict in RefLenses")
|
"comparable fields should use MakeLensStrictWithName in RefLenses")
|
||||||
|
|
||||||
// Data is not comparable (slice), should use MakeLensRef
|
// Data is not comparable (slice), should use MakeLensRefWithName
|
||||||
assert.Contains(t, contentStr, "L.MakeLensRef",
|
assert.Contains(t, contentStr, "__lens.MakeLensRefWithName",
|
||||||
"non-comparable fields should use MakeLensRef in RefLenses")
|
"non-comparable fields should use MakeLensRefWithName in RefLenses")
|
||||||
|
|
||||||
// Verify the pattern appears for Name field (comparable)
|
// Verify the pattern appears for Name field (comparable)
|
||||||
namePattern := "lensName := L.MakeLensStrict("
|
namePattern := "lensName := __lens.MakeLensStrictWithName("
|
||||||
assert.Contains(t, contentStr, namePattern,
|
assert.Contains(t, contentStr, namePattern,
|
||||||
"Name field should use MakeLensStrict")
|
"Name field should use MakeLensStrictWithName")
|
||||||
|
|
||||||
// Verify the pattern appears for Data field (not comparable)
|
// Verify the pattern appears for Data field (not comparable)
|
||||||
dataPattern := "lensData := L.MakeLensRef("
|
dataPattern := "lensData := __lens.MakeLensRefWithName("
|
||||||
assert.Contains(t, contentStr, dataPattern,
|
assert.Contains(t, contentStr, dataPattern,
|
||||||
"Data field should use MakeLensRef")
|
"Data field should use MakeLensRefWithName")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateLensHelpers(t *testing.T) {
|
func TestGenerateLensHelpers(t *testing.T) {
|
||||||
@@ -597,12 +598,12 @@ type TestStruct struct {
|
|||||||
`
|
`
|
||||||
|
|
||||||
testFile := filepath.Join(tmpDir, "test.go")
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Generate lens code
|
// Generate lens code
|
||||||
outputFile := "gen.go"
|
outputFile := "gen.go"
|
||||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify the generated file exists
|
// Verify the generated file exists
|
||||||
@@ -621,9 +622,9 @@ type TestStruct struct {
|
|||||||
assert.Contains(t, contentStr, "Code generated by go generate")
|
assert.Contains(t, contentStr, "Code generated by go generate")
|
||||||
assert.Contains(t, contentStr, "TestStructLenses")
|
assert.Contains(t, contentStr, "TestStructLenses")
|
||||||
assert.Contains(t, contentStr, "MakeTestStructLenses")
|
assert.Contains(t, contentStr, "MakeTestStructLenses")
|
||||||
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
|
assert.Contains(t, contentStr, "__lens.Lens[TestStruct, string]")
|
||||||
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
|
assert.Contains(t, contentStr, "__lens_option.LensO[TestStruct, *int]")
|
||||||
assert.Contains(t, contentStr, "IO.FromZero")
|
assert.Contains(t, contentStr, "__iso_option.FromZero")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
|
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
|
||||||
@@ -639,12 +640,12 @@ type TestStruct struct {
|
|||||||
`
|
`
|
||||||
|
|
||||||
testFile := filepath.Join(tmpDir, "test.go")
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Generate lens code (should not create file)
|
// Generate lens code (should not create file)
|
||||||
outputFile := "gen.go"
|
outputFile := "gen.go"
|
||||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify the generated file does not exist
|
// Verify the generated file does not exist
|
||||||
@@ -669,10 +670,10 @@ func TestLensTemplates(t *testing.T) {
|
|||||||
|
|
||||||
structStr := structBuf.String()
|
structStr := structBuf.String()
|
||||||
assert.Contains(t, structStr, "type TestStructLenses struct")
|
assert.Contains(t, structStr, "type TestStructLenses struct")
|
||||||
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
|
assert.Contains(t, structStr, "Name __lens.Lens[TestStruct, string]")
|
||||||
assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]")
|
assert.Contains(t, structStr, "NameO __lens_option.LensO[TestStruct, string]")
|
||||||
assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]")
|
assert.Contains(t, structStr, "Value __lens.Lens[TestStruct, *int]")
|
||||||
assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]")
|
assert.Contains(t, structStr, "ValueO __lens_option.LensO[TestStruct, *int]")
|
||||||
|
|
||||||
// Test constructor template
|
// Test constructor template
|
||||||
var constructorBuf bytes.Buffer
|
var constructorBuf bytes.Buffer
|
||||||
@@ -686,7 +687,7 @@ func TestLensTemplates(t *testing.T) {
|
|||||||
assert.Contains(t, constructorStr, "NameO: lensNameO,")
|
assert.Contains(t, constructorStr, "NameO: lensNameO,")
|
||||||
assert.Contains(t, constructorStr, "Value: lensValue,")
|
assert.Contains(t, constructorStr, "Value: lensValue,")
|
||||||
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
|
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
|
||||||
assert.Contains(t, constructorStr, "IO.FromZero")
|
assert.Contains(t, constructorStr, "__iso_option.FromZero")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||||
@@ -707,14 +708,14 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
|||||||
|
|
||||||
structStr := structBuf.String()
|
structStr := structBuf.String()
|
||||||
assert.Contains(t, structStr, "type ConfigStructLenses struct")
|
assert.Contains(t, structStr, "type ConfigStructLenses struct")
|
||||||
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
|
assert.Contains(t, structStr, "Name __lens.Lens[ConfigStruct, string]")
|
||||||
assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]")
|
assert.Contains(t, structStr, "NameO __lens_option.LensO[ConfigStruct, string]")
|
||||||
assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
|
assert.Contains(t, structStr, "Value __lens.Lens[ConfigStruct, string]")
|
||||||
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
|
assert.Contains(t, structStr, "ValueO __lens_option.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
|
||||||
assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]")
|
assert.Contains(t, structStr, "Count __lens.Lens[ConfigStruct, int]")
|
||||||
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
|
assert.Contains(t, structStr, "CountO __lens_option.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
|
||||||
assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]")
|
assert.Contains(t, structStr, "Pointer __lens.Lens[ConfigStruct, *string]")
|
||||||
assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]")
|
assert.Contains(t, structStr, "PointerO __lens_option.LensO[ConfigStruct, *string]")
|
||||||
|
|
||||||
// Test constructor template
|
// Test constructor template
|
||||||
var constructorBuf bytes.Buffer
|
var constructorBuf bytes.Buffer
|
||||||
@@ -723,9 +724,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
|||||||
|
|
||||||
constructorStr := constructorBuf.String()
|
constructorStr := constructorBuf.String()
|
||||||
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
|
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
|
||||||
assert.Contains(t, constructorStr, "IO.FromZero[string]()")
|
assert.Contains(t, constructorStr, "__iso_option.FromZero[string]()")
|
||||||
assert.Contains(t, constructorStr, "IO.FromZero[int]()")
|
assert.Contains(t, constructorStr, "__iso_option.FromZero[int]()")
|
||||||
assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
|
assert.Contains(t, constructorStr, "__iso_option.FromZero[*string]()")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLensCommandFlags(t *testing.T) {
|
func TestLensCommandFlags(t *testing.T) {
|
||||||
@@ -737,9 +738,9 @@ func TestLensCommandFlags(t *testing.T) {
|
|||||||
assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")
|
assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")
|
||||||
|
|
||||||
// Check flags
|
// Check flags
|
||||||
assert.Len(t, cmd.Flags, 3)
|
assert.Len(t, cmd.Flags, 4)
|
||||||
|
|
||||||
var hasDir, hasFilename, hasVerbose bool
|
var hasDir, hasFilename, hasVerbose, hasIncludeTestFiles bool
|
||||||
for _, flag := range cmd.Flags {
|
for _, flag := range cmd.Flags {
|
||||||
switch flag.Names()[0] {
|
switch flag.Names()[0] {
|
||||||
case "dir":
|
case "dir":
|
||||||
@@ -748,12 +749,15 @@ func TestLensCommandFlags(t *testing.T) {
|
|||||||
hasFilename = true
|
hasFilename = true
|
||||||
case "verbose":
|
case "verbose":
|
||||||
hasVerbose = true
|
hasVerbose = true
|
||||||
|
case "include-test-files":
|
||||||
|
hasIncludeTestFiles = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.True(t, hasDir, "should have dir flag")
|
assert.True(t, hasDir, "should have dir flag")
|
||||||
assert.True(t, hasFilename, "should have filename flag")
|
assert.True(t, hasFilename, "should have filename flag")
|
||||||
assert.True(t, hasVerbose, "should have verbose flag")
|
assert.True(t, hasVerbose, "should have verbose flag")
|
||||||
|
assert.True(t, hasIncludeTestFiles, "should have include-test-files flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseFileWithEmbeddedStruct(t *testing.T) {
|
func TestParseFileWithEmbeddedStruct(t *testing.T) {
|
||||||
@@ -776,7 +780,7 @@ type Extended struct {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse the file
|
// Parse the file
|
||||||
@@ -824,12 +828,12 @@ type Person struct {
|
|||||||
`
|
`
|
||||||
|
|
||||||
testFile := filepath.Join(tmpDir, "test.go")
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Generate lens code
|
// Generate lens code
|
||||||
outputFile := "gen.go"
|
outputFile := "gen.go"
|
||||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify the generated file exists
|
// Verify the generated file exists
|
||||||
@@ -849,14 +853,14 @@ type Person struct {
|
|||||||
assert.Contains(t, contentStr, "MakePersonLenses")
|
assert.Contains(t, contentStr, "MakePersonLenses")
|
||||||
|
|
||||||
// Check that embedded fields are included
|
// Check that embedded fields are included
|
||||||
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
|
assert.Contains(t, contentStr, "Street __lens.Lens[Person, string]", "Should have lens for embedded Street field")
|
||||||
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
|
assert.Contains(t, contentStr, "City __lens.Lens[Person, string]", "Should have lens for embedded City field")
|
||||||
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
|
assert.Contains(t, contentStr, "Name __lens.Lens[Person, string]", "Should have lens for Name field")
|
||||||
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")
|
assert.Contains(t, contentStr, "Age __lens.Lens[Person, int]", "Should have lens for Age field")
|
||||||
|
|
||||||
// Check that optional lenses are also generated for embedded fields
|
// Check that optional lenses are also generated for embedded fields
|
||||||
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
|
assert.Contains(t, contentStr, "StreetO __lens_option.LensO[Person, string]")
|
||||||
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
|
assert.Contains(t, contentStr, "CityO __lens_option.LensO[Person, string]")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
|
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
|
||||||
@@ -880,7 +884,7 @@ type Document struct {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse the file
|
// Parse the file
|
||||||
@@ -922,7 +926,7 @@ type Container[T any] struct {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse the file
|
// Parse the file
|
||||||
@@ -960,7 +964,7 @@ type Pair[K comparable, V any] struct {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Parse the file
|
// Parse the file
|
||||||
@@ -998,12 +1002,12 @@ type Box[T any] struct {
|
|||||||
`
|
`
|
||||||
|
|
||||||
testFile := filepath.Join(tmpDir, "test.go")
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Generate lens code
|
// Generate lens code
|
||||||
outputFile := "gen.go"
|
outputFile := "gen.go"
|
||||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify the generated file exists
|
// Verify the generated file exists
|
||||||
@@ -1025,14 +1029,14 @@ type Box[T any] struct {
|
|||||||
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
|
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
|
||||||
|
|
||||||
// Check that fields use the generic type parameter
|
// Check that fields use the generic type parameter
|
||||||
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
|
assert.Contains(t, contentStr, "Content __lens.Lens[Box[T], T]", "Should have lens for generic Content field")
|
||||||
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")
|
assert.Contains(t, contentStr, "Label __lens.Lens[Box[T], string]", "Should have lens for Label field")
|
||||||
|
|
||||||
// Check optional lenses - only for comparable types
|
// Check optional lenses - only for comparable types
|
||||||
// T any is not comparable, so ContentO should NOT be generated
|
// T any is not comparable, so ContentO should NOT be generated
|
||||||
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
|
assert.NotContains(t, contentStr, "ContentO __lens_option.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
|
||||||
// string is comparable, so LabelO should be generated
|
// string is comparable, so LabelO should be generated
|
||||||
assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens")
|
assert.Contains(t, contentStr, "LabelO __lens_option.LensO[Box[T], string]", "string is comparable, should have optional lens")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
|
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
|
||||||
@@ -1049,12 +1053,12 @@ type ComparableBox[T comparable] struct {
|
|||||||
`
|
`
|
||||||
|
|
||||||
testFile := filepath.Join(tmpDir, "test.go")
|
testFile := filepath.Join(tmpDir, "test.go")
|
||||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Generate lens code
|
// Generate lens code
|
||||||
outputFile := "gen.go"
|
outputFile := "gen.go"
|
||||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify the generated file exists
|
// Verify the generated file exists
|
||||||
@@ -1074,11 +1078,11 @@ type ComparableBox[T comparable] struct {
|
|||||||
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
|
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
|
||||||
|
|
||||||
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
|
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
|
||||||
assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict")
|
assert.Contains(t, contentStr, "lensKey := __lens.MakeLensStrictWithName(", "Key field with comparable constraint should use MakeLensStrictWithName")
|
||||||
|
|
||||||
// Check that Value field (string, always comparable) also uses MakeLensStrict
|
// Check that Value field (string, always comparable) also uses MakeLensStrict
|
||||||
assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict")
|
assert.Contains(t, contentStr, "lensValue := __lens.MakeLensStrictWithName(", "Value field (string) should use MakeLensStrictWithName")
|
||||||
|
|
||||||
// Verify that MakeLensRef is NOT used (since both fields are comparable)
|
// Verify that MakeLensRef is NOT used (since both fields are comparable)
|
||||||
assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable")
|
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deprecated:
|
// Deprecated:
|
||||||
@@ -176,7 +178,7 @@ func generateTraverseTuple1(
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(f, "F%d ~func(A%d) %s", j+1, j+1, hkt(fmt.Sprintf("T%d", j+1)))
|
fmt.Fprintf(f, "F%d ~func(A%d) %s", j+1, j+1, hkt(fmt.Sprintf("T%d", j+1)))
|
||||||
}
|
}
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, ", %s", infix)
|
fmt.Fprintf(f, ", %s", infix)
|
||||||
}
|
}
|
||||||
// types
|
// types
|
||||||
@@ -209,7 +211,7 @@ func generateTraverseTuple1(
|
|||||||
fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i)
|
fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i)
|
||||||
// map
|
// map
|
||||||
fmt.Fprintf(f, " Map[")
|
fmt.Fprintf(f, " Map[")
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, "%s, T1,", infix)
|
fmt.Fprintf(f, "%s, T1,", infix)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(f, "T1,")
|
fmt.Fprintf(f, "T1,")
|
||||||
@@ -231,7 +233,7 @@ func generateTraverseTuple1(
|
|||||||
fmt.Fprintf(f, " ")
|
fmt.Fprintf(f, " ")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, "%s", tuple)
|
fmt.Fprintf(f, "%s", tuple)
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, ", %s", infix)
|
fmt.Fprintf(f, ", %s", infix)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, ", T%d],\n", j+1)
|
fmt.Fprintf(f, ", T%d],\n", j+1)
|
||||||
@@ -256,11 +258,11 @@ func generateSequenceTuple1(
|
|||||||
|
|
||||||
fmt.Fprintf(f, "\n// SequenceTuple%d converts a [Tuple%d] of [%s] into an [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
|
fmt.Fprintf(f, "\n// SequenceTuple%d converts a [Tuple%d] of [%s] into an [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
|
||||||
fmt.Fprintf(f, "func SequenceTuple%d[", i)
|
fmt.Fprintf(f, "func SequenceTuple%d[", i)
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, "%s", infix)
|
fmt.Fprintf(f, "%s", infix)
|
||||||
}
|
}
|
||||||
for j := 0; j < i; j++ {
|
for j := 0; j < i; j++ {
|
||||||
if infix != "" || j > 0 {
|
if S.IsNonEmpty(infix) || j > 0 {
|
||||||
fmt.Fprintf(f, ", ")
|
fmt.Fprintf(f, ", ")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, "T%d", j+1)
|
fmt.Fprintf(f, "T%d", j+1)
|
||||||
@@ -276,7 +278,7 @@ func generateSequenceTuple1(
|
|||||||
fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i)
|
fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i)
|
||||||
// map
|
// map
|
||||||
fmt.Fprintf(f, " Map[")
|
fmt.Fprintf(f, " Map[")
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, "%s, T1,", infix)
|
fmt.Fprintf(f, "%s, T1,", infix)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(f, "T1,")
|
fmt.Fprintf(f, "T1,")
|
||||||
@@ -298,7 +300,7 @@ func generateSequenceTuple1(
|
|||||||
fmt.Fprintf(f, " ")
|
fmt.Fprintf(f, " ")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, "%s", tuple)
|
fmt.Fprintf(f, "%s", tuple)
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, ", %s", infix)
|
fmt.Fprintf(f, ", %s", infix)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, ", T%d],\n", j+1)
|
fmt.Fprintf(f, ", T%d],\n", j+1)
|
||||||
@@ -319,11 +321,11 @@ func generateSequenceT1(
|
|||||||
|
|
||||||
fmt.Fprintf(f, "\n// SequenceT%d converts %d parameters of [%s] into a [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
|
fmt.Fprintf(f, "\n// SequenceT%d converts %d parameters of [%s] into a [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
|
||||||
fmt.Fprintf(f, "func SequenceT%d[", i)
|
fmt.Fprintf(f, "func SequenceT%d[", i)
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, "%s", infix)
|
fmt.Fprintf(f, "%s", infix)
|
||||||
}
|
}
|
||||||
for j := 0; j < i; j++ {
|
for j := 0; j < i; j++ {
|
||||||
if infix != "" || j > 0 {
|
if S.IsNonEmpty(infix) || j > 0 {
|
||||||
fmt.Fprintf(f, ", ")
|
fmt.Fprintf(f, ", ")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, "T%d", j+1)
|
fmt.Fprintf(f, "T%d", j+1)
|
||||||
@@ -339,7 +341,7 @@ func generateSequenceT1(
|
|||||||
fmt.Fprintf(f, " return A.SequenceT%d(\n", i)
|
fmt.Fprintf(f, " return A.SequenceT%d(\n", i)
|
||||||
// map
|
// map
|
||||||
fmt.Fprintf(f, " Map[")
|
fmt.Fprintf(f, " Map[")
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, "%s, T1,", infix)
|
fmt.Fprintf(f, "%s, T1,", infix)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(f, "T1,")
|
fmt.Fprintf(f, "T1,")
|
||||||
@@ -361,7 +363,7 @@ func generateSequenceT1(
|
|||||||
fmt.Fprintf(f, " ")
|
fmt.Fprintf(f, " ")
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, "%s", tuple)
|
fmt.Fprintf(f, "%s", tuple)
|
||||||
if infix != "" {
|
if S.IsNonEmpty(infix) {
|
||||||
fmt.Fprintf(f, ", %s", infix)
|
fmt.Fprintf(f, ", %s", infix)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(f, ", T%d],\n", j+1)
|
fmt.Fprintf(f, ", T%d],\n", j+1)
|
||||||
|
|||||||
177
v2/consumer/consumer.go
Normal file
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
|
// withContext wraps an existing IOEither and performs a context check for cancellation before delegating
|
||||||
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
|
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
|
||||||
return func() Result[A] {
|
return func() Result[A] {
|
||||||
if err := context.Cause(ctx); err != nil {
|
if ctx.Err() != nil {
|
||||||
return result.Left[A](err)
|
return result.Left[A](context.Cause(ctx))
|
||||||
}
|
}
|
||||||
return ma()
|
return ma()
|
||||||
}
|
}
|
||||||
|
|||||||
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](
|
func TraverseReader[R, A, B any](
|
||||||
f reader.Kleisli[R, A, B],
|
f reader.Kleisli[R, A, B],
|
||||||
) func(ReaderIO[A]) Kleisli[R, B] {
|
) func(ReaderIO[A]) Kleisli[R, B] {
|
||||||
return RIO.TraverseReader[context.Context, R](f)
|
return RIO.TraverseReader[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|||||||
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/IBM/fp-go/v2/function"
|
"github.com/IBM/fp-go/v2/function"
|
||||||
"github.com/IBM/fp-go/v2/reader"
|
"github.com/IBM/fp-go/v2/reader"
|
||||||
@@ -558,3 +559,211 @@ func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A
|
|||||||
func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
|
||||||
return RIO.Read[A](r)
|
return RIO.Read[A](r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
|
||||||
|
//
|
||||||
|
// This is the Reader's local operation, which allows you to modify the environment
|
||||||
|
// for a specific computation without affecting the outer context. The transformation
|
||||||
|
// function receives the current context and returns a new context along with a
|
||||||
|
// cancel function. The cancel function is automatically called when the computation
|
||||||
|
// completes (via defer), ensuring proper cleanup of resources.
|
||||||
|
//
|
||||||
|
// This is useful for:
|
||||||
|
// - Adding timeouts or deadlines to specific operations
|
||||||
|
// - Adding context values for nested computations
|
||||||
|
// - Creating isolated context scopes
|
||||||
|
// - Implementing context-based dependency injection
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The value type of the ReaderIO
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - f: A function that transforms the context and returns a cancel function
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that runs the computation with the transformed context
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// import F "github.com/IBM/fp-go/v2/function"
|
||||||
|
//
|
||||||
|
// // Add a custom value to the context
|
||||||
|
// type key int
|
||||||
|
// const userKey key = 0
|
||||||
|
//
|
||||||
|
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||||
|
// return newCtx, func() {} // No-op cancel
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// getUser := readerio.FromReader(func(ctx context.Context) string {
|
||||||
|
// if user := ctx.Value(userKey); user != nil {
|
||||||
|
// return user.(string)
|
||||||
|
// }
|
||||||
|
// return "unknown"
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// getUser,
|
||||||
|
// addUser,
|
||||||
|
// )
|
||||||
|
// user := result(context.Background())() // Returns "Alice"
|
||||||
|
//
|
||||||
|
// Timeout Example:
|
||||||
|
//
|
||||||
|
// // Add a 5-second timeout to a specific operation
|
||||||
|
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
// return context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// withTimeout,
|
||||||
|
// )
|
||||||
|
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||||
|
return func(rr ReaderIO[A]) ReaderIO[A] {
|
||||||
|
return func(ctx context.Context) IO[A] {
|
||||||
|
return func() A {
|
||||||
|
otherCtx, otherCancel := f(ctx)
|
||||||
|
defer otherCancel()
|
||||||
|
return rr(otherCtx)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeout adds a timeout to the context for a ReaderIO computation.
|
||||||
|
//
|
||||||
|
// This is a convenience wrapper around Local that uses context.WithTimeout.
|
||||||
|
// The computation must complete within the specified duration, or it will be
|
||||||
|
// cancelled. This is useful for ensuring operations don't run indefinitely
|
||||||
|
// and for implementing timeout-based error handling.
|
||||||
|
//
|
||||||
|
// The timeout is relative to when the ReaderIO is executed, not when
|
||||||
|
// WithTimeout is called. The cancel function is automatically called when
|
||||||
|
// the computation completes, ensuring proper cleanup.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The value type of the ReaderIO
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - timeout: The maximum duration for the computation
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that runs the computation with a timeout
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "time"
|
||||||
|
// F "github.com/IBM/fp-go/v2/function"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Fetch data with a 5-second timeout
|
||||||
|
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
|
||||||
|
// // Simulate slow operation
|
||||||
|
// select {
|
||||||
|
// case <-time.After(10 * time.Second):
|
||||||
|
// return Data{Value: "slow"}
|
||||||
|
// case <-ctx.Done():
|
||||||
|
// return Data{}
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// readerio.WithTimeout[Data](5*time.Second),
|
||||||
|
// )
|
||||||
|
// data := result(context.Background())() // Returns Data{} after 5s timeout
|
||||||
|
//
|
||||||
|
// Successful Example:
|
||||||
|
//
|
||||||
|
// quickFetch := readerio.Of(Data{Value: "quick"})
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// quickFetch,
|
||||||
|
// readerio.WithTimeout[Data](5*time.Second),
|
||||||
|
// )
|
||||||
|
// data := result(context.Background())() // Returns Data{Value: "quick"}
|
||||||
|
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||||
|
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(ctx, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDeadline adds an absolute deadline to the context for a ReaderIO computation.
|
||||||
|
//
|
||||||
|
// This is a convenience wrapper around Local that uses context.WithDeadline.
|
||||||
|
// The computation must complete before the specified time, or it will be
|
||||||
|
// cancelled. This is useful for coordinating operations that must finish
|
||||||
|
// by a specific time, such as request deadlines or scheduled tasks.
|
||||||
|
//
|
||||||
|
// The deadline is an absolute time, unlike WithTimeout which uses a relative
|
||||||
|
// duration. The cancel function is automatically called when the computation
|
||||||
|
// completes, ensuring proper cleanup.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The value type of the ReaderIO
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - deadline: The absolute time by which the computation must complete
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that runs the computation with a deadline
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "time"
|
||||||
|
// F "github.com/IBM/fp-go/v2/function"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Operation must complete by 3 PM
|
||||||
|
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
|
||||||
|
//
|
||||||
|
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
|
||||||
|
// // Simulate operation
|
||||||
|
// select {
|
||||||
|
// case <-time.After(1 * time.Hour):
|
||||||
|
// return Data{Value: "done"}
|
||||||
|
// case <-ctx.Done():
|
||||||
|
// return Data{}
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// readerio.WithDeadline[Data](deadline),
|
||||||
|
// )
|
||||||
|
// data := result(context.Background())() // Returns Data{} if past deadline
|
||||||
|
//
|
||||||
|
// Combining with Parent Context:
|
||||||
|
//
|
||||||
|
// // If parent context already has a deadline, the earlier one takes precedence
|
||||||
|
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||||
|
// defer cancel()
|
||||||
|
//
|
||||||
|
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// readerio.WithDeadline[Data](laterDeadline),
|
||||||
|
// )
|
||||||
|
// data := result(parentCtx)() // Will use parent's 1-hour deadline
|
||||||
|
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
|
||||||
|
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithDeadline(ctx, deadline)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay creates an operation that passes in the value after some delay
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
|
func Delay[A any](delay time.Duration) Operator[A, A] {
|
||||||
|
return RIO.Delay[context.Context, A](delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After creates an operation that passes after the given [time.Time]
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
|
func After[R, E, A any](timestamp time.Time) Operator[A, A] {
|
||||||
|
return RIO.After[context.Context, A](timestamp)
|
||||||
|
}
|
||||||
|
|||||||
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/IBM/fp-go/v2/consumer"
|
||||||
|
"github.com/IBM/fp-go/v2/either"
|
||||||
"github.com/IBM/fp-go/v2/io"
|
"github.com/IBM/fp-go/v2/io"
|
||||||
"github.com/IBM/fp-go/v2/lazy"
|
"github.com/IBM/fp-go/v2/lazy"
|
||||||
"github.com/IBM/fp-go/v2/reader"
|
"github.com/IBM/fp-go/v2/reader"
|
||||||
@@ -66,4 +68,8 @@ type (
|
|||||||
//
|
//
|
||||||
// Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B
|
// Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B
|
||||||
Operator[A, B any] = Kleisli[ReaderIO[A], B]
|
Operator[A, B any] = Kleisli[ReaderIO[A], B]
|
||||||
|
|
||||||
|
Consumer[A any] = consumer.Consumer[A]
|
||||||
|
|
||||||
|
Either[E, A any] = either.Either[E, A]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ This document explains how the `Sequence*` functions in the `context/readeriores
|
|||||||
2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
|
2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
|
||||||
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
|
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
|
||||||
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
|
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
|
||||||
5. [Practical Benefits](#practical-benefits)
|
5. [TraverseReader: Introducing Dependencies](#traversereader-introducing-dependencies)
|
||||||
6. [Examples](#examples)
|
6. [Practical Benefits](#practical-benefits)
|
||||||
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
7. [Examples](#examples)
|
||||||
|
8. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
||||||
|
|
||||||
## What is Point-Free Style?
|
## What is Point-Free Style?
|
||||||
|
|
||||||
@@ -25,10 +26,7 @@ func double(x int) int {
|
|||||||
|
|
||||||
**Point-free style (without points):**
|
**Point-free style (without points):**
|
||||||
```go
|
```go
|
||||||
var double = F.Flow2(
|
var double = N.Mul(2)
|
||||||
N.Mul(2),
|
|
||||||
identity,
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
|
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
|
||||||
@@ -99,7 +97,7 @@ The `Sequence*` functions solve this by "flipping" or "sequencing" the nested st
|
|||||||
```go
|
```go
|
||||||
func SequenceReader[R, A any](
|
func SequenceReader[R, A any](
|
||||||
ma ReaderIOResult[Reader[R, A]]
|
ma ReaderIOResult[Reader[R, A]]
|
||||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
) Kleisli[R, A]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Type transformation:**
|
**Type transformation:**
|
||||||
@@ -115,7 +113,7 @@ Now `R` (the Reader's environment) comes **first**, before `context.Context`!
|
|||||||
```go
|
```go
|
||||||
func SequenceReaderIO[R, A any](
|
func SequenceReaderIO[R, A any](
|
||||||
ma ReaderIOResult[ReaderIO[R, A]]
|
ma ReaderIOResult[ReaderIO[R, A]]
|
||||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
) Kleisli[R, A]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Type transformation:**
|
**Type transformation:**
|
||||||
@@ -129,7 +127,7 @@ To: func(R) func(context.Context) func() Either[error, A]
|
|||||||
```go
|
```go
|
||||||
func SequenceReaderResult[R, A any](
|
func SequenceReaderResult[R, A any](
|
||||||
ma ReaderIOResult[ReaderResult[R, A]]
|
ma ReaderIOResult[ReaderResult[R, A]]
|
||||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
) Kleisli[R, A]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Type transformation:**
|
**Type transformation:**
|
||||||
@@ -222,6 +220,186 @@ authInfo := authService(ctx)()
|
|||||||
userInfo := userService(ctx)()
|
userInfo := userService(ctx)()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## TraverseReader: Introducing Dependencies
|
||||||
|
|
||||||
|
While `SequenceReader` flips the parameter order of an existing nested structure, `TraverseReader` allows you to **introduce** a new Reader dependency into an existing computation.
|
||||||
|
|
||||||
|
### Function Signature
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TraverseReader[R, A, B any](
|
||||||
|
f reader.Kleisli[R, A, B],
|
||||||
|
) func(ReaderIOResult[A]) Kleisli[R, B]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type transformation:**
|
||||||
|
```
|
||||||
|
Input: ReaderIOResult[A] = func(context.Context) func() Either[error, A]
|
||||||
|
With: reader.Kleisli[R, A, B] = func(A) func(R) B
|
||||||
|
Output: Kleisli[R, B] = func(R) func(context.Context) func() Either[error, B]
|
||||||
|
```
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
|
||||||
|
`TraverseReader` takes:
|
||||||
|
1. A Reader-based transformation `f: func(A) func(R) B` that depends on environment `R`
|
||||||
|
2. Returns a function that transforms `ReaderIOResult[A]` into `Kleisli[R, B]`
|
||||||
|
|
||||||
|
This allows you to:
|
||||||
|
- Add environment dependencies to computations that don't have them yet
|
||||||
|
- Transform values within a ReaderIOResult using environment-dependent logic
|
||||||
|
- Build composable pipelines where transformations depend on configuration
|
||||||
|
|
||||||
|
### Key Difference from SequenceReader
|
||||||
|
|
||||||
|
- **SequenceReader**: Works with computations that **already contain** a Reader (`ReaderIOResult[Reader[R, A]]`)
|
||||||
|
- Flips the order so `R` comes first
|
||||||
|
- No transformation of the value itself
|
||||||
|
|
||||||
|
- **TraverseReader**: Works with computations that **don't have** a Reader yet (`ReaderIOResult[A]`)
|
||||||
|
- Introduces a new Reader dependency via a transformation function
|
||||||
|
- Transforms `A` to `B` using environment `R`
|
||||||
|
|
||||||
|
### Example: Adding Configuration to a Computation
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
Multiplier int
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original computation that just produces an int
|
||||||
|
getValue := func(ctx context.Context) func() Either[error, int] {
|
||||||
|
return func() Either[error, int] {
|
||||||
|
return Right[error](10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Reader-based transformation that depends on Config
|
||||||
|
formatWithConfig := func(n int) func(Config) string {
|
||||||
|
return func(cfg Config) string {
|
||||||
|
result := n * cfg.Multiplier
|
||||||
|
return fmt.Sprintf("%s: %d", cfg.Prefix, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use TraverseReader to introduce Config dependency
|
||||||
|
traversed := TraverseReader[Config, int, string](formatWithConfig)
|
||||||
|
withConfig := traversed(getValue)
|
||||||
|
|
||||||
|
// Now we can provide Config to get the final result
|
||||||
|
cfg := Config{Multiplier: 5, Prefix: "Result"}
|
||||||
|
ctx := context.Background()
|
||||||
|
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Point-Free Composition with TraverseReader
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Build a pipeline that introduces dependencies at each stage
|
||||||
|
var pipeline = F.Flow4(
|
||||||
|
loadValue, // ReaderIOResult[int]
|
||||||
|
TraverseReader(multiplyByConfig), // Kleisli[Config, int]
|
||||||
|
applyConfig(cfg), // ReaderIOResult[int]
|
||||||
|
Chain(TraverseReader(formatWithStyle)), // Introduce another dependency
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use TraverseReader vs SequenceReader
|
||||||
|
|
||||||
|
**Use SequenceReader when:**
|
||||||
|
- Your computation already returns a Reader: `ReaderIOResult[Reader[R, A]]`
|
||||||
|
- You just want to flip the parameter order
|
||||||
|
- No transformation of the value is needed
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Already have Reader[Config, int]
|
||||||
|
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
|
||||||
|
sequenced := SequenceReader[Config, int](computation)
|
||||||
|
result := sequenced(cfg)(ctx)()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use TraverseReader when:**
|
||||||
|
- Your computation doesn't have a Reader yet: `ReaderIOResult[A]`
|
||||||
|
- You want to transform the value using environment-dependent logic
|
||||||
|
- You're introducing a new dependency into the pipeline
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Have ReaderIOResult[int], want to add Config dependency
|
||||||
|
computation := getValue() // ReaderIOResult[int]
|
||||||
|
traversed := TraverseReader[Config, int, string](formatWithConfig)
|
||||||
|
withDep := traversed(computation)
|
||||||
|
result := withDep(cfg)(ctx)()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Practical Example: Multi-Stage Processing
|
||||||
|
|
||||||
|
```go
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
ConnectionString string
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormattingConfig struct {
|
||||||
|
DateFormat string
|
||||||
|
Timezone string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 1: Load raw data (no dependencies yet)
|
||||||
|
loadData := func(ctx context.Context) func() Either[error, RawData] {
|
||||||
|
// ... implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 2: Process with database config
|
||||||
|
processWithDB := func(raw RawData) func(DatabaseConfig) ProcessedData {
|
||||||
|
return func(cfg DatabaseConfig) ProcessedData {
|
||||||
|
// Use cfg.ConnectionString, cfg.Timeout
|
||||||
|
return ProcessedData{/* ... */}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 3: Format with formatting config
|
||||||
|
formatData := func(processed ProcessedData) func(FormattingConfig) string {
|
||||||
|
return func(cfg FormattingConfig) string {
|
||||||
|
// Use cfg.DateFormat, cfg.Timezone
|
||||||
|
return "formatted result"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build pipeline introducing dependencies at each stage
|
||||||
|
var pipeline = F.Flow3(
|
||||||
|
loadData,
|
||||||
|
TraverseReader[DatabaseConfig, RawData, ProcessedData](processWithDB),
|
||||||
|
// Now we have Kleisli[DatabaseConfig, ProcessedData]
|
||||||
|
applyConfig(dbConfig),
|
||||||
|
// Now we have ReaderIOResult[ProcessedData]
|
||||||
|
TraverseReader[FormattingConfig, ProcessedData, string](formatData),
|
||||||
|
// Now we have Kleisli[FormattingConfig, string]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute with both configs
|
||||||
|
result := pipeline(fmtConfig)(ctx)()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combining TraverseReader and SequenceReader
|
||||||
|
|
||||||
|
You can combine both functions in complex pipelines:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Start with nested Reader
|
||||||
|
computation := getComputation() // ReaderIOResult[Reader[Config, User]]
|
||||||
|
|
||||||
|
var pipeline = F.Flow4(
|
||||||
|
computation,
|
||||||
|
SequenceReader[Config, User], // Flip to get Kleisli[Config, User]
|
||||||
|
applyConfig(cfg), // Apply config, get ReaderIOResult[User]
|
||||||
|
TraverseReader(enrichWithDatabase), // Add database dependency
|
||||||
|
// Now have Kleisli[Database, EnrichedUser]
|
||||||
|
)
|
||||||
|
|
||||||
|
result := pipeline(db)(ctx)()
|
||||||
|
```
|
||||||
|
|
||||||
## Practical Benefits
|
## Practical Benefits
|
||||||
|
|
||||||
### 1. **Improved Testability**
|
### 1. **Improved Testability**
|
||||||
|
|||||||
@@ -18,14 +18,13 @@ package readerioresult
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/IBM/fp-go/v2/context/readerio"
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
"github.com/IBM/fp-go/v2/internal/apply"
|
"github.com/IBM/fp-go/v2/internal/apply"
|
||||||
"github.com/IBM/fp-go/v2/io"
|
"github.com/IBM/fp-go/v2/io"
|
||||||
"github.com/IBM/fp-go/v2/ioeither"
|
"github.com/IBM/fp-go/v2/ioeither"
|
||||||
"github.com/IBM/fp-go/v2/ioresult"
|
"github.com/IBM/fp-go/v2/ioresult"
|
||||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
|
||||||
"github.com/IBM/fp-go/v2/reader"
|
"github.com/IBM/fp-go/v2/reader"
|
||||||
"github.com/IBM/fp-go/v2/readerio"
|
|
||||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||||
"github.com/IBM/fp-go/v2/result"
|
"github.com/IBM/fp-go/v2/result"
|
||||||
)
|
)
|
||||||
@@ -96,7 +95,7 @@ func Bind[S1, S2, T any](
|
|||||||
setter func(T) func(S1) S2,
|
setter func(T) func(S1) S2,
|
||||||
f Kleisli[S1, T],
|
f Kleisli[S1, T],
|
||||||
) Operator[S1, S2] {
|
) Operator[S1, S2] {
|
||||||
return RIOR.Bind(setter, f)
|
return RIOR.Bind(setter, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||||
@@ -128,6 +127,13 @@ func BindTo[S1, T any](
|
|||||||
return RIOR.BindTo[context.Context](setter)
|
return RIOR.BindTo[context.Context](setter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func BindToP[S1, T any](
|
||||||
|
setter Prism[S1, T],
|
||||||
|
) Operator[T, S1] {
|
||||||
|
return BindTo(setter.ReverseGet)
|
||||||
|
}
|
||||||
|
|
||||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||||
// the context and the value concurrently (using Applicative rather than Monad).
|
// the context and the value concurrently (using Applicative rather than Monad).
|
||||||
// This allows independent computations to be combined without one depending on the result of the other.
|
// This allows independent computations to be combined without one depending on the result of the other.
|
||||||
@@ -214,7 +220,7 @@ func ApS[S1, S2, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApSL[S, T any](
|
func ApSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa ReaderIOResult[T],
|
fa ReaderIOResult[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return ApS(lens.Set, fa)
|
return ApS(lens.Set, fa)
|
||||||
@@ -253,10 +259,10 @@ func ApSL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func BindL[S, T any](
|
func BindL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f Kleisli[T, T],
|
f Kleisli[T, T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return RIOR.BindL(lens, f)
|
return RIOR.BindL(lens, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||||
@@ -289,8 +295,8 @@ func BindL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func LetL[S, T any](
|
func LetL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f func(T) T,
|
f Endomorphism[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return RIOR.LetL[context.Context](lens, f)
|
return RIOR.LetL[context.Context](lens, f)
|
||||||
}
|
}
|
||||||
@@ -322,7 +328,7 @@ func LetL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func LetToL[S, T any](
|
func LetToL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
b T,
|
b T,
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return RIOR.LetToL[context.Context](lens, b)
|
return RIOR.LetToL[context.Context](lens, b)
|
||||||
@@ -398,7 +404,7 @@ func BindReaderK[S1, S2, T any](
|
|||||||
//go:inline
|
//go:inline
|
||||||
func BindReaderIOK[S1, S2, T any](
|
func BindReaderIOK[S1, S2, T any](
|
||||||
setter func(T) func(S1) S2,
|
setter func(T) func(S1) S2,
|
||||||
f readerio.Kleisli[context.Context, S1, T],
|
f readerio.Kleisli[S1, T],
|
||||||
) Operator[S1, S2] {
|
) Operator[S1, S2] {
|
||||||
return Bind(setter, F.Flow2(f, FromReaderIO[T]))
|
return Bind(setter, F.Flow2(f, FromReaderIO[T]))
|
||||||
}
|
}
|
||||||
@@ -443,7 +449,7 @@ func BindResultK[S1, S2, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func BindIOEitherKL[S, T any](
|
func BindIOEitherKL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f ioresult.Kleisli[T, T],
|
f ioresult.Kleisli[T, T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||||
@@ -458,7 +464,7 @@ func BindIOEitherKL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func BindIOResultKL[S, T any](
|
func BindIOResultKL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f ioresult.Kleisli[T, T],
|
f ioresult.Kleisli[T, T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||||
@@ -474,7 +480,7 @@ func BindIOResultKL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func BindIOKL[S, T any](
|
func BindIOKL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f io.Kleisli[T, T],
|
f io.Kleisli[T, T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return BindL(lens, F.Flow2(f, FromIO[T]))
|
return BindL(lens, F.Flow2(f, FromIO[T]))
|
||||||
@@ -490,7 +496,7 @@ func BindIOKL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func BindReaderKL[S, T any](
|
func BindReaderKL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f reader.Kleisli[context.Context, T, T],
|
f reader.Kleisli[context.Context, T, T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return BindL(lens, F.Flow2(f, FromReader[T]))
|
return BindL(lens, F.Flow2(f, FromReader[T]))
|
||||||
@@ -506,8 +512,8 @@ func BindReaderKL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func BindReaderIOKL[S, T any](
|
func BindReaderIOKL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f readerio.Kleisli[context.Context, T, T],
|
f readerio.Kleisli[T, T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
|
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
|
||||||
}
|
}
|
||||||
@@ -627,7 +633,7 @@ func ApResultS[S1, S2, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApIOEitherSL[S, T any](
|
func ApIOEitherSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa IOResult[T],
|
fa IOResult[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
||||||
@@ -642,7 +648,7 @@ func ApIOEitherSL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApIOResultSL[S, T any](
|
func ApIOResultSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa IOResult[T],
|
fa IOResult[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
||||||
@@ -657,7 +663,7 @@ func ApIOResultSL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApIOSL[S, T any](
|
func ApIOSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa IO[T],
|
fa IO[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return ApSL(lens, FromIO(fa))
|
return ApSL(lens, FromIO(fa))
|
||||||
@@ -672,7 +678,7 @@ func ApIOSL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApReaderSL[S, T any](
|
func ApReaderSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa Reader[context.Context, T],
|
fa Reader[context.Context, T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return ApSL(lens, FromReader(fa))
|
return ApSL(lens, FromReader(fa))
|
||||||
@@ -687,7 +693,7 @@ func ApReaderSL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApReaderIOSL[S, T any](
|
func ApReaderIOSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa ReaderIO[T],
|
fa ReaderIO[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return ApSL(lens, FromReaderIO(fa))
|
return ApSL(lens, FromReaderIO(fa))
|
||||||
@@ -702,7 +708,7 @@ func ApReaderIOSL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApEitherSL[S, T any](
|
func ApEitherSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa Result[T],
|
fa Result[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return ApSL(lens, FromEither(fa))
|
return ApSL(lens, FromEither(fa))
|
||||||
@@ -717,7 +723,7 @@ func ApEitherSL[S, T any](
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ApResultSL[S, T any](
|
func ApResultSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa Result[T],
|
fa Result[T],
|
||||||
) Operator[S, S] {
|
) Operator[S, S] {
|
||||||
return ApSL(lens, FromResult(fa))
|
return ApSL(lens, FromResult(fa))
|
||||||
|
|||||||
@@ -203,9 +203,7 @@ func TestApS_EmptyState(t *testing.T) {
|
|||||||
result := res(t.Context())()
|
result := res(t.Context())()
|
||||||
assert.True(t, E.IsRight(result))
|
assert.True(t, E.IsRight(result))
|
||||||
emptyOpt := E.ToOption(result)
|
emptyOpt := E.ToOption(result)
|
||||||
assert.True(t, O.IsSome(emptyOpt))
|
assert.Equal(t, O.Of(Empty{}), emptyOpt)
|
||||||
empty, _ := O.Unwrap(emptyOpt)
|
|
||||||
assert.Equal(t, Empty{}, empty)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApS_ChainedWithBind(t *testing.T) {
|
func TestApS_ChainedWithBind(t *testing.T) {
|
||||||
|
|||||||
@@ -16,11 +16,14 @@
|
|||||||
package readerioresult
|
package readerioresult
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
|
||||||
// whether the body action returns and error or not.
|
// whether the body action returns and error or not.
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Bracket[
|
func Bracket[
|
||||||
A, B, ANY any](
|
A, B, ANY any](
|
||||||
|
|
||||||
@@ -28,5 +31,5 @@ func Bracket[
|
|||||||
use Kleisli[A, B],
|
use Kleisli[A, B],
|
||||||
release func(A, Either[B]) ReaderIOResult[ANY],
|
release func(A, Either[B]) ReaderIOResult[ANY],
|
||||||
) ReaderIOResult[B] {
|
) ReaderIOResult[B] {
|
||||||
return RIOR.Bracket(acquire, use, release)
|
return RIOR.Bracket(acquire, F.Flow2(use, WithContext), release)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
|
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
|
||||||
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
"github.com/IBM/fp-go/v2/ioeither"
|
"github.com/IBM/fp-go/v2/ioeither"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,9 +35,17 @@ import (
|
|||||||
// Returns a ReaderIOResult that checks for cancellation before executing.
|
// Returns a ReaderIOResult that checks for cancellation before executing.
|
||||||
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||||
return func(ctx context.Context) IOEither[A] {
|
return func(ctx context.Context) IOEither[A] {
|
||||||
if err := context.Cause(ctx); err != nil {
|
if ctx.Err() != nil {
|
||||||
return ioeither.Left[A](err)
|
return ioeither.Left[A](context.Cause(ctx))
|
||||||
}
|
}
|
||||||
return CIOE.WithContext(ctx, ma(ctx))
|
return CIOE.WithContext(ctx, ma(ctx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
|
||||||
|
return F.Flow2(
|
||||||
|
f,
|
||||||
|
WithContext,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
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
|
//go:inline
|
||||||
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
|
||||||
return RIOR.SequenceReader(ma)
|
return RIOR.SequenceReader(ma)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[co
|
|||||||
// )
|
// )
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R, A] {
|
||||||
return RIOR.SequenceReaderIO(ma)
|
return RIOR.SequenceReaderIO(ma)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kl
|
|||||||
// )
|
// )
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kleisli[R, A] {
|
||||||
return RIOR.SequenceReaderEither(ma)
|
return RIOR.SequenceReaderEither(ma)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/IBM/fp-go/v2/context/readerio"
|
||||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||||
"github.com/IBM/fp-go/v2/either"
|
"github.com/IBM/fp-go/v2/either"
|
||||||
"github.com/IBM/fp-go/v2/errors"
|
"github.com/IBM/fp-go/v2/errors"
|
||||||
@@ -26,10 +27,11 @@ import (
|
|||||||
"github.com/IBM/fp-go/v2/io"
|
"github.com/IBM/fp-go/v2/io"
|
||||||
"github.com/IBM/fp-go/v2/ioeither"
|
"github.com/IBM/fp-go/v2/ioeither"
|
||||||
"github.com/IBM/fp-go/v2/ioresult"
|
"github.com/IBM/fp-go/v2/ioresult"
|
||||||
|
"github.com/IBM/fp-go/v2/option"
|
||||||
"github.com/IBM/fp-go/v2/reader"
|
"github.com/IBM/fp-go/v2/reader"
|
||||||
"github.com/IBM/fp-go/v2/readerio"
|
|
||||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||||
"github.com/IBM/fp-go/v2/readeroption"
|
"github.com/IBM/fp-go/v2/readeroption"
|
||||||
|
"github.com/IBM/fp-go/v2/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -150,7 +152,7 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
|
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
|
||||||
return RIOR.MonadChain(ma, f)
|
return RIOR.MonadChain(ma, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
|
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
|
||||||
@@ -163,7 +165,7 @@ func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||||
return RIOR.Chain(f)
|
return RIOR.Chain(WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||||
@@ -177,12 +179,12 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadChainFirst(ma, f)
|
return RIOR.MonadChainFirst(ma, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadTap(ma, f)
|
return RIOR.MonadTap(ma, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||||
@@ -195,12 +197,12 @@ func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A]
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||||
return RIOR.ChainFirst(f)
|
return RIOR.ChainFirst(WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||||
return RIOR.Tap(f)
|
return RIOR.Tap(WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Of creates a [ReaderIOResult] that always succeeds with the given value.
|
// Of creates a [ReaderIOResult] that always succeeds with the given value.
|
||||||
@@ -243,14 +245,14 @@ func MonadApPar[B, A any](fab ReaderIOResult[func(A) B], fa ReaderIOResult[A]) R
|
|||||||
|
|
||||||
return func(ctx context.Context) IOResult[B] {
|
return func(ctx context.Context) IOResult[B] {
|
||||||
// quick check for cancellation
|
// quick check for cancellation
|
||||||
if err := context.Cause(ctx); err != nil {
|
if ctx.Err() != nil {
|
||||||
return ioeither.Left[B](err)
|
return ioeither.Left[B](context.Cause(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
return func() Result[B] {
|
return func() Result[B] {
|
||||||
// quick check for cancellation
|
// quick check for cancellation
|
||||||
if err := context.Cause(ctx); err != nil {
|
if ctx.Err() != nil {
|
||||||
return either.Left[B](err)
|
return either.Left[B](context.Cause(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// create sub-contexts for fa and fab, so they can cancel one other
|
// create sub-contexts for fa and fab, so they can cancel one other
|
||||||
@@ -382,7 +384,7 @@ func Ask() ReaderIOResult[context.Context] {
|
|||||||
// Returns a new ReaderIOResult with the chained computation.
|
// Returns a new ReaderIOResult with the chained computation.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[B] {
|
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[B] {
|
||||||
return RIOR.MonadChainEitherK(ma, f)
|
return RIOR.MonadChainEitherK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +397,12 @@ func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Read
|
|||||||
// Returns a function that chains the Either-returning function.
|
// Returns a function that chains the Either-returning function.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
|
func ChainEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
|
||||||
|
return RIOR.ChainEitherK[context.Context](f)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func ChainResultK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
|
||||||
return RIOR.ChainEitherK[context.Context](f)
|
return RIOR.ChainEitherK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,12 +416,12 @@ func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
|
|||||||
// Returns a ReaderIOResult with the original value if both computations succeed.
|
// Returns a ReaderIOResult with the original value if both computations succeed.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
|
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadChainFirstEitherK(ma, f)
|
return RIOR.MonadChainFirstEitherK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
|
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadTapEitherK(ma, f)
|
return RIOR.MonadTapEitherK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,12 +434,12 @@ func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Reader
|
|||||||
// Returns a function that chains the Either-returning function.
|
// Returns a function that chains the Either-returning function.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
func ChainFirstEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||||
return RIOR.ChainFirstEitherK[context.Context](f)
|
return RIOR.ChainFirstEitherK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
|
||||||
return RIOR.TapEitherK[context.Context](f)
|
return RIOR.TapEitherK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +452,7 @@ func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
|
|||||||
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
// Returns a function that chains Option-returning functions into ReaderIOResult.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||||
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
return RIOR.ChainOptionK[context.Context, A, B](onNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,7 +534,7 @@ func Never[A any]() ReaderIOResult[A] {
|
|||||||
// Returns a new ReaderIOResult with the chained IO computation.
|
// Returns a new ReaderIOResult with the chained IO computation.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[B] {
|
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[B] {
|
||||||
return RIOR.MonadChainIOK(ma, f)
|
return RIOR.MonadChainIOK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +547,7 @@ func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResu
|
|||||||
// Returns a function that chains the IO-returning function.
|
// Returns a function that chains the IO-returning function.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
|
||||||
return RIOR.ChainIOK[context.Context](f)
|
return RIOR.ChainIOK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,12 +561,12 @@ func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
|
|||||||
// Returns a ReaderIOResult with the original value after executing the IO.
|
// Returns a ReaderIOResult with the original value after executing the IO.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
|
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadChainFirstIOK(ma, f)
|
return RIOR.MonadChainFirstIOK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
|
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadTapIOK(ma, f)
|
return RIOR.MonadTapIOK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,12 +579,12 @@ func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult
|
|||||||
// Returns a function that chains the IO-returning function.
|
// Returns a function that chains the IO-returning function.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
|
||||||
return RIOR.ChainFirstIOK[context.Context](f)
|
return RIOR.ChainFirstIOK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
func TapIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
|
||||||
return RIOR.TapIOK[context.Context](f)
|
return RIOR.TapIOK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,7 +597,7 @@ func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
|
|||||||
// Returns a function that chains the IOResult-returning function.
|
// Returns a function that chains the IOResult-returning function.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainIOEitherK[A, B any](f func(A) IOResult[B]) Operator[A, B] {
|
func ChainIOEitherK[A, B any](f ioresult.Kleisli[A, B]) Operator[A, B] {
|
||||||
return RIOR.ChainIOEitherK[context.Context](f)
|
return RIOR.ChainIOEitherK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +760,7 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
|
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
|
||||||
return RIOR.Fold(onLeft, onRight)
|
return RIOR.Fold(function.Flow2(onLeft, WithContext), function.Flow2(onRight, WithContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrElse extracts the value from a [ReaderIOResult], providing a default via a function if it fails.
|
// GetOrElse extracts the value from a [ReaderIOResult], providing a default via a function if it fails.
|
||||||
@@ -765,7 +772,7 @@ func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A,
|
|||||||
// Returns a function that converts a ReaderIOResult to a ReaderIO.
|
// Returns a function that converts a ReaderIOResult to a ReaderIO.
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) ReaderIO[A] {
|
func GetOrElse[A any](onLeft readerio.Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIO[A] {
|
||||||
return RIOR.GetOrElse(onLeft)
|
return RIOR.GetOrElse(onLeft)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,32 +865,32 @@ func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
|
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[B] {
|
||||||
return RIOR.MonadChainReaderIOK(ma, f)
|
return RIOR.MonadChainReaderIOK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] {
|
func ChainReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, B] {
|
||||||
return RIOR.ChainReaderIOK(f)
|
return RIOR.ChainReaderIOK(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadChainFirstReaderIOK(ma, f)
|
return RIOR.MonadChainFirstReaderIOK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
|
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadTapReaderIOK(ma, f)
|
return RIOR.MonadTapReaderIOK(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
|
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||||
return RIOR.ChainFirstReaderIOK(f)
|
return RIOR.ChainFirstReaderIOK(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
|
func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
|
||||||
return RIOR.TapReaderIOK(f)
|
return RIOR.TapReaderIOK(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -913,15 +920,15 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
|
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadChainLeft(fa, f)
|
return RIOR.MonadChainLeft(fa, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChainLeft is the curried version of [MonadChainLeft].
|
// ChainLeft is the curried version of [MonadChainLeft].
|
||||||
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
|
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
|
||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
|
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
|
||||||
return RIOR.ChainLeft(f)
|
return RIOR.ChainLeft(WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
|
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
|
||||||
@@ -934,12 +941,12 @@ func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResul
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadChainFirstLeft(ma, f)
|
return RIOR.MonadChainFirstLeft(ma, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||||
return RIOR.MonadTapLeft(ma, f)
|
return RIOR.MonadTapLeft(ma, WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
|
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
|
||||||
@@ -951,10 +958,212 @@ func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOR
|
|||||||
//
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||||
return RIOR.ChainFirstLeft[A](f)
|
return RIOR.ChainFirstLeft[A](WithContextK(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:inline
|
//go:inline
|
||||||
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||||
return RIOR.TapLeft[A](f)
|
return RIOR.TapLeft[A](WithContextK(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local transforms the context.Context environment before passing it to a ReaderIOResult computation.
|
||||||
|
//
|
||||||
|
// This is the Reader's local operation, which allows you to modify the environment
|
||||||
|
// for a specific computation without affecting the outer context. The transformation
|
||||||
|
// function receives the current context and returns a new context along with a
|
||||||
|
// cancel function. The cancel function is automatically called when the computation
|
||||||
|
// completes (via defer), ensuring proper cleanup of resources.
|
||||||
|
//
|
||||||
|
// The function checks for context cancellation before applying the transformation,
|
||||||
|
// returning an error immediately if the context is already cancelled.
|
||||||
|
//
|
||||||
|
// This is useful for:
|
||||||
|
// - Adding timeouts or deadlines to specific operations
|
||||||
|
// - Adding context values for nested computations
|
||||||
|
// - Creating isolated context scopes
|
||||||
|
// - Implementing context-based dependency injection
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The value type of the ReaderIOResult
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - f: A function that transforms the context and returns a cancel function
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that runs the computation with the transformed context
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// import F "github.com/IBM/fp-go/v2/function"
|
||||||
|
//
|
||||||
|
// // Add a custom value to the context
|
||||||
|
// type key int
|
||||||
|
// const userKey key = 0
|
||||||
|
//
|
||||||
|
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
// newCtx := context.WithValue(ctx, userKey, "Alice")
|
||||||
|
// return newCtx, func() {} // No-op cancel
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
|
||||||
|
// if user := ctx.Value(userKey); user != nil {
|
||||||
|
// return user.(string)
|
||||||
|
// }
|
||||||
|
// return "unknown"
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// getUser,
|
||||||
|
// addUser,
|
||||||
|
// )
|
||||||
|
// value, err := result(context.Background())() // Returns ("Alice", nil)
|
||||||
|
//
|
||||||
|
// Timeout Example:
|
||||||
|
//
|
||||||
|
// // Add a 5-second timeout to a specific operation
|
||||||
|
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
// return context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// withTimeout,
|
||||||
|
// )
|
||||||
|
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
|
||||||
|
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
|
||||||
|
return func(ctx context.Context) IOResult[A] {
|
||||||
|
return func() Result[A] {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return result.Left[A](context.Cause(ctx))
|
||||||
|
}
|
||||||
|
otherCtx, otherCancel := f(ctx)
|
||||||
|
defer otherCancel()
|
||||||
|
return rr(otherCtx)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeout adds a timeout to the context for a ReaderIOResult computation.
|
||||||
|
//
|
||||||
|
// This is a convenience wrapper around Local that uses context.WithTimeout.
|
||||||
|
// The computation must complete within the specified duration, or it will be
|
||||||
|
// cancelled. This is useful for ensuring operations don't run indefinitely
|
||||||
|
// and for implementing timeout-based error handling.
|
||||||
|
//
|
||||||
|
// The timeout is relative to when the ReaderIOResult is executed, not when
|
||||||
|
// WithTimeout is called. The cancel function is automatically called when
|
||||||
|
// the computation completes, ensuring proper cleanup. If the timeout expires,
|
||||||
|
// the computation will receive a context.DeadlineExceeded error.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The value type of the ReaderIOResult
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - timeout: The maximum duration for the computation
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that runs the computation with a timeout
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "time"
|
||||||
|
// F "github.com/IBM/fp-go/v2/function"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Fetch data with a 5-second timeout
|
||||||
|
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
|
||||||
|
// // Simulate slow operation
|
||||||
|
// select {
|
||||||
|
// case <-time.After(10 * time.Second):
|
||||||
|
// return Data{Value: "slow"}
|
||||||
|
// case <-ctx.Done():
|
||||||
|
// return Data{}
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||||
|
// )
|
||||||
|
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
|
||||||
|
//
|
||||||
|
// Successful Example:
|
||||||
|
//
|
||||||
|
// quickFetch := readerioresult.Right(Data{Value: "quick"})
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// quickFetch,
|
||||||
|
// readerioresult.WithTimeout[Data](5*time.Second),
|
||||||
|
// )
|
||||||
|
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
|
||||||
|
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||||
|
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(ctx, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
|
||||||
|
//
|
||||||
|
// This is a convenience wrapper around Local that uses context.WithDeadline.
|
||||||
|
// The computation must complete before the specified time, or it will be
|
||||||
|
// cancelled. This is useful for coordinating operations that must finish
|
||||||
|
// by a specific time, such as request deadlines or scheduled tasks.
|
||||||
|
//
|
||||||
|
// The deadline is an absolute time, unlike WithTimeout which uses a relative
|
||||||
|
// duration. The cancel function is automatically called when the computation
|
||||||
|
// completes, ensuring proper cleanup. If the deadline passes, the computation
|
||||||
|
// will receive a context.DeadlineExceeded error.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The value type of the ReaderIOResult
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - deadline: The absolute time by which the computation must complete
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that runs the computation with a deadline
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "time"
|
||||||
|
// F "github.com/IBM/fp-go/v2/function"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Operation must complete by 3 PM
|
||||||
|
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
|
||||||
|
//
|
||||||
|
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
|
||||||
|
// // Simulate operation
|
||||||
|
// select {
|
||||||
|
// case <-time.After(1 * time.Hour):
|
||||||
|
// return Data{Value: "done"}
|
||||||
|
// case <-ctx.Done():
|
||||||
|
// return Data{}
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// readerioresult.WithDeadline[Data](deadline),
|
||||||
|
// )
|
||||||
|
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
|
||||||
|
//
|
||||||
|
// Combining with Parent Context:
|
||||||
|
//
|
||||||
|
// // If parent context already has a deadline, the earlier one takes precedence
|
||||||
|
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
|
||||||
|
// defer cancel()
|
||||||
|
//
|
||||||
|
// laterDeadline := time.Now().Add(2 * time.Hour)
|
||||||
|
// result := F.Pipe1(
|
||||||
|
// fetchData,
|
||||||
|
// readerioresult.WithDeadline[Data](laterDeadline),
|
||||||
|
// )
|
||||||
|
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
|
||||||
|
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
|
||||||
|
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithDeadline(ctx, deadline)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,15 +567,13 @@ func TestMemoize(t *testing.T) {
|
|||||||
res1 := computation(context.Background())()
|
res1 := computation(context.Background())()
|
||||||
assert.True(t, E.IsRight(res1))
|
assert.True(t, E.IsRight(res1))
|
||||||
val1 := E.ToOption(res1)
|
val1 := E.ToOption(res1)
|
||||||
v1, _ := O.Unwrap(val1)
|
assert.Equal(t, O.Of(1), val1)
|
||||||
assert.Equal(t, 1, v1)
|
|
||||||
|
|
||||||
// Second execution should return cached value
|
// Second execution should return cached value
|
||||||
res2 := computation(context.Background())()
|
res2 := computation(context.Background())()
|
||||||
assert.True(t, E.IsRight(res2))
|
assert.True(t, E.IsRight(res2))
|
||||||
val2 := E.ToOption(res2)
|
val2 := E.ToOption(res2)
|
||||||
v2, _ := O.Unwrap(val2)
|
assert.Equal(t, O.Of(1), val2)
|
||||||
assert.Equal(t, 1, v2)
|
|
||||||
|
|
||||||
// Counter should only be incremented once
|
// Counter should only be incremented once
|
||||||
assert.Equal(t, 1, counter)
|
assert.Equal(t, 1, counter)
|
||||||
@@ -739,9 +737,7 @@ func TestTraverseArray(t *testing.T) {
|
|||||||
res := result(context.Background())()
|
res := result(context.Background())()
|
||||||
assert.True(t, E.IsRight(res))
|
assert.True(t, E.IsRight(res))
|
||||||
arrOpt := E.ToOption(res)
|
arrOpt := E.ToOption(res)
|
||||||
assert.True(t, O.IsSome(arrOpt))
|
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
|
||||||
resultArr, _ := O.Unwrap(arrOpt)
|
|
||||||
assert.Equal(t, []int{2, 4, 6}, resultArr)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("TraverseArray with error", func(t *testing.T) {
|
t.Run("TraverseArray with error", func(t *testing.T) {
|
||||||
@@ -765,9 +761,7 @@ func TestSequenceArray(t *testing.T) {
|
|||||||
res := result(context.Background())()
|
res := result(context.Background())()
|
||||||
assert.True(t, E.IsRight(res))
|
assert.True(t, E.IsRight(res))
|
||||||
arrOpt := E.ToOption(res)
|
arrOpt := E.ToOption(res)
|
||||||
assert.True(t, O.IsSome(arrOpt))
|
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
|
||||||
resultArr, _ := O.Unwrap(arrOpt)
|
|
||||||
assert.Equal(t, []int{1, 2, 3}, resultArr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTraverseRecord(t *testing.T) {
|
func TestTraverseRecord(t *testing.T) {
|
||||||
|
|||||||
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
|
package readerioresult
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||||
|
"github.com/IBM/fp-go/v2/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource.
|
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource.
|
||||||
@@ -55,3 +59,111 @@ import (
|
|||||||
func WithResource[A, R, ANY any](onCreate ReaderIOResult[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
|
func WithResource[A, R, ANY any](onCreate ReaderIOResult[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
|
||||||
return RIOR.WithResource[A](onCreate, onRelease)
|
return RIOR.WithResource[A](onCreate, onRelease)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onClose is a helper function that creates a ReaderIOResult for closing an io.Closer resource.
|
||||||
|
// It safely calls the Close() method and handles any errors that may occur during closing.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: Must implement io.Closer interface
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - a: The resource to close
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - ReaderIOResult[any]: A computation that closes the resource and returns nil on success
|
||||||
|
//
|
||||||
|
// The function ignores the context parameter since closing operations typically don't need context.
|
||||||
|
// Any error from Close() is captured and returned as a Result error.
|
||||||
|
func onClose[A io.Closer](a A) ReaderIOResult[any] {
|
||||||
|
return func(_ context.Context) IOResult[any] {
|
||||||
|
return func() Result[any] {
|
||||||
|
return result.TryCatchError[any](nil, a.Close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCloser creates a resource management function specifically for io.Closer resources.
|
||||||
|
// This is a specialized version of WithResource that automatically handles closing of resources
|
||||||
|
// that implement the io.Closer interface.
|
||||||
|
//
|
||||||
|
// The function ensures that:
|
||||||
|
// - The resource is created using the onCreate function
|
||||||
|
// - The resource is automatically closed when the operation completes (success or failure)
|
||||||
|
// - Any errors during closing are properly handled
|
||||||
|
// - The resource is closed even if the main operation fails or the context is canceled
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - B: The type of value returned by the resource-using function
|
||||||
|
// - A: The type of resource that implements io.Closer
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - onCreate: ReaderIOResult that creates the io.Closer resource
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A function that takes a resource-using function and returns a ReaderIOResult[B]
|
||||||
|
//
|
||||||
|
// Example with file operations:
|
||||||
|
//
|
||||||
|
// openFile := func(filename string) ReaderIOResult[*os.File] {
|
||||||
|
// return TryCatch(func(ctx context.Context) func() (*os.File, error) {
|
||||||
|
// return func() (*os.File, error) {
|
||||||
|
// return os.Open(filename)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fileReader := WithCloser(openFile("data.txt"))
|
||||||
|
// result := fileReader(func(f *os.File) ReaderIOResult[string] {
|
||||||
|
// return TryCatch(func(ctx context.Context) func() (string, error) {
|
||||||
|
// return func() (string, error) {
|
||||||
|
// data, err := io.ReadAll(f)
|
||||||
|
// return string(data), err
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Example with HTTP response:
|
||||||
|
//
|
||||||
|
// httpGet := func(url string) ReaderIOResult[*http.Response] {
|
||||||
|
// return TryCatch(func(ctx context.Context) func() (*http.Response, error) {
|
||||||
|
// return func() (*http.Response, error) {
|
||||||
|
// return http.Get(url)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// responseReader := WithCloser(httpGet("https://api.example.com/data"))
|
||||||
|
// result := responseReader(func(resp *http.Response) ReaderIOResult[[]byte] {
|
||||||
|
// return TryCatch(func(ctx context.Context) func() ([]byte, error) {
|
||||||
|
// return func() ([]byte, error) {
|
||||||
|
// return io.ReadAll(resp.Body)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Example with database connection:
|
||||||
|
//
|
||||||
|
// openDB := func(dsn string) ReaderIOResult[*sql.DB] {
|
||||||
|
// return TryCatch(func(ctx context.Context) func() (*sql.DB, error) {
|
||||||
|
// return func() (*sql.DB, error) {
|
||||||
|
// return sql.Open("postgres", dsn)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// dbQuery := WithCloser(openDB("postgres://..."))
|
||||||
|
// result := dbQuery(func(db *sql.DB) ReaderIOResult[[]User] {
|
||||||
|
// return TryCatch(func(ctx context.Context) func() ([]User, error) {
|
||||||
|
// return func() ([]User, error) {
|
||||||
|
// rows, err := db.QueryContext(ctx, "SELECT * FROM users")
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// defer rows.Close()
|
||||||
|
// return scanUsers(rows)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
func WithCloser[B any, A io.Closer](onCreate ReaderIOResult[A]) Kleisli[Kleisli[A, B], B] {
|
||||||
|
return WithResource[B](onCreate, onClose[A])
|
||||||
|
}
|
||||||
|
|||||||
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 (
|
import (
|
||||||
"github.com/IBM/fp-go/v2/array"
|
"github.com/IBM/fp-go/v2/array"
|
||||||
"github.com/IBM/fp-go/v2/function"
|
"github.com/IBM/fp-go/v2/function"
|
||||||
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
"github.com/IBM/fp-go/v2/internal/record"
|
"github.com/IBM/fp-go/v2/internal/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
|||||||
Map[[]B, func(B) []B],
|
Map[[]B, func(B) []B],
|
||||||
Ap[[]B, B],
|
Ap[[]B, B],
|
||||||
|
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, ma
|
|||||||
Map[map[K]B, func(B) map[K]B],
|
Map[map[K]B, func(B) map[K]B],
|
||||||
Ap[map[K]B, B],
|
Ap[map[K]B, B],
|
||||||
|
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
|
|||||||
Map[[]B, func(B) []B],
|
Map[[]B, func(B) []B],
|
||||||
ApSeq[[]B, B],
|
ApSeq[[]B, B],
|
||||||
as,
|
as,
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +140,7 @@ func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
|||||||
Of[[]B],
|
Of[[]B],
|
||||||
Map[[]B, func(B) []B],
|
Map[[]B, func(B) []B],
|
||||||
ApSeq[[]B, B],
|
ApSeq[[]B, B],
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
|
|||||||
Map[map[K]B, func(B) map[K]B],
|
Map[map[K]B, func(B) map[K]B],
|
||||||
ApSeq[map[K]B, B],
|
ApSeq[map[K]B, B],
|
||||||
as,
|
as,
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
|
|||||||
Map[map[K]B, func(B) map[K]B],
|
Map[map[K]B, func(B) map[K]B],
|
||||||
ApSeq[map[K]B, B],
|
ApSeq[map[K]B, B],
|
||||||
|
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +217,7 @@ func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
|
|||||||
Map[[]B, func(B) []B],
|
Map[[]B, func(B) []B],
|
||||||
ApPar[[]B, B],
|
ApPar[[]B, B],
|
||||||
as,
|
as,
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +233,7 @@ func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
|||||||
Of[[]B],
|
Of[[]B],
|
||||||
Map[[]B, func(B) []B],
|
Map[[]B, func(B) []B],
|
||||||
ApPar[[]B, B],
|
ApPar[[]B, B],
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +265,7 @@ func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
|
|||||||
Map[map[K]B, func(B) map[K]B],
|
Map[map[K]B, func(B) map[K]B],
|
||||||
ApPar[map[K]B, B],
|
ApPar[map[K]B, B],
|
||||||
|
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +287,7 @@ func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
|
|||||||
Map[map[K]B, func(B) map[K]B],
|
Map[map[K]B, func(B) map[K]B],
|
||||||
ApPar[map[K]B, B],
|
ApPar[map[K]B, B],
|
||||||
as,
|
as,
|
||||||
f,
|
F.Flow2(f, WithContext),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,12 +18,16 @@ package readerioresult
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/IBM/fp-go/v2/consumer"
|
||||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||||
"github.com/IBM/fp-go/v2/either"
|
"github.com/IBM/fp-go/v2/either"
|
||||||
|
"github.com/IBM/fp-go/v2/endomorphism"
|
||||||
"github.com/IBM/fp-go/v2/io"
|
"github.com/IBM/fp-go/v2/io"
|
||||||
"github.com/IBM/fp-go/v2/ioeither"
|
"github.com/IBM/fp-go/v2/ioeither"
|
||||||
"github.com/IBM/fp-go/v2/lazy"
|
"github.com/IBM/fp-go/v2/lazy"
|
||||||
|
"github.com/IBM/fp-go/v2/optics/lens"
|
||||||
|
"github.com/IBM/fp-go/v2/optics/prism"
|
||||||
"github.com/IBM/fp-go/v2/option"
|
"github.com/IBM/fp-go/v2/option"
|
||||||
"github.com/IBM/fp-go/v2/reader"
|
"github.com/IBM/fp-go/v2/reader"
|
||||||
"github.com/IBM/fp-go/v2/readereither"
|
"github.com/IBM/fp-go/v2/readereither"
|
||||||
@@ -126,4 +130,11 @@ type (
|
|||||||
ReaderResult[A any] = readerresult.ReaderResult[A]
|
ReaderResult[A any] = readerresult.ReaderResult[A]
|
||||||
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
|
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
|
||||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||||
|
|
||||||
|
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||||
|
|
||||||
|
Consumer[A any] = consumer.Consumer[A]
|
||||||
|
|
||||||
|
Prism[S, T any] = prism.Prism[S, T]
|
||||||
|
Lens[S, T any] = lens.Lens[S, T]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,11 +15,14 @@
|
|||||||
|
|
||||||
package readerresult
|
package readerresult
|
||||||
|
|
||||||
import "github.com/IBM/fp-go/v2/readereither"
|
import (
|
||||||
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
|
"github.com/IBM/fp-go/v2/readereither"
|
||||||
|
)
|
||||||
|
|
||||||
// TraverseArray transforms an array
|
// TraverseArray transforms an array
|
||||||
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||||
return readereither.TraverseArray(f)
|
return readereither.TraverseArray(F.Flow2(f, WithContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TraverseArrayWithIndex transforms an array
|
// TraverseArrayWithIndex transforms an array
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ package readerresult
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
|
||||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,16 +30,26 @@ import (
|
|||||||
// TenantID string
|
// TenantID string
|
||||||
// }
|
// }
|
||||||
// result := readereither.Do(State{})
|
// result := readereither.Do(State{})
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Do[S any](
|
func Do[S any](
|
||||||
empty S,
|
empty S,
|
||||||
) ReaderResult[S] {
|
) ReaderResult[S] {
|
||||||
return G.Do[ReaderResult[S]](empty)
|
return G.Do[ReaderResult[S]](empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
// Bind attaches the result of an EFFECTFUL computation to a context [S1] to produce a context [S2].
|
||||||
// This enables sequential composition where each step can depend on the results of previous steps
|
// This enables sequential composition where each step can depend on the results of previous steps
|
||||||
// and access the context.Context from the environment.
|
// and access the context.Context from the environment.
|
||||||
//
|
//
|
||||||
|
// IMPORTANT: Bind is for EFFECTFUL FUNCTIONS that depend on context.Context.
|
||||||
|
// The function parameter takes state and returns a ReaderResult[T], which is effectful because
|
||||||
|
// it depends on context.Context (can be cancelled, has deadlines, carries values).
|
||||||
|
//
|
||||||
|
// For PURE FUNCTIONS (side-effect free), use:
|
||||||
|
// - BindResultK: For pure functions with errors (State -> (Value, error))
|
||||||
|
// - Let: For pure functions without errors (State -> Value)
|
||||||
|
//
|
||||||
// The setter function takes the result of the computation and returns a function that
|
// The setter function takes the result of the computation and returns a function that
|
||||||
// updates the context from S1 to S2.
|
// updates the context from S1 to S2.
|
||||||
//
|
//
|
||||||
@@ -78,14 +87,27 @@ func Do[S any](
|
|||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
// )
|
// )
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Bind[S1, S2, T any](
|
func Bind[S1, S2, T any](
|
||||||
setter func(T) func(S1) S2,
|
setter func(T) func(S1) S2,
|
||||||
f Kleisli[S1, T],
|
f Kleisli[S1, T],
|
||||||
) Kleisli[ReaderResult[S1], S2] {
|
) Kleisli[ReaderResult[S1], S2] {
|
||||||
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, f)
|
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, F.Flow2(f, WithContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
// Let attaches the result of a PURE computation to a context [S1] to produce a context [S2].
|
||||||
|
//
|
||||||
|
// IMPORTANT: Let is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
|
||||||
|
// The function parameter takes state and returns a value directly, with no errors or effects.
|
||||||
|
//
|
||||||
|
// For EFFECTFUL FUNCTIONS (that need context.Context), use:
|
||||||
|
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
|
||||||
|
//
|
||||||
|
// For PURE FUNCTIONS with error handling, use:
|
||||||
|
// - BindResultK: For pure functions with errors (State -> (Value, error))
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Let[S1, S2, T any](
|
func Let[S1, S2, T any](
|
||||||
setter func(T) func(S1) S2,
|
setter func(T) func(S1) S2,
|
||||||
f func(S1) T,
|
f func(S1) T,
|
||||||
@@ -93,7 +115,10 @@ func Let[S1, S2, T any](
|
|||||||
return G.Let[ReaderResult[S1], ReaderResult[S2]](setter, f)
|
return G.Let[ReaderResult[S1], ReaderResult[S2]](setter, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
|
||||||
|
// This is a PURE operation (side-effect free) that simply sets a field to a constant value.
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func LetTo[S1, S2, T any](
|
func LetTo[S1, S2, T any](
|
||||||
setter func(T) func(S1) S2,
|
setter func(T) func(S1) S2,
|
||||||
b T,
|
b T,
|
||||||
@@ -102,15 +127,27 @@ func LetTo[S1, S2, T any](
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BindTo initializes a new state [S1] from a value [T]
|
// BindTo initializes a new state [S1] from a value [T]
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func BindTo[S1, T any](
|
func BindTo[S1, T any](
|
||||||
setter func(T) S1,
|
setter func(T) S1,
|
||||||
) Kleisli[ReaderResult[T], S1] {
|
) Operator[T, S1] {
|
||||||
return G.BindTo[ReaderResult[S1], ReaderResult[T]](setter)
|
return G.BindTo[ReaderResult[S1], ReaderResult[T]](setter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func BindToP[S1, T any](
|
||||||
|
setter Prism[S1, T],
|
||||||
|
) Operator[T, S1] {
|
||||||
|
return BindTo(setter.ReverseGet)
|
||||||
|
}
|
||||||
|
|
||||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||||
// the context and the value concurrently (using Applicative rather than Monad).
|
// the context and the value concurrently (using Applicative rather than Monad).
|
||||||
// This allows independent computations to be combined without one depending on the result of the other.
|
// This allows independent EFFECTFUL computations to be combined without one depending on the result of the other.
|
||||||
|
//
|
||||||
|
// IMPORTANT: ApS is for EFFECTFUL FUNCTIONS that depend on context.Context.
|
||||||
|
// The ReaderResult parameter is effectful because it depends on context.Context.
|
||||||
//
|
//
|
||||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||||
// and can conceptually run in parallel.
|
// and can conceptually run in parallel.
|
||||||
@@ -145,6 +182,8 @@ func BindTo[S1, T any](
|
|||||||
// getTenantID,
|
// getTenantID,
|
||||||
// ),
|
// ),
|
||||||
// )
|
// )
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func ApS[S1, S2, T any](
|
func ApS[S1, S2, T any](
|
||||||
setter func(T) func(S1) S2,
|
setter func(T) func(S1) S2,
|
||||||
fa ReaderResult[T],
|
fa ReaderResult[T],
|
||||||
@@ -183,17 +222,24 @@ func ApS[S1, S2, T any](
|
|||||||
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
||||||
// readereither.ApSL(ageLens, getAge),
|
// readereither.ApSL(ageLens, getAge),
|
||||||
// )
|
// )
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func ApSL[S, T any](
|
func ApSL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
fa ReaderResult[T],
|
fa ReaderResult[T],
|
||||||
) Kleisli[ReaderResult[S], S] {
|
) Kleisli[ReaderResult[S], S] {
|
||||||
return ApS(lens.Set, fa)
|
return ApS(lens.Set, fa)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
|
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
|
||||||
// It combines the lens-based field access with monadic composition, allowing you to:
|
// It combines the lens-based field access with monadic composition for EFFECTFUL computations.
|
||||||
|
//
|
||||||
|
// IMPORTANT: BindL is for EFFECTFUL FUNCTIONS that depend on context.Context.
|
||||||
|
// The function parameter returns a ReaderResult, which is effectful.
|
||||||
|
//
|
||||||
|
// It allows you to:
|
||||||
// 1. Extract a field value using the lens
|
// 1. Extract a field value using the lens
|
||||||
// 2. Use that value in a computation that may fail
|
// 2. Use that value in an effectful computation that may fail
|
||||||
// 3. Update the field with the result
|
// 3. Update the field with the result
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
@@ -227,15 +273,20 @@ func ApSL[S, T any](
|
|||||||
// readereither.Of[error](Counter{Value: 42}),
|
// readereither.Of[error](Counter{Value: 42}),
|
||||||
// readereither.BindL(valueLens, increment),
|
// readereither.BindL(valueLens, increment),
|
||||||
// )
|
// )
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func BindL[S, T any](
|
func BindL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f Kleisli[T, T],
|
f Kleisli[T, T],
|
||||||
) Kleisli[ReaderResult[S], S] {
|
) Kleisli[ReaderResult[S], S] {
|
||||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
return Bind(lens.Set, F.Flow2(lens.Get, F.Flow2(f, WithContext)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
|
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
|
||||||
// It applies a pure transformation to the focused field without any effects.
|
// It applies a PURE transformation to the focused field without any effects.
|
||||||
|
//
|
||||||
|
// IMPORTANT: LetL is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
|
||||||
|
// The function parameter is a pure endomorphism (T -> T) with no errors or effects.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - lens: A lens that focuses on a field of type T within state S
|
// - lens: A lens that focuses on a field of type T within state S
|
||||||
@@ -262,15 +313,17 @@ func BindL[S, T any](
|
|||||||
// readereither.LetL(valueLens, double),
|
// readereither.LetL(valueLens, double),
|
||||||
// )
|
// )
|
||||||
// // result when executed will be Right(Counter{Value: 42})
|
// // result when executed will be Right(Counter{Value: 42})
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func LetL[S, T any](
|
func LetL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
f func(T) T,
|
f Endomorphism[T],
|
||||||
) Kleisli[ReaderResult[S], S] {
|
) Kleisli[ReaderResult[S], S] {
|
||||||
return Let(lens.Set, F.Flow2(lens.Get, f))
|
return Let(lens.Set, F.Flow2(lens.Get, f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
|
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
|
||||||
// It sets the focused field to a constant value.
|
// It sets the focused field to a constant value. This is a PURE operation (side-effect free).
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - lens: A lens that focuses on a field of type T within state S
|
// - lens: A lens that focuses on a field of type T within state S
|
||||||
@@ -296,8 +349,10 @@ func LetL[S, T any](
|
|||||||
// readereither.LetToL(debugLens, false),
|
// readereither.LetToL(debugLens, false),
|
||||||
// )
|
// )
|
||||||
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func LetToL[S, T any](
|
func LetToL[S, T any](
|
||||||
lens L.Lens[S, T],
|
lens Lens[S, T],
|
||||||
b T,
|
b T,
|
||||||
) Kleisli[ReaderResult[S], S] {
|
) Kleisli[ReaderResult[S], S] {
|
||||||
return LetTo(lens.Set, b)
|
return LetTo(lens.Set, b)
|
||||||
|
|||||||
@@ -19,14 +19,23 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
E "github.com/IBM/fp-go/v2/either"
|
E "github.com/IBM/fp-go/v2/either"
|
||||||
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
)
|
)
|
||||||
|
|
||||||
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
|
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
|
||||||
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
||||||
return func(ctx context.Context) E.Either[error, A] {
|
return func(ctx context.Context) E.Either[error, A] {
|
||||||
if err := context.Cause(ctx); err != nil {
|
if ctx.Err() != nil {
|
||||||
return E.Left[A](err)
|
return E.Left[A](context.Cause(ctx))
|
||||||
}
|
}
|
||||||
return ma(ctx)
|
return ma(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
|
||||||
|
return F.Flow2(
|
||||||
|
f,
|
||||||
|
WithContext,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
package readerresult
|
package readerresult
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,11 +22,131 @@ import (
|
|||||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SequenceReader swaps the order of environment parameters when the inner computation is a Reader.
|
||||||
|
//
|
||||||
|
// This function is specialized for the context.Context-based ReaderResult monad. It takes a
|
||||||
|
// ReaderResult that produces a Reader and returns a reader.Kleisli that produces Results.
|
||||||
|
// The context.Context is implicitly used as the outer environment type.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - R: The inner environment type (becomes outer after flip)
|
||||||
|
// - A: The success value type
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ma: A ReaderResult that takes context.Context and may produce a Reader[R, A]
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A reader.Kleisli[context.Context, R, Result[A]], which is func(context.Context) func(R) Result[A]
|
||||||
|
//
|
||||||
|
// The function preserves error handling from the outer ReaderResult layer. If the outer
|
||||||
|
// computation fails, the error is propagated to the inner Result.
|
||||||
|
//
|
||||||
|
// Note: This is an inline wrapper around readerresult.SequenceReader, specialized for
|
||||||
|
// context.Context as the outer environment type.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// type Database struct {
|
||||||
|
// ConnectionString string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Original: takes context, may fail, produces Reader[Database, string]
|
||||||
|
// original := func(ctx context.Context) result.Result[reader.Reader[Database, string]] {
|
||||||
|
// if ctx.Err() != nil {
|
||||||
|
// return result.Error[reader.Reader[Database, string]](ctx.Err())
|
||||||
|
// }
|
||||||
|
// return result.Ok[error](func(db Database) string {
|
||||||
|
// return fmt.Sprintf("Query on %s", db.ConnectionString)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Sequenced: takes context first, then Database
|
||||||
|
// sequenced := SequenceReader(original)
|
||||||
|
//
|
||||||
|
// ctx := context.Background()
|
||||||
|
// db := Database{ConnectionString: "localhost:5432"}
|
||||||
|
//
|
||||||
|
// // Apply context first to get a function that takes database
|
||||||
|
// dbReader := sequenced(ctx)
|
||||||
|
// // Then apply database to get the final result
|
||||||
|
// result := dbReader(db)
|
||||||
|
// // result is Result[string]
|
||||||
|
//
|
||||||
|
// Use Cases:
|
||||||
|
// - Dependency injection: Flip parameter order to inject context first, then dependencies
|
||||||
|
// - Testing: Separate context handling from business logic for easier testing
|
||||||
|
// - Composition: Enable point-free style by fixing the context parameter first
|
||||||
|
//
|
||||||
//go:inline
|
//go:inline
|
||||||
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
|
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
|
||||||
return RR.SequenceReader(ma)
|
return RR.SequenceReader(ma)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TraverseReader transforms a value using a Reader function and swaps environment parameter order.
|
||||||
|
//
|
||||||
|
// This function combines mapping and parameter flipping in a single operation. It takes a
|
||||||
|
// Reader function (pure computation without error handling) and returns a function that:
|
||||||
|
// 1. Maps a ReaderResult[A] to ReaderResult[B] using the provided Reader function
|
||||||
|
// 2. Flips the parameter order so R comes before context.Context
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - R: The inner environment type (becomes outer after flip)
|
||||||
|
// - A: The input value type
|
||||||
|
// - B: The output value type
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - f: A reader.Kleisli[R, A, B], which is func(R) func(A) B - a pure Reader function
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A function that takes ReaderResult[A] and returns Kleisli[R, B]
|
||||||
|
// - Kleisli[R, B] is func(R) ReaderResult[B], which is func(R) func(context.Context) Result[B]
|
||||||
|
//
|
||||||
|
// The function preserves error handling from the input ReaderResult. If the input computation
|
||||||
|
// fails, the error is propagated without applying the transformation function.
|
||||||
|
//
|
||||||
|
// Note: This is a wrapper around readerresult.TraverseReader, specialized for context.Context.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// type Config struct {
|
||||||
|
// MaxRetries int
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // A pure Reader function that depends on Config
|
||||||
|
// formatMessage := func(cfg Config) func(int) string {
|
||||||
|
// return func(value int) string {
|
||||||
|
// return fmt.Sprintf("Value: %d, MaxRetries: %d", value, cfg.MaxRetries)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Original computation that may fail
|
||||||
|
// computation := func(ctx context.Context) result.Result[int] {
|
||||||
|
// if ctx.Err() != nil {
|
||||||
|
// return result.Error[int](ctx.Err())
|
||||||
|
// }
|
||||||
|
// return result.Ok[error](42)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Create a traversal that applies formatMessage and flips parameters
|
||||||
|
// traverse := TraverseReader[Config, int, string](formatMessage)
|
||||||
|
//
|
||||||
|
// // Apply to the computation
|
||||||
|
// flipped := traverse(computation)
|
||||||
|
//
|
||||||
|
// // Now we can provide Config first, then context
|
||||||
|
// cfg := Config{MaxRetries: 3}
|
||||||
|
// ctx := context.Background()
|
||||||
|
//
|
||||||
|
// result := flipped(cfg)(ctx)
|
||||||
|
// // result is Result[string] containing "Value: 42, MaxRetries: 3"
|
||||||
|
//
|
||||||
|
// Use Cases:
|
||||||
|
// - Dependency injection: Inject configuration/dependencies before context
|
||||||
|
// - Testing: Separate pure business logic from context handling
|
||||||
|
// - Composition: Build pipelines where dependencies are fixed before execution
|
||||||
|
// - Point-free style: Enable partial application by fixing dependencies first
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func TraverseReader[R, A, B any](
|
func TraverseReader[R, A, B any](
|
||||||
f reader.Kleisli[R, A, B],
|
f reader.Kleisli[R, A, B],
|
||||||
) func(ReaderResult[A]) Kleisli[R, B] {
|
) func(ReaderResult[A]) Kleisli[R, B] {
|
||||||
|
|||||||
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
|
"github.com/IBM/fp-go/v2/internal/chain"
|
||||||
|
"github.com/IBM/fp-go/v2/option"
|
||||||
|
"github.com/IBM/fp-go/v2/reader"
|
||||||
"github.com/IBM/fp-go/v2/readereither"
|
"github.com/IBM/fp-go/v2/readereither"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
|
||||||
|
return readereither.FromReader[error](r)
|
||||||
|
}
|
||||||
|
|
||||||
func FromEither[A any](e Either[A]) ReaderResult[A] {
|
func FromEither[A any](e Either[A]) ReaderResult[A] {
|
||||||
return readereither.FromEither[context.Context](e)
|
return readereither.FromEither[context.Context](e)
|
||||||
}
|
}
|
||||||
@@ -42,11 +50,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
|
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
|
||||||
return readereither.MonadChain(ma, f)
|
return readereither.MonadChain(ma, F.Flow2(f, WithContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||||
return readereither.Chain(f)
|
return readereither.Chain(F.Flow2(f, WithContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Of[A any](a A) ReaderResult[A] {
|
func Of[A any](a A) ReaderResult[A] {
|
||||||
@@ -66,7 +74,7 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
|
|||||||
}
|
}
|
||||||
|
|
||||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
|
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
|
||||||
return readereither.OrElse(onLeft)
|
return readereither.OrElse(F.Flow2(onLeft, WithContext))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Ask() ReaderResult[context.Context] {
|
func Ask() ReaderResult[context.Context] {
|
||||||
@@ -81,7 +89,7 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderResult[A]) Reader
|
|||||||
return readereither.ChainEitherK[context.Context](f)
|
return readereither.ChainEitherK[context.Context](f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||||
return readereither.ChainOptionK[context.Context, A, B](onNone)
|
return readereither.ChainOptionK[context.Context, A, B](onNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,3 +105,197 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
|||||||
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
|
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
|
||||||
return readereither.Read[error, A](r)
|
return readereither.Read[error, A](r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MonadMapTo executes a ReaderResult computation, discards its success value, and returns a constant value.
|
||||||
|
// This is the monadic version that takes both the ReaderResult and the constant value as parameters.
|
||||||
|
//
|
||||||
|
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||||
|
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadMapTo WILL
|
||||||
|
// execute the original ReaderResult to allow any side effects to occur, then discard the success result
|
||||||
|
// and return the constant value. If the original computation fails, the error is preserved.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||||
|
// - B: The type of the constant value to return on success
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ma: The ReaderResult to execute (side effects will occur, success value discarded)
|
||||||
|
// - b: The constant value to return if ma succeeds
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A ReaderResult that executes ma, preserves errors, but replaces success values with b
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// type Config struct { Counter int }
|
||||||
|
// increment := func(ctx context.Context) result.Result[int] {
|
||||||
|
// // Side effect: log the operation
|
||||||
|
// fmt.Println("incrementing")
|
||||||
|
// return result.Of(5)
|
||||||
|
// }
|
||||||
|
// r := readerresult.MonadMapTo(increment, "done")
|
||||||
|
// result := r(context.Background()) // Prints "incrementing", returns Right("done")
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
|
func MonadMapTo[A, B any](ma ReaderResult[A], b B) ReaderResult[B] {
|
||||||
|
return MonadMap(ma, reader.Of[A](b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapTo creates an operator that executes a ReaderResult computation, discards its success value,
|
||||||
|
// and returns a constant value. This is the curried version where the constant value is provided first,
|
||||||
|
// returning a function that can be applied to any ReaderResult.
|
||||||
|
//
|
||||||
|
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||||
|
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MapTo WILL
|
||||||
|
// execute the input ReaderResult to allow any side effects to occur, then discard the success result
|
||||||
|
// and return the constant value. If the computation fails, the error is preserved.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The success type of the input ReaderResult (will be discarded if successful)
|
||||||
|
// - B: The type of the constant value to return on success
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - b: The constant value to return on success
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that executes a ReaderResult[A], preserves errors, but replaces success with b
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// logStep := func(ctx context.Context) result.Result[int] {
|
||||||
|
// fmt.Println("step executed")
|
||||||
|
// return result.Of(42)
|
||||||
|
// }
|
||||||
|
// toDone := readerresult.MapTo[int, string]("done")
|
||||||
|
// pipeline := toDone(logStep)
|
||||||
|
// result := pipeline(context.Background()) // Prints "step executed", returns Right("done")
|
||||||
|
//
|
||||||
|
// Example - In a functional pipeline:
|
||||||
|
//
|
||||||
|
// step1 := func(ctx context.Context) result.Result[int] {
|
||||||
|
// fmt.Println("processing")
|
||||||
|
// return result.Of(1)
|
||||||
|
// }
|
||||||
|
// pipeline := F.Pipe1(
|
||||||
|
// step1,
|
||||||
|
// readerresult.MapTo[int, string]("complete"),
|
||||||
|
// )
|
||||||
|
// output := pipeline(context.Background()) // Prints "processing", returns Right("complete")
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
|
func MapTo[A, B any](b B) Operator[A, B] {
|
||||||
|
return Map(reader.Of[A](b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonadChainTo sequences two ReaderResult computations where the second ignores the first's success value.
|
||||||
|
// This is the monadic version that takes both ReaderResults as parameters.
|
||||||
|
//
|
||||||
|
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||||
|
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadChainTo WILL
|
||||||
|
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||||
|
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||||
|
// returned immediately without executing the second computation.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||||
|
// - B: The success type of the second ReaderResult
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - ma: The first ReaderResult to execute (side effects will occur, success value discarded)
|
||||||
|
// - b: The second ReaderResult to execute if ma succeeds
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A ReaderResult that executes ma, then b if ma succeeds, returning b's result
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// logStart := func(ctx context.Context) result.Result[int] {
|
||||||
|
// fmt.Println("starting")
|
||||||
|
// return result.Of(1)
|
||||||
|
// }
|
||||||
|
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||||
|
// fmt.Println("ending")
|
||||||
|
// return result.Of("done")
|
||||||
|
// }
|
||||||
|
// r := readerresult.MonadChainTo(logStart, logEnd)
|
||||||
|
// result := r(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
|
func MonadChainTo[A, B any](ma ReaderResult[A], b ReaderResult[B]) ReaderResult[B] {
|
||||||
|
return MonadChain(ma, reader.Of[A](b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainTo creates an operator that sequences two ReaderResult computations where the second ignores
|
||||||
|
// the first's success value. This is the curried version where the second ReaderResult is provided first,
|
||||||
|
// returning a function that can be applied to any first ReaderResult.
|
||||||
|
//
|
||||||
|
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||||
|
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, ChainTo WILL
|
||||||
|
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||||
|
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||||
|
// returned immediately without executing the second computation.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||||
|
// - B: The success type of the second ReaderResult
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - b: The second ReaderResult to execute after the first succeeds
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An Operator that executes the first ReaderResult, then b if successful
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||||
|
// fmt.Println("ending")
|
||||||
|
// return result.Of("done")
|
||||||
|
// }
|
||||||
|
// thenLogEnd := readerresult.ChainTo[int, string](logEnd)
|
||||||
|
//
|
||||||
|
// logStart := func(ctx context.Context) result.Result[int] {
|
||||||
|
// fmt.Println("starting")
|
||||||
|
// return result.Of(1)
|
||||||
|
// }
|
||||||
|
// pipeline := thenLogEnd(logStart)
|
||||||
|
// result := pipeline(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||||
|
//
|
||||||
|
// Example - In a functional pipeline:
|
||||||
|
//
|
||||||
|
// step1 := func(ctx context.Context) result.Result[int] {
|
||||||
|
// fmt.Println("step 1")
|
||||||
|
// return result.Of(1)
|
||||||
|
// }
|
||||||
|
// step2 := func(ctx context.Context) result.Result[string] {
|
||||||
|
// fmt.Println("step 2")
|
||||||
|
// return result.Of("complete")
|
||||||
|
// }
|
||||||
|
// pipeline := F.Pipe1(
|
||||||
|
// step1,
|
||||||
|
// readerresult.ChainTo[int, string](step2),
|
||||||
|
// )
|
||||||
|
// output := pipeline(context.Background()) // Prints "step 1" then "step 2", returns Right("complete")
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
|
func ChainTo[A, B any](b ReaderResult[B]) Operator[A, B] {
|
||||||
|
return Chain(reader.Of[A](b))
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func MonadChainFirst[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[A] {
|
||||||
|
return chain.MonadChainFirst(
|
||||||
|
MonadChain,
|
||||||
|
MonadMap,
|
||||||
|
ma,
|
||||||
|
F.Flow2(f, WithContext),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:inline
|
||||||
|
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||||
|
return chain.ChainFirst(
|
||||||
|
Chain,
|
||||||
|
Map,
|
||||||
|
F.Flow2(f, WithContext),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
|
// Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error.
|
||||||
|
//
|
||||||
|
// # Pure vs Effectful Functions
|
||||||
|
//
|
||||||
|
// This package distinguishes between pure (side-effect free) and effectful (side-effectful) functions:
|
||||||
|
//
|
||||||
|
// EFFECTFUL FUNCTIONS (depend on context.Context):
|
||||||
|
// - ReaderResult[A]: func(context.Context) (A, error) - Effectful computation that needs context
|
||||||
|
// - These functions are effectful because context.Context is effectful (can be cancelled, has deadlines, carries values)
|
||||||
|
// - Use for: operations that need cancellation, timeouts, context values, or any context-dependent behavior
|
||||||
|
// - Examples: database queries, HTTP requests, operations that respect cancellation
|
||||||
|
//
|
||||||
|
// PURE FUNCTIONS (side-effect free):
|
||||||
|
// - func(State) (Value, error) - Pure computation that only depends on state, not context
|
||||||
|
// - func(State) Value - Pure transformation without errors
|
||||||
|
// - These functions are pure because they only read from their input state and don't depend on external context
|
||||||
|
// - Use for: parsing, validation, calculations, data transformations that don't need context
|
||||||
|
// - Examples: JSON parsing, input validation, mathematical computations
|
||||||
|
//
|
||||||
|
// The package provides different bind operations for each:
|
||||||
|
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
|
||||||
|
// - BindResultK: For pure functions with errors (State -> (Value, error))
|
||||||
|
// - Let: For pure functions without errors (State -> Value)
|
||||||
|
// - BindReaderK: For context-dependent pure functions (State -> Reader[Context, Value])
|
||||||
|
// - BindEitherK: For pure Result/Either values (State -> Result[Value])
|
||||||
package readerresult
|
package readerresult
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/IBM/fp-go/v2/either"
|
"github.com/IBM/fp-go/v2/either"
|
||||||
|
"github.com/IBM/fp-go/v2/endomorphism"
|
||||||
|
"github.com/IBM/fp-go/v2/optics/lens"
|
||||||
|
"github.com/IBM/fp-go/v2/optics/prism"
|
||||||
"github.com/IBM/fp-go/v2/option"
|
"github.com/IBM/fp-go/v2/option"
|
||||||
"github.com/IBM/fp-go/v2/reader"
|
"github.com/IBM/fp-go/v2/reader"
|
||||||
"github.com/IBM/fp-go/v2/readereither"
|
"github.com/IBM/fp-go/v2/readereither"
|
||||||
@@ -34,6 +61,9 @@ type (
|
|||||||
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
|
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
|
||||||
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
|
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||||
|
|
||||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||||
|
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||||
|
Prism[S, T any] = prism.Prism[S, T]
|
||||||
|
Lens[S, T any] = lens.Lens[S, T]
|
||||||
)
|
)
|
||||||
|
|||||||
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] {
|
func (t *token[T]) ProviderFactory() Option[DIE.ProviderFactory] {
|
||||||
return t.base.providerFactory
|
return t.base.providerFactory
|
||||||
}
|
}
|
||||||
func makeTokenBase(name string, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
|
func makeTokenBase(name, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
|
||||||
return &tokenBase{name, id, typ, providerFactory}
|
return &tokenBase{name, id, typ, providerFactory}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeToken[T any](name string, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
|
func makeToken[T any](name, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
|
||||||
return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
|
return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
|
|||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// validate := func(i int, s string) either.Either[error, string] {
|
// validate := func(i int, s string) either.Either[error, string] {
|
||||||
// if len(s) > 0 {
|
// if S.IsNonEmpty(s) {
|
||||||
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
|
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
|
||||||
// }
|
// }
|
||||||
// return either.Left[string](fmt.Errorf("empty at index %d", i))
|
// return either.Left[string](fmt.Errorf("empty at index %d", i))
|
||||||
@@ -105,7 +105,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, E, A, B any](f func(int, A) Eithe
|
|||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// validate := func(i int, s string) either.Either[error, string] {
|
// validate := func(i int, s string) either.Either[error, string] {
|
||||||
// if len(s) > 0 {
|
// if S.IsNonEmpty(s) {
|
||||||
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
|
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
|
||||||
// }
|
// }
|
||||||
// return either.Left[string](fmt.Errorf("empty at index %d", i))
|
// return either.Left[string](fmt.Errorf("empty at index %d", i))
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func Curry0[R any](f func() (R, error)) func() Either[error, R] {
|
|||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
// parse := strconv.Atoi
|
||||||
// curried := either.Curry1(parse)
|
// curried := either.Curry1(parse)
|
||||||
// result := curried("42") // Right(42)
|
// result := curried("42") // Right(42)
|
||||||
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] {
|
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] {
|
||||||
|
|||||||
@@ -19,6 +19,21 @@
|
|||||||
// - Left represents an error or failure case (type E)
|
// - Left represents an error or failure case (type E)
|
||||||
// - Right represents a success case (type A)
|
// - Right represents a success case (type A)
|
||||||
//
|
//
|
||||||
|
// # Fantasy Land Specification
|
||||||
|
//
|
||||||
|
// This implementation corresponds to the Fantasy Land Either type:
|
||||||
|
// https://github.com/fantasyland/fantasy-land#either
|
||||||
|
//
|
||||||
|
// Implemented Fantasy Land algebras:
|
||||||
|
// - Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||||
|
// - Bifunctor: https://github.com/fantasyland/fantasy-land#bifunctor
|
||||||
|
// - Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||||
|
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||||
|
// - Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||||
|
// - Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||||
|
// - Alt: https://github.com/fantasyland/fantasy-land#alt
|
||||||
|
// - Foldable: https://github.com/fantasyland/fantasy-land#foldable
|
||||||
|
//
|
||||||
// # Core Concepts
|
// # Core Concepts
|
||||||
//
|
//
|
||||||
// The Either type is a discriminated union that can hold either a Left value (typically an error)
|
// The Either type is a discriminated union that can hold either a Left value (typically an error)
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
M "github.com/IBM/fp-go/v2/monoid"
|
N "github.com/IBM/fp-go/v2/number"
|
||||||
O "github.com/IBM/fp-go/v2/option"
|
O "github.com/IBM/fp-go/v2/option"
|
||||||
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -305,7 +306,7 @@ func TestTraverseArray(t *testing.T) {
|
|||||||
// Test TraverseArrayWithIndex
|
// Test TraverseArrayWithIndex
|
||||||
func TestTraverseArrayWithIndex(t *testing.T) {
|
func TestTraverseArrayWithIndex(t *testing.T) {
|
||||||
validate := func(i int, s string) Either[error, string] {
|
validate := func(i int, s string) Either[error, string] {
|
||||||
if len(s) > 0 {
|
if S.IsNonEmpty(s) {
|
||||||
return Right[error](fmt.Sprintf("%d:%s", i, s))
|
return Right[error](fmt.Sprintf("%d:%s", i, s))
|
||||||
}
|
}
|
||||||
return Left[string](fmt.Errorf("empty at index %d", i))
|
return Left[string](fmt.Errorf("empty at index %d", i))
|
||||||
@@ -334,7 +335,7 @@ func TestTraverseRecord(t *testing.T) {
|
|||||||
// Test TraverseRecordWithIndex
|
// Test TraverseRecordWithIndex
|
||||||
func TestTraverseRecordWithIndex(t *testing.T) {
|
func TestTraverseRecordWithIndex(t *testing.T) {
|
||||||
validate := func(k string, v string) Either[error, string] {
|
validate := func(k string, v string) Either[error, string] {
|
||||||
if len(v) > 0 {
|
if S.IsNonEmpty(v) {
|
||||||
return Right[error](k + ":" + v)
|
return Right[error](k + ":" + v)
|
||||||
}
|
}
|
||||||
return Left[string](fmt.Errorf("empty value for key %s", k))
|
return Left[string](fmt.Errorf("empty value for key %s", k))
|
||||||
@@ -373,7 +374,7 @@ func TestCurry0(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCurry1(t *testing.T) {
|
func TestCurry1(t *testing.T) {
|
||||||
parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
parse := strconv.Atoi
|
||||||
curried := Curry1(parse)
|
curried := Curry1(parse)
|
||||||
result := curried("42")
|
result := curried("42")
|
||||||
assert.Equal(t, Right[error](42), result)
|
assert.Equal(t, Right[error](42), result)
|
||||||
@@ -645,7 +646,7 @@ func TestAltSemigroup(t *testing.T) {
|
|||||||
|
|
||||||
// Test AlternativeMonoid
|
// Test AlternativeMonoid
|
||||||
func TestAlternativeMonoid(t *testing.T) {
|
func TestAlternativeMonoid(t *testing.T) {
|
||||||
intAdd := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
intAdd := N.MonoidSum[int]()
|
||||||
m := AlternativeMonoid[error](intAdd)
|
m := AlternativeMonoid[error](intAdd)
|
||||||
|
|
||||||
result := m.Concat(Right[error](1), Right[error](2))
|
result := m.Concat(Right[error](1), Right[error](2))
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
|
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
"github.com/IBM/fp-go/v2/internal/utils"
|
"github.com/IBM/fp-go/v2/internal/utils"
|
||||||
IO "github.com/IBM/fp-go/v2/io"
|
|
||||||
O "github.com/IBM/fp-go/v2/option"
|
O "github.com/IBM/fp-go/v2/option"
|
||||||
S "github.com/IBM/fp-go/v2/string"
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -120,10 +119,3 @@ func TestStringer(t *testing.T) {
|
|||||||
var s fmt.Stringer = &e
|
var s fmt.Stringer = &e
|
||||||
assert.Equal(t, exp, s.String())
|
assert.Equal(t, exp, s.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFromIO(t *testing.T) {
|
|
||||||
f := IO.Of("abc")
|
|
||||||
e := FromIO[error](f)
|
|
||||||
|
|
||||||
assert.Equal(t, Right[error]("abc"), e)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,11 +17,19 @@ package either
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
L "github.com/IBM/fp-go/v2/logging"
|
L "github.com/IBM/fp-go/v2/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// slogError creates a slog.Attr with key "error" for logging error values
|
||||||
|
slogError = F.Bind1st(slog.Any, "error")
|
||||||
|
// slogValue creates a slog.Attr with key "value" for logging success values
|
||||||
|
slogValue = F.Bind1st(slog.Any, "value")
|
||||||
|
)
|
||||||
|
|
||||||
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
|
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
|
||||||
return Fold(
|
return Fold(
|
||||||
func(e E) Either[E, A] {
|
func(e E) Either[E, A] {
|
||||||
@@ -62,3 +70,91 @@ func Logger[E, A any](loggers ...*log.Logger) func(string) Operator[E, A, A] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToSLogAttr converts an Either value to a structured logging attribute (slog.Attr).
|
||||||
|
//
|
||||||
|
// This function creates a converter that transforms Either values into slog.Attr for use
|
||||||
|
// with Go's structured logging (log/slog). It maps:
|
||||||
|
// - Left values to an "error" attribute
|
||||||
|
// - Right values to a "value" attribute
|
||||||
|
//
|
||||||
|
// This is particularly useful when integrating Either-based error handling with structured
|
||||||
|
// logging systems, allowing you to log both successful values and errors in a consistent,
|
||||||
|
// structured format.
|
||||||
|
//
|
||||||
|
// Type Parameters:
|
||||||
|
// - E: The Left (error) type of the Either
|
||||||
|
// - A: The Right (success) type of the Either
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A function that converts Either[E, A] to slog.Attr
|
||||||
|
//
|
||||||
|
// Example with Left (error):
|
||||||
|
//
|
||||||
|
// converter := either.ToSLogAttr[error, int]()
|
||||||
|
// leftValue := either.Left[int](errors.New("connection failed"))
|
||||||
|
// attr := converter(leftValue)
|
||||||
|
// // attr is: slog.Any("error", errors.New("connection failed"))
|
||||||
|
//
|
||||||
|
// logger.LogAttrs(ctx, slog.LevelError, "Operation failed", attr)
|
||||||
|
// // Logs: {"level":"error","msg":"Operation failed","error":"connection failed"}
|
||||||
|
//
|
||||||
|
// Example with Right (success):
|
||||||
|
//
|
||||||
|
// converter := either.ToSLogAttr[error, User]()
|
||||||
|
// rightValue := either.Right[error](User{ID: 123, Name: "Alice"})
|
||||||
|
// attr := converter(rightValue)
|
||||||
|
// // attr is: slog.Any("value", User{ID: 123, Name: "Alice"})
|
||||||
|
//
|
||||||
|
// logger.LogAttrs(ctx, slog.LevelInfo, "User fetched", attr)
|
||||||
|
// // Logs: {"level":"info","msg":"User fetched","value":{"ID":123,"Name":"Alice"}}
|
||||||
|
//
|
||||||
|
// Example in a pipeline with structured logging:
|
||||||
|
//
|
||||||
|
// toAttr := either.ToSLogAttr[error, Data]()
|
||||||
|
//
|
||||||
|
// result := F.Pipe2(
|
||||||
|
// fetchData(id),
|
||||||
|
// either.Map(processData),
|
||||||
|
// either.Map(validateData),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// attr := toAttr(result)
|
||||||
|
// logger.LogAttrs(ctx, slog.LevelInfo, "Data processing complete", attr)
|
||||||
|
// // Logs success: {"level":"info","msg":"Data processing complete","value":{...}}
|
||||||
|
// // Or error: {"level":"info","msg":"Data processing complete","error":"validation failed"}
|
||||||
|
//
|
||||||
|
// Example with custom log levels based on Either:
|
||||||
|
//
|
||||||
|
// toAttr := either.ToSLogAttr[error, Response]()
|
||||||
|
// result := callAPI(endpoint)
|
||||||
|
//
|
||||||
|
// level := either.Fold(
|
||||||
|
// func(error) slog.Level { return slog.LevelError },
|
||||||
|
// func(Response) slog.Level { return slog.LevelInfo },
|
||||||
|
// )(result)
|
||||||
|
//
|
||||||
|
// logger.LogAttrs(ctx, level, "API call completed", toAttr(result))
|
||||||
|
//
|
||||||
|
// Use Cases:
|
||||||
|
// - Structured logging: Convert Either results to structured log attributes
|
||||||
|
// - Error tracking: Log errors with consistent "error" key in structured logs
|
||||||
|
// - Success monitoring: Log successful values with consistent "value" key
|
||||||
|
// - Observability: Integrate Either-based error handling with logging systems
|
||||||
|
// - Debugging: Inspect Either values in logs with proper structure
|
||||||
|
// - Metrics: Extract Either values for metrics collection in logging pipelines
|
||||||
|
//
|
||||||
|
// Note: The returned slog.Attr uses "error" for Left values and "value" for Right values.
|
||||||
|
// These keys are consistent with common structured logging conventions.
|
||||||
|
func ToSLogAttr[E, A any]() func(Either[E, A]) slog.Attr {
|
||||||
|
return Fold(
|
||||||
|
F.Flow2(
|
||||||
|
F.ToAny[E],
|
||||||
|
slogError,
|
||||||
|
),
|
||||||
|
F.Flow2(
|
||||||
|
F.ToAny[A],
|
||||||
|
slogValue,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
package either
|
package either
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
|
N "github.com/IBM/fp-go/v2/number"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,3 +38,139 @@ func TestLogger(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, r, res)
|
assert.Equal(t, r, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_Left(t *testing.T) {
|
||||||
|
// Test with Left (error) value
|
||||||
|
converter := ToSLogAttr[error, int]()
|
||||||
|
testErr := errors.New("test error")
|
||||||
|
leftValue := Left[int](testErr)
|
||||||
|
|
||||||
|
attr := converter(leftValue)
|
||||||
|
|
||||||
|
// Verify the attribute has the correct key
|
||||||
|
assert.Equal(t, "error", attr.Key)
|
||||||
|
// Verify the attribute value is the error
|
||||||
|
assert.Equal(t, testErr, attr.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_Right(t *testing.T) {
|
||||||
|
// Test with Right (success) value
|
||||||
|
converter := ToSLogAttr[error, string]()
|
||||||
|
rightValue := Right[error]("success value")
|
||||||
|
|
||||||
|
attr := converter(rightValue)
|
||||||
|
|
||||||
|
// Verify the attribute has the correct key
|
||||||
|
assert.Equal(t, "value", attr.Key)
|
||||||
|
// Verify the attribute value is the success value
|
||||||
|
assert.Equal(t, "success value", attr.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_LeftWithCustomType(t *testing.T) {
|
||||||
|
// Test with custom error type
|
||||||
|
type CustomError struct {
|
||||||
|
Code int
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
converter := ToSLogAttr[CustomError, string]()
|
||||||
|
customErr := CustomError{Code: 404, Message: "not found"}
|
||||||
|
leftValue := Left[string](customErr)
|
||||||
|
|
||||||
|
attr := converter(leftValue)
|
||||||
|
|
||||||
|
assert.Equal(t, "error", attr.Key)
|
||||||
|
assert.Equal(t, customErr, attr.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_RightWithCustomType(t *testing.T) {
|
||||||
|
// Test with custom success type
|
||||||
|
type User struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
converter := ToSLogAttr[error, User]()
|
||||||
|
user := User{ID: 123, Name: "Alice"}
|
||||||
|
rightValue := Right[error](user)
|
||||||
|
|
||||||
|
attr := converter(rightValue)
|
||||||
|
|
||||||
|
assert.Equal(t, "value", attr.Key)
|
||||||
|
assert.Equal(t, user, attr.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_InPipeline(t *testing.T) {
|
||||||
|
// Test ToSLogAttr in a functional pipeline
|
||||||
|
converter := ToSLogAttr[error, int]()
|
||||||
|
|
||||||
|
// Test with successful pipeline
|
||||||
|
successResult := F.Pipe2(
|
||||||
|
Right[error](10),
|
||||||
|
Map[error](N.Mul(2)),
|
||||||
|
converter,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, "value", successResult.Key)
|
||||||
|
// slog.Any converts int to int64
|
||||||
|
assert.Equal(t, int64(20), successResult.Value.Any())
|
||||||
|
|
||||||
|
// Test with failed pipeline
|
||||||
|
testErr := errors.New("computation failed")
|
||||||
|
failureResult := F.Pipe2(
|
||||||
|
Left[int](testErr),
|
||||||
|
Map[error](N.Mul(2)),
|
||||||
|
converter,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, "error", failureResult.Key)
|
||||||
|
assert.Equal(t, testErr, failureResult.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_WithNilError(t *testing.T) {
|
||||||
|
// Test with nil error (edge case)
|
||||||
|
converter := ToSLogAttr[error, string]()
|
||||||
|
var nilErr error = nil
|
||||||
|
leftValue := Left[string](nilErr)
|
||||||
|
|
||||||
|
attr := converter(leftValue)
|
||||||
|
|
||||||
|
assert.Equal(t, "error", attr.Key)
|
||||||
|
assert.Nil(t, attr.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_WithZeroValue(t *testing.T) {
|
||||||
|
// Test with zero value of success type
|
||||||
|
converter := ToSLogAttr[error, int]()
|
||||||
|
rightValue := Right[error](0)
|
||||||
|
|
||||||
|
attr := converter(rightValue)
|
||||||
|
|
||||||
|
assert.Equal(t, "value", attr.Key)
|
||||||
|
// slog.Any converts int to int64
|
||||||
|
assert.Equal(t, int64(0), attr.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_WithEmptyString(t *testing.T) {
|
||||||
|
// Test with empty string as success value
|
||||||
|
converter := ToSLogAttr[error, string]()
|
||||||
|
rightValue := Right[error]("")
|
||||||
|
|
||||||
|
attr := converter(rightValue)
|
||||||
|
|
||||||
|
assert.Equal(t, "value", attr.Key)
|
||||||
|
assert.Equal(t, "", attr.Value.Any())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToSLogAttr_AttributeKind(t *testing.T) {
|
||||||
|
// Verify that the returned attribute has the correct Kind
|
||||||
|
converter := ToSLogAttr[error, string]()
|
||||||
|
|
||||||
|
leftAttr := converter(Left[string](errors.New("error")))
|
||||||
|
// Errors are stored as KindAny (which has value 0)
|
||||||
|
assert.Equal(t, slog.KindAny, leftAttr.Value.Kind())
|
||||||
|
|
||||||
|
rightAttr := converter(Right[error]("value"))
|
||||||
|
// Strings have KindString
|
||||||
|
assert.Equal(t, slog.KindString, rightAttr.Value.Kind())
|
||||||
|
}
|
||||||
|
|||||||
34
v2/either/rec.go
Normal file
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)
|
// increment := N.Add(1)
|
||||||
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
|
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
|
||||||
// // result(5) = double(increment(5)) = double(6) = 12
|
// // result(5) = double(increment(5)) = double(6) = 12
|
||||||
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
|
func MonadAp[A any](fab, fa Endomorphism[A]) Endomorphism[A] {
|
||||||
return MonadCompose(fab, fa)
|
return MonadCompose(fab, fa)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
|
|||||||
// // Compare with MonadCompose which executes RIGHT-TO-LEFT:
|
// // Compare with MonadCompose which executes RIGHT-TO-LEFT:
|
||||||
// composed := endomorphism.MonadCompose(increment, double)
|
// composed := endomorphism.MonadCompose(increment, double)
|
||||||
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
|
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
|
||||||
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
func MonadChain[A any](ma, f Endomorphism[A]) Endomorphism[A] {
|
||||||
return function.Flow2(ma, f)
|
return function.Flow2(ma, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
|||||||
// log := func(x int) int { fmt.Println(x); return x }
|
// log := func(x int) int { fmt.Println(x); return x }
|
||||||
// chained := endomorphism.MonadChainFirst(double, log)
|
// chained := endomorphism.MonadChainFirst(double, log)
|
||||||
// result := chained(5) // Prints 10, returns 10
|
// result := chained(5) // Prints 10, returns 10
|
||||||
func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
func MonadChainFirst[A any](ma, f Endomorphism[A]) Endomorphism[A] {
|
||||||
return func(a A) A {
|
return func(a A) A {
|
||||||
result := ma(a)
|
result := ma(a)
|
||||||
f(result) // Apply f for its effect
|
f(result) // Apply f for its effect
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ func TestFromStrictEquals(t *testing.T) {
|
|||||||
|
|
||||||
func TestFromEquals(t *testing.T) {
|
func TestFromEquals(t *testing.T) {
|
||||||
t.Run("case-insensitive string equality", func(t *testing.T) {
|
t.Run("case-insensitive string equality", func(t *testing.T) {
|
||||||
caseInsensitiveEq := FromEquals(func(a, b string) bool {
|
caseInsensitiveEq := FromEquals(strings.EqualFold)
|
||||||
return strings.EqualFold(a, b)
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.True(t, caseInsensitiveEq.Equals("hello", "HELLO"))
|
assert.True(t, caseInsensitiveEq.Equals("hello", "HELLO"))
|
||||||
assert.True(t, caseInsensitiveEq.Equals("Hello", "hello"))
|
assert.True(t, caseInsensitiveEq.Equals("Hello", "hello"))
|
||||||
@@ -243,9 +241,7 @@ func TestContramap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("case-insensitive name comparison", func(t *testing.T) {
|
t.Run("case-insensitive name comparison", func(t *testing.T) {
|
||||||
caseInsensitiveEq := FromEquals(func(a, b string) bool {
|
caseInsensitiveEq := FromEquals(strings.EqualFold)
|
||||||
return strings.EqualFold(a, b)
|
|
||||||
})
|
|
||||||
|
|
||||||
personEqByNameCI := Contramap(func(p Person) string {
|
personEqByNameCI := Contramap(func(p Person) string {
|
||||||
return p.Name
|
return p.Name
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ func Identity[A any](a A) A {
|
|||||||
//
|
//
|
||||||
// getMessage := Constant("Hello")
|
// getMessage := Constant("Hello")
|
||||||
// msg := getMessage() // "Hello"
|
// msg := getMessage() // "Hello"
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Constant[A any](a A) func() A {
|
func Constant[A any](a A) func() A {
|
||||||
|
//go:inline
|
||||||
return func() A {
|
return func() A {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,10 @@ func Constant[A any](a A) func() A {
|
|||||||
//
|
//
|
||||||
// defaultName := Constant1[int, string]("Unknown")
|
// defaultName := Constant1[int, string]("Unknown")
|
||||||
// name := defaultName(42) // "Unknown"
|
// name := defaultName(42) // "Unknown"
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Constant1[B, A any](a A) func(B) A {
|
func Constant1[B, A any](a A) func(B) A {
|
||||||
|
//go:inline
|
||||||
return func(_ B) A {
|
return func(_ B) A {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
@@ -107,7 +113,10 @@ func Constant1[B, A any](a A) func(B) A {
|
|||||||
//
|
//
|
||||||
// alwaysTrue := Constant2[int, string, bool](true)
|
// alwaysTrue := Constant2[int, string, bool](true)
|
||||||
// result := alwaysTrue(42, "test") // true
|
// result := alwaysTrue(42, "test") // true
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Constant2[B, C, A any](a A) func(B, C) A {
|
func Constant2[B, C, A any](a A) func(B, C) A {
|
||||||
|
//go:inline
|
||||||
return func(_ B, _ C) A {
|
return func(_ B, _ C) A {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
@@ -128,6 +137,8 @@ func Constant2[B, C, A any](a A) func(B, C) A {
|
|||||||
//
|
//
|
||||||
// value := 42
|
// value := 42
|
||||||
// IsNil(&value) // false
|
// IsNil(&value) // false
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func IsNil[A any](a *A) bool {
|
func IsNil[A any](a *A) bool {
|
||||||
return a == nil
|
return a == nil
|
||||||
}
|
}
|
||||||
@@ -149,6 +160,8 @@ func IsNil[A any](a *A) bool {
|
|||||||
//
|
//
|
||||||
// value := 42
|
// value := 42
|
||||||
// IsNonNil(&value) // true
|
// IsNonNil(&value) // true
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func IsNonNil[A any](a *A) bool {
|
func IsNonNil[A any](a *A) bool {
|
||||||
return a != nil
|
return a != nil
|
||||||
}
|
}
|
||||||
@@ -207,6 +220,8 @@ func Swap[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R {
|
|||||||
//
|
//
|
||||||
// result := First(42, "hello") // 42
|
// result := First(42, "hello") // 42
|
||||||
// result := First(true, 100) // true
|
// result := First(true, 100) // true
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func First[T1, T2 any](t1 T1, _ T2) T1 {
|
func First[T1, T2 any](t1 T1, _ T2) T1 {
|
||||||
return t1
|
return t1
|
||||||
}
|
}
|
||||||
@@ -231,6 +246,14 @@ func First[T1, T2 any](t1 T1, _ T2) T1 {
|
|||||||
//
|
//
|
||||||
// result := Second(42, "hello") // "hello"
|
// result := Second(42, "hello") // "hello"
|
||||||
// result := Second(true, 100) // 100
|
// result := Second(true, 100) // 100
|
||||||
|
//
|
||||||
|
//go:inline
|
||||||
func Second[T1, T2 any](_ T1, t2 T2) T2 {
|
func Second[T1, T2 any](_ T1, t2 T2) T2 {
|
||||||
return t2
|
return t2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zero returns the zero value of the given type.
|
||||||
|
func Zero[A comparable]() A {
|
||||||
|
var zero A
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,9 +117,13 @@ func Nullary2[F1 ~func() T1, F2 ~func(T1) T2, T1, T2 any](f1 F1, f2 F2) func() T
|
|||||||
|
|
||||||
// Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter.
|
// Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter.
|
||||||
// The inverse function is [Uncurry2]
|
// The inverse function is [Uncurry2]
|
||||||
|
//go:inline
|
||||||
func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 {
|
func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 {
|
||||||
|
//go:inline
|
||||||
return func(t0 T0) func(t1 T1) T2 {
|
return func(t0 T0) func(t1 T1) T2 {
|
||||||
|
//go:inline
|
||||||
return func(t1 T1) T2 {
|
return func(t1 T1) T2 {
|
||||||
|
//go:inline
|
||||||
return f(t0, t1)
|
return f(t0, t1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ package function
|
|||||||
// )
|
// )
|
||||||
// result := classify(5) // "positive"
|
// result := classify(5) // "positive"
|
||||||
// result2 := classify(-3) // "non-positive"
|
// result2 := classify(-3) // "non-positive"
|
||||||
func Ternary[A, B any](pred func(A) bool, onTrue func(A) B, onFalse func(A) B) func(A) B {
|
func Ternary[A, B any](pred func(A) bool, onTrue, onFalse func(A) B) func(A) B {
|
||||||
return func(a A) B {
|
return func(a A) B {
|
||||||
if pred(a) {
|
if pred(a) {
|
||||||
return onTrue(a)
|
return onTrue(a)
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ func (builder *Builder) GetTargetURL() Result[string] {
|
|||||||
parseQuery,
|
parseQuery,
|
||||||
result.Map(F.Flow2(
|
result.Map(F.Flow2(
|
||||||
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
|
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
|
||||||
(url.Values).Encode,
|
url.Values.Encode,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -351,13 +351,13 @@ func Header(name string) Lens[*Builder, Option[string]] {
|
|||||||
LZ.Map(delHeader(name)),
|
LZ.Map(delHeader(name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return L.MakeLens(get, func(b *Builder, value Option[string]) *Builder {
|
return L.MakeLensWithName(get, func(b *Builder, value Option[string]) *Builder {
|
||||||
cpy := b.clone()
|
cpy := b.clone()
|
||||||
return F.Pipe1(
|
return F.Pipe1(
|
||||||
value,
|
value,
|
||||||
O.Fold(del(cpy), set(cpy)),
|
O.Fold(del(cpy), set(cpy)),
|
||||||
)
|
)
|
||||||
})
|
}, fmt.Sprintf("HttpHeader[%s]", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithHeader creates a [Endomorphism] for a certain header
|
// WithHeader creates a [Endomorphism] for a certain header
|
||||||
|
|||||||
@@ -16,6 +16,18 @@
|
|||||||
/*
|
/*
|
||||||
Package identity implements the Identity monad, the simplest possible monad.
|
Package identity implements the Identity monad, the simplest possible monad.
|
||||||
|
|
||||||
|
# Fantasy Land Specification
|
||||||
|
|
||||||
|
This implementation corresponds to the Fantasy Land Identity type:
|
||||||
|
https://github.com/fantasyland/fantasy-land
|
||||||
|
|
||||||
|
Implemented Fantasy Land algebras:
|
||||||
|
- Functor: https://github.com/fantasyland/fantasy-land#functor
|
||||||
|
- Apply: https://github.com/fantasyland/fantasy-land#apply
|
||||||
|
- Applicative: https://github.com/fantasyland/fantasy-land#applicative
|
||||||
|
- Chain: https://github.com/fantasyland/fantasy-land#chain
|
||||||
|
- Monad: https://github.com/fantasyland/fantasy-land#monad
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
|
|
||||||
The Identity monad is a trivial monad that simply wraps a value without adding
|
The Identity monad is a trivial monad that simply wraps a value without adding
|
||||||
|
|||||||
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()
|
tmpDir := t.TempDir()
|
||||||
newDir := filepath.Join(tmpDir, "testdir")
|
newDir := filepath.Join(tmpDir, "testdir")
|
||||||
|
|
||||||
result := Mkdir(newDir, 0755)
|
result := Mkdir(newDir, 0o755)
|
||||||
path, err := result()
|
path, err := result()
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -43,14 +43,14 @@ func TestMkdir(t *testing.T) {
|
|||||||
t.Run("mkdir with existing directory", func(t *testing.T) {
|
t.Run("mkdir with existing directory", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
result := Mkdir(tmpDir, 0755)
|
result := Mkdir(tmpDir, 0o755)
|
||||||
_, err := result()
|
_, err := result()
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("mkdir with parent directory not existing", func(t *testing.T) {
|
t.Run("mkdir with parent directory not existing", func(t *testing.T) {
|
||||||
result := Mkdir("/non/existent/parent/child", 0755)
|
result := Mkdir("/non/existent/parent/child", 0o755)
|
||||||
_, err := result()
|
_, err := result()
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -62,7 +62,7 @@ func TestMkdirAll(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
|
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
|
||||||
|
|
||||||
result := MkdirAll(nestedDir, 0755)
|
result := MkdirAll(nestedDir, 0o755)
|
||||||
path, err := result()
|
path, err := result()
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -88,7 +88,7 @@ func TestMkdirAll(t *testing.T) {
|
|||||||
t.Run("mkdirall with existing directory", func(t *testing.T) {
|
t.Run("mkdirall with existing directory", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
result := MkdirAll(tmpDir, 0755)
|
result := MkdirAll(tmpDir, 0o755)
|
||||||
path, err := result()
|
path, err := result()
|
||||||
|
|
||||||
// MkdirAll should succeed even if directory exists
|
// MkdirAll should succeed even if directory exists
|
||||||
@@ -100,7 +100,7 @@ func TestMkdirAll(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
newDir := filepath.Join(tmpDir, "single")
|
newDir := filepath.Join(tmpDir, "single")
|
||||||
|
|
||||||
result := MkdirAll(newDir, 0755)
|
result := MkdirAll(newDir, 0o755)
|
||||||
path, err := result()
|
path, err := result()
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -116,11 +116,11 @@ func TestMkdirAll(t *testing.T) {
|
|||||||
filePath := filepath.Join(tmpDir, "file.txt")
|
filePath := filepath.Join(tmpDir, "file.txt")
|
||||||
|
|
||||||
// Create a file
|
// Create a file
|
||||||
err := os.WriteFile(filePath, []byte("content"), 0644)
|
err := os.WriteFile(filePath, []byte("content"), 0o644)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Try to create a directory where file exists
|
// Try to create a directory where file exists
|
||||||
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755)
|
result := MkdirAll(filepath.Join(filePath, "subdir"), 0o755)
|
||||||
_, err = result()
|
_, err = result()
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestOpen(t *testing.T) {
|
|||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
// Write some content
|
// Write some content
|
||||||
err = os.WriteFile(tmpPath, []byte("test content"), 0644)
|
err = os.WriteFile(tmpPath, []byte("test content"), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Test Open
|
// Test Open
|
||||||
@@ -127,7 +127,7 @@ func TestWriteFile(t *testing.T) {
|
|||||||
testPath := filepath.Join(tmpDir, "write-test.txt")
|
testPath := filepath.Join(tmpDir, "write-test.txt")
|
||||||
testData := []byte("test data")
|
testData := []byte("test data")
|
||||||
|
|
||||||
result := WriteFile(testPath, 0644)(testData)
|
result := WriteFile(testPath, 0o644)(testData)
|
||||||
returnedData, err := result()
|
returnedData, err := result()
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -141,7 +141,7 @@ func TestWriteFile(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("write to invalid path", func(t *testing.T) {
|
t.Run("write to invalid path", func(t *testing.T) {
|
||||||
testData := []byte("test data")
|
testData := []byte("test data")
|
||||||
result := WriteFile("/non/existent/dir/file.txt", 0644)(testData)
|
result := WriteFile("/non/existent/dir/file.txt", 0o644)(testData)
|
||||||
_, err := result()
|
_, err := result()
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -155,12 +155,12 @@ func TestWriteFile(t *testing.T) {
|
|||||||
defer os.Remove(tmpPath)
|
defer os.Remove(tmpPath)
|
||||||
|
|
||||||
// Write initial content
|
// Write initial content
|
||||||
err = os.WriteFile(tmpPath, []byte("initial"), 0644)
|
err = os.WriteFile(tmpPath, []byte("initial"), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Overwrite with new content
|
// Overwrite with new content
|
||||||
newData := []byte("overwritten")
|
newData := []byte("overwritten")
|
||||||
result := WriteFile(tmpPath, 0644)(newData)
|
result := WriteFile(tmpPath, 0o644)(newData)
|
||||||
returnedData, err := result()
|
returnedData, err := result()
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -212,7 +212,7 @@ func TestClose(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify file is closed by attempting to write
|
// Verify file is closed by attempting to write
|
||||||
_, writeErr := tmpFile.Write([]byte("test"))
|
_, writeErr := tmpFile.WriteString("test")
|
||||||
assert.Error(t, writeErr)
|
assert.Error(t, writeErr)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func TestReadAll(t *testing.T) {
|
|||||||
largeContent[i] = byte('A' + (i % 26))
|
largeContent[i] = byte('A' + (i % 26))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := os.WriteFile(testPath, largeContent, 0644)
|
err := os.WriteFile(testPath, largeContent, 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result := ReadAll(Open(testPath))
|
result := ReadAll(Open(testPath))
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func TestWriteAll(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify file is closed by trying to write to it
|
// Verify file is closed by trying to write to it
|
||||||
_, writeErr := capturedFile.Write([]byte("more"))
|
_, writeErr := capturedFile.WriteString("more")
|
||||||
assert.Error(t, writeErr)
|
assert.Error(t, writeErr)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ func TestWrite(t *testing.T) {
|
|||||||
|
|
||||||
useFile := func(f *os.File) IOResult[string] {
|
useFile := func(f *os.File) IOResult[string] {
|
||||||
return func() (string, error) {
|
return func() (string, error) {
|
||||||
_, err := f.Write([]byte("data"))
|
_, err := f.WriteString("data")
|
||||||
return "success", err
|
return "success", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ func TestWrite(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify file is closed
|
// Verify file is closed
|
||||||
_, writeErr := capturedFile.Write([]byte("more"))
|
_, writeErr := capturedFile.WriteString("more")
|
||||||
assert.Error(t, writeErr)
|
assert.Error(t, writeErr)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ func TestWrite(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// Verify file is still closed even on error
|
// Verify file is still closed even on error
|
||||||
_, writeErr := capturedFile.Write([]byte("more"))
|
_, writeErr := capturedFile.WriteString("more")
|
||||||
assert.Error(t, writeErr)
|
assert.Error(t, writeErr)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func Requester(builder *R.Builder) IOEH.Requester {
|
|||||||
|
|
||||||
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
|
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
|
||||||
return func() (*http.Request, error) {
|
return func() (*http.Request, error) {
|
||||||
req, err := http.NewRequest(method, url, nil)
|
req, err := http.NewRequest(method, url, http.NoBody)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
F "github.com/IBM/fp-go/v2/function"
|
F "github.com/IBM/fp-go/v2/function"
|
||||||
"github.com/IBM/fp-go/v2/internal/utils"
|
"github.com/IBM/fp-go/v2/internal/utils"
|
||||||
"github.com/IBM/fp-go/v2/io"
|
"github.com/IBM/fp-go/v2/io"
|
||||||
|
S "github.com/IBM/fp-go/v2/string"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ func TestChainWithIO(t *testing.T) {
|
|||||||
Of("test"),
|
Of("test"),
|
||||||
ChainIOK(func(s string) IO[bool] {
|
ChainIOK(func(s string) IO[bool] {
|
||||||
return func() bool {
|
return func() bool {
|
||||||
return len(s) > 0
|
return S.IsNonEmpty(s)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user