mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-19 23:42:05 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6c6ea804f | ||
|
|
31ff98901e | ||
|
|
255cf4353c | ||
|
|
4dfc1b5a44 | ||
|
|
20398e67a9 | ||
|
|
fceda15701 | ||
|
|
4ebfcadabe | ||
|
|
acb601fc01 | ||
|
|
d17663f016 | ||
|
|
829365fc24 | ||
|
|
64b5660b4e |
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
|
||||
19
v2/README.md
19
v2/README.md
@@ -61,6 +61,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -145,6 +146,8 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ Breaking Changes
|
||||
|
||||
### From V1 to V2
|
||||
|
||||
#### 1. Generic Type Aliases
|
||||
@@ -449,17 +452,27 @@ func process() IOResult[string] {
|
||||
|
||||
### Core Modules
|
||||
|
||||
#### Standard Packages (Struct-based)
|
||||
- **Option** - Represent optional values without nil
|
||||
- **Either** - Type-safe error handling with left/right values
|
||||
- **Result** - Simplified Either with error as left type
|
||||
- **Result** - Simplified Either with error as left type (recommended for error handling)
|
||||
- **IO** - Lazy evaluation and side effect management
|
||||
- **IOEither** - Combine IO with error handling
|
||||
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
|
||||
- **Reader** - Dependency injection pattern
|
||||
- **ReaderIOEither** - Combine Reader, IO, and Either for complex workflows
|
||||
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
|
||||
#### Idiomatic Packages (Tuple-based, High Performance)
|
||||
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples
|
||||
- **idiomatic/result** - Result monad using native Go `(value, error)` tuples
|
||||
- **idiomatic/ioresult** - IOResult monad using `func() (value, error)` for IO operations
|
||||
- **idiomatic/readerresult** - Reader monad combined with Result pattern
|
||||
- **idiomatic/readerioresult** - Reader monad combined with IOResult pattern
|
||||
|
||||
The idiomatic packages offer 2-10x performance improvements and zero allocations by using Go's native tuple patterns instead of struct wrappers. Use them for performance-critical code or when you prefer Go's native error handling style.
|
||||
|
||||
## 🤔 Should I Migrate?
|
||||
|
||||
**Migrate to V2 if:**
|
||||
|
||||
@@ -536,3 +536,89 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
func Prepend[A any](head A) Operator[A, A] {
|
||||
return G.Prepend[Operator[A, A]](head)
|
||||
}
|
||||
|
||||
// Reverse returns a new slice with elements in reverse order.
|
||||
// This function creates a new slice containing all elements from the input slice
|
||||
// in reverse order, without modifying the original slice.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the slice
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input slice to reverse
|
||||
//
|
||||
// Returns:
|
||||
// - A new slice with elements in reverse order
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new slice with the same length as the input
|
||||
// - Copies elements from the input slice in reverse order
|
||||
// - Does not modify the original slice
|
||||
// - Returns an empty slice if the input is empty
|
||||
// - Returns a single-element slice unchanged if input has one element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// reversed := array.Reverse(numbers)
|
||||
// // reversed: []int{5, 4, 3, 2, 1}
|
||||
// // numbers: []int{1, 2, 3, 4, 5} (unchanged)
|
||||
//
|
||||
// Example with strings:
|
||||
//
|
||||
// words := []string{"hello", "world", "foo", "bar"}
|
||||
// reversed := array.Reverse(words)
|
||||
// // reversed: []string{"bar", "foo", "world", "hello"}
|
||||
//
|
||||
// Example with empty slice:
|
||||
//
|
||||
// empty := []int{}
|
||||
// reversed := array.Reverse(empty)
|
||||
// // reversed: []int{} (empty slice)
|
||||
//
|
||||
// Example with single element:
|
||||
//
|
||||
// single := []string{"only"}
|
||||
// reversed := array.Reverse(single)
|
||||
// // reversed: []string{"only"}
|
||||
//
|
||||
// Use cases:
|
||||
// - Reversing the order of elements for display or processing
|
||||
// - Implementing stack-like behavior (LIFO)
|
||||
// - Processing data in reverse chronological order
|
||||
// - Reversing transformation pipelines
|
||||
// - Creating palindrome checks
|
||||
// - Implementing undo/redo functionality
|
||||
//
|
||||
// Example with processing in reverse:
|
||||
//
|
||||
// events := []string{"start", "middle", "end"}
|
||||
// reversed := array.Reverse(events)
|
||||
// // Process events in reverse order
|
||||
// for _, event := range reversed {
|
||||
// fmt.Println(event) // Prints: "end", "middle", "start"
|
||||
// }
|
||||
//
|
||||
// Example with functional composition:
|
||||
//
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// result := F.Pipe2(
|
||||
// numbers,
|
||||
// array.Map(N.Mul(2)),
|
||||
// array.Reverse,
|
||||
// )
|
||||
// // result: []int{10, 8, 6, 4, 2}
|
||||
//
|
||||
// Performance:
|
||||
// - Time complexity: O(n) where n is the length of the slice
|
||||
// - Space complexity: O(n) for the new slice
|
||||
// - Does not allocate if the input slice is empty
|
||||
//
|
||||
// Note: This function is immutable - it does not modify the original slice.
|
||||
// If you need to reverse a slice in-place, consider using a different approach
|
||||
// or modifying the slice directly.
|
||||
//
|
||||
//go:inline
|
||||
func Reverse[A any](as []A) []A {
|
||||
return G.Reverse(as)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
@@ -214,3 +215,262 @@ func ExampleFoldMap() {
|
||||
// Output: ABC
|
||||
|
||||
}
|
||||
|
||||
// TestReverse tests the Reverse function
|
||||
func TestReverse(t *testing.T) {
|
||||
t.Run("Reverse integers", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Reverse(input)
|
||||
expected := []int{5, 4, 3, 2, 1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse strings", func(t *testing.T) {
|
||||
input := []string{"hello", "world", "foo", "bar"}
|
||||
result := Reverse(input)
|
||||
expected := []string{"bar", "foo", "world", "hello"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse empty slice", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := Reverse(input)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse single element", func(t *testing.T) {
|
||||
input := []string{"only"}
|
||||
result := Reverse(input)
|
||||
assert.Equal(t, []string{"only"}, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse two elements", func(t *testing.T) {
|
||||
input := []int{1, 2}
|
||||
result := Reverse(input)
|
||||
assert.Equal(t, []int{2, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("Does not modify original slice", func(t *testing.T) {
|
||||
original := []int{1, 2, 3, 4, 5}
|
||||
originalCopy := []int{1, 2, 3, 4, 5}
|
||||
_ = Reverse(original)
|
||||
assert.Equal(t, originalCopy, original)
|
||||
})
|
||||
|
||||
t.Run("Reverse with floats", func(t *testing.T) {
|
||||
input := []float64{1.1, 2.2, 3.3}
|
||||
result := Reverse(input)
|
||||
expected := []float64{3.3, 2.2, 1.1}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse with structs", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
input := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Charlie", 35},
|
||||
}
|
||||
result := Reverse(input)
|
||||
expected := []Person{
|
||||
{"Charlie", 35},
|
||||
{"Bob", 25},
|
||||
{"Alice", 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse with pointers", func(t *testing.T) {
|
||||
a, b, c := 1, 2, 3
|
||||
input := []*int{&a, &b, &c}
|
||||
result := Reverse(input)
|
||||
assert.Equal(t, []*int{&c, &b, &a}, result)
|
||||
})
|
||||
|
||||
t.Run("Double reverse returns original order", func(t *testing.T) {
|
||||
original := []int{1, 2, 3, 4, 5}
|
||||
reversed := Reverse(original)
|
||||
doubleReversed := Reverse(reversed)
|
||||
assert.Equal(t, original, doubleReversed)
|
||||
})
|
||||
|
||||
t.Run("Reverse with large slice", func(t *testing.T) {
|
||||
input := MakeBy(1000, F.Identity[int])
|
||||
result := Reverse(input)
|
||||
|
||||
// Check first and last elements
|
||||
assert.Equal(t, 999, result[0])
|
||||
assert.Equal(t, 0, result[999])
|
||||
|
||||
// Check length
|
||||
assert.Equal(t, 1000, len(result))
|
||||
})
|
||||
|
||||
t.Run("Reverse palindrome", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 2, 1}
|
||||
result := Reverse(input)
|
||||
assert.Equal(t, input, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseComposition tests Reverse with other array operations
|
||||
func TestReverseComposition(t *testing.T) {
|
||||
t.Run("Reverse after Map", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Map(N.Mul(2)),
|
||||
Reverse[int],
|
||||
)
|
||||
expected := []int{10, 8, 6, 4, 2}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Map after Reverse", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Reverse[int],
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
expected := []int{10, 8, 6, 4, 2}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse with Filter", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5, 6}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Filter(func(n int) bool { return n%2 == 0 }),
|
||||
Reverse[int],
|
||||
)
|
||||
expected := []int{6, 4, 2}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("Reverse with Reduce", func(t *testing.T) {
|
||||
input := []string{"a", "b", "c"}
|
||||
reversed := Reverse(input)
|
||||
result := Reduce(func(acc, val string) string {
|
||||
return acc + val
|
||||
}, "")(reversed)
|
||||
assert.Equal(t, "cba", result)
|
||||
})
|
||||
|
||||
t.Run("Reverse with Flatten", func(t *testing.T) {
|
||||
input := [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
Reverse[[]int],
|
||||
Flatten[int],
|
||||
)
|
||||
expected := []int{5, 6, 3, 4, 1, 2}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseUseCases demonstrates practical use cases for Reverse
|
||||
func TestReverseUseCases(t *testing.T) {
|
||||
t.Run("Process events in reverse chronological order", func(t *testing.T) {
|
||||
events := []string{"2024-01-01", "2024-01-02", "2024-01-03"}
|
||||
reversed := Reverse(events)
|
||||
|
||||
// Most recent first
|
||||
assert.Equal(t, "2024-01-03", reversed[0])
|
||||
assert.Equal(t, "2024-01-01", reversed[2])
|
||||
})
|
||||
|
||||
t.Run("Implement stack behavior (LIFO)", func(t *testing.T) {
|
||||
stack := []int{1, 2, 3, 4, 5}
|
||||
reversed := Reverse(stack)
|
||||
|
||||
// Pop from reversed (LIFO)
|
||||
assert.Equal(t, 5, reversed[0])
|
||||
assert.Equal(t, 4, reversed[1])
|
||||
})
|
||||
|
||||
t.Run("Reverse string characters", func(t *testing.T) {
|
||||
chars := []rune("hello")
|
||||
reversed := Reverse(chars)
|
||||
result := string(reversed)
|
||||
assert.Equal(t, "olleh", result)
|
||||
})
|
||||
|
||||
t.Run("Check palindrome", func(t *testing.T) {
|
||||
word := []rune("racecar")
|
||||
reversed := Reverse(word)
|
||||
assert.Equal(t, word, reversed)
|
||||
|
||||
notPalindrome := []rune("hello")
|
||||
reversedNot := Reverse(notPalindrome)
|
||||
assert.NotEqual(t, notPalindrome, reversedNot)
|
||||
})
|
||||
|
||||
t.Run("Reverse transformation pipeline", func(t *testing.T) {
|
||||
// Apply transformations in reverse order
|
||||
numbers := []int{1, 2, 3}
|
||||
|
||||
// Normal: add 10, then multiply by 2
|
||||
normal := F.Pipe2(
|
||||
numbers,
|
||||
Map(N.Add(10)),
|
||||
Map(N.Mul(2)),
|
||||
)
|
||||
|
||||
// Reversed order of operations
|
||||
reversed := F.Pipe2(
|
||||
numbers,
|
||||
Map(N.Mul(2)),
|
||||
Map(N.Add(10)),
|
||||
)
|
||||
|
||||
assert.NotEqual(t, normal, reversed)
|
||||
assert.Equal(t, []int{22, 24, 26}, normal)
|
||||
assert.Equal(t, []int{12, 14, 16}, reversed)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseProperties tests mathematical properties of Reverse
|
||||
func TestReverseProperties(t *testing.T) {
|
||||
t.Run("Involution property: Reverse(Reverse(x)) == x", func(t *testing.T) {
|
||||
testCases := [][]int{
|
||||
{1, 2, 3, 4, 5},
|
||||
{1},
|
||||
{},
|
||||
{1, 2},
|
||||
{5, 4, 3, 2, 1},
|
||||
}
|
||||
|
||||
for _, original := range testCases {
|
||||
result := Reverse(Reverse(original))
|
||||
assert.Equal(t, original, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Length preservation: len(Reverse(x)) == len(x)", func(t *testing.T) {
|
||||
testCases := [][]int{
|
||||
{1, 2, 3, 4, 5},
|
||||
{1},
|
||||
{},
|
||||
MakeBy(100, F.Identity[int]),
|
||||
}
|
||||
|
||||
for _, input := range testCases {
|
||||
result := Reverse(input)
|
||||
assert.Equal(t, len(input), len(result))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("First element becomes last", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
result := Reverse(input)
|
||||
|
||||
if len(input) > 0 {
|
||||
assert.Equal(t, input[0], result[len(result)-1])
|
||||
assert.Equal(t, input[len(input)-1], result[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
E "github.com/IBM/fp-go/v2/eq"
|
||||
)
|
||||
|
||||
func equals[T any](left []T, right []T, eq func(T, T) bool) bool {
|
||||
func equals[T any](left, right []T, eq func(T, T) bool) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -140,22 +140,27 @@ func Empty[GA ~[]A, A any]() GA {
|
||||
return array.Empty[GA]()
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func UpsertAt[GA ~[]A, A any](a A) func(GA) GA {
|
||||
return array.UpsertAt[GA](a)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB {
|
||||
return array.MonadMap[GA, GB](as, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Map[GA ~[]A, GB ~[]B, A, B any](f func(a A) B) func(GA) GB {
|
||||
return array.Map[GA, GB](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(int, A) B) GB {
|
||||
return array.MonadMapWithIndex[GA, GB](as, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
|
||||
return F.Bind2nd(MonadMapWithIndex[GA, GB, A, B], f)
|
||||
}
|
||||
@@ -297,7 +302,7 @@ func MatchLeft[AS ~[]A, A, B any](onEmpty func() B, onNonEmpty func(A, AS) B) fu
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Slice[AS ~[]A, A any](start int, end int) func(AS) AS {
|
||||
func Slice[AS ~[]A, A any](start, end int) func(AS) AS {
|
||||
return array.Slice[AS](start, end)
|
||||
}
|
||||
|
||||
@@ -361,6 +366,12 @@ func Flap[FAB ~func(A) B, GFAB ~[]FAB, GB ~[]B, A, B any](a A) func(GFAB) GB {
|
||||
return FC.Flap(Map[GFAB, GB], a)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
|
||||
return array.Prepend[ENDO](head)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
return array.Reverse(as)
|
||||
}
|
||||
|
||||
@@ -18,14 +18,11 @@ package nonempty
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/array/generic"
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// NonEmptyArray represents an array with at least one element
|
||||
type NonEmptyArray[A any] []A
|
||||
|
||||
// Of constructs a single element array
|
||||
func Of[A any](first A) NonEmptyArray[A] {
|
||||
return G.Of[NonEmptyArray[A]](first)
|
||||
@@ -44,20 +41,24 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
|
||||
return buffer
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
|
||||
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
|
||||
}
|
||||
|
||||
func Map[A, B any](f func(a A) B) func(NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
return F.Bind2nd(MonadMap[A, B], f)
|
||||
//go:inline
|
||||
func Map[A, B any](f func(a A) B) Operator[A, B] {
|
||||
return G.Map[NonEmptyArray[A], NonEmptyArray[B]](f)
|
||||
}
|
||||
|
||||
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
|
||||
@@ -72,22 +73,27 @@ func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Tail[A any](as NonEmptyArray[A]) []A {
|
||||
return as[1:]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Head[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func First[A any](as NonEmptyArray[A]) A {
|
||||
return as[0]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Last[A any](as NonEmptyArray[A]) A {
|
||||
return as[len(as)-1]
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Size[A any](as NonEmptyArray[A]) int {
|
||||
return G.Size(as)
|
||||
}
|
||||
@@ -96,11 +102,11 @@ func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
|
||||
return G.Flatten(mma)
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](fa NonEmptyArray[A], f func(a A) NonEmptyArray[B]) NonEmptyArray[B] {
|
||||
func MonadChain[A, B any](fa NonEmptyArray[A], f Kleisli[A, B]) NonEmptyArray[B] {
|
||||
return G.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
func Chain[A, B any](f func(A) NonEmptyArray[B]) func(NonEmptyArray[A]) NonEmptyArray[B] {
|
||||
func Chain[A, B any](f func(A) NonEmptyArray[B]) Operator[A, B] {
|
||||
return G.Chain[NonEmptyArray[A]](f)
|
||||
}
|
||||
|
||||
@@ -134,3 +140,89 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
|
||||
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
|
||||
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
|
||||
}
|
||||
|
||||
// ToNonEmptyArray attempts to convert a regular slice into a NonEmptyArray.
|
||||
// This function provides a safe way to create a NonEmptyArray from a slice that might be empty,
|
||||
// returning an Option type to handle the case where the input slice is empty.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type of the array
|
||||
//
|
||||
// Parameters:
|
||||
// - as: A regular slice that may or may not be empty
|
||||
//
|
||||
// Returns:
|
||||
// - Option[NonEmptyArray[A]]: Some(NonEmptyArray) if the input slice is non-empty, None if empty
|
||||
//
|
||||
// Behavior:
|
||||
// - If the input slice is empty, returns None
|
||||
// - If the input slice has at least one element, wraps it in Some and returns it as a NonEmptyArray
|
||||
// - The conversion is a type cast, so no data is copied
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Convert non-empty slice
|
||||
// numbers := []int{1, 2, 3}
|
||||
// result := ToNonEmptyArray(numbers) // Some(NonEmptyArray[1, 2, 3])
|
||||
//
|
||||
// // Convert empty slice
|
||||
// empty := []int{}
|
||||
// result := ToNonEmptyArray(empty) // None
|
||||
//
|
||||
// // Use with Option methods
|
||||
// numbers := []int{1, 2, 3}
|
||||
// result := ToNonEmptyArray(numbers)
|
||||
// if O.IsSome(result) {
|
||||
// nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||
// head := Head(nea) // 1
|
||||
// }
|
||||
//
|
||||
// Use cases:
|
||||
// - Safely converting user input or external data to NonEmptyArray
|
||||
// - Validating that a collection has at least one element before processing
|
||||
// - Converting results from functions that return regular slices
|
||||
// - Ensuring type safety when working with collections that must not be empty
|
||||
//
|
||||
// Example with validation:
|
||||
//
|
||||
// func processItems(items []string) Option[string] {
|
||||
// return F.Pipe2(
|
||||
// items,
|
||||
// ToNonEmptyArray[string],
|
||||
// O.Map(func(nea NonEmptyArray[string]) string {
|
||||
// return Head(nea) // Safe to get head since we know it's non-empty
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// Example with error handling:
|
||||
//
|
||||
// items := []int{1, 2, 3}
|
||||
// result := ToNonEmptyArray(items)
|
||||
// switch {
|
||||
// case O.IsSome(result):
|
||||
// nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||
// fmt.Println("First item:", Head(nea))
|
||||
// case O.IsNone(result):
|
||||
// fmt.Println("Array is empty")
|
||||
// }
|
||||
//
|
||||
// Example with chaining:
|
||||
//
|
||||
// // Process only if non-empty
|
||||
// result := F.Pipe3(
|
||||
// []int{1, 2, 3},
|
||||
// ToNonEmptyArray[int],
|
||||
// O.Map(Map(func(x int) int { return x * 2 })),
|
||||
// O.Map(Head[int]),
|
||||
// ) // Some(2)
|
||||
//
|
||||
// Note: This function is particularly useful when working with APIs or functions
|
||||
// that return regular slices but you need the type-level guarantee that the
|
||||
// collection is non-empty for subsequent operations.
|
||||
func ToNonEmptyArray[A any](as []A) Option[NonEmptyArray[A]] {
|
||||
if G.IsEmpty(as) {
|
||||
return option.None[NonEmptyArray[A]]()
|
||||
}
|
||||
return option.Some(NonEmptyArray[A](as))
|
||||
}
|
||||
|
||||
370
v2/array/nonempty/array_test.go
Normal file
370
v2/array/nonempty/array_test.go
Normal file
@@ -0,0 +1,370 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package nonempty
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestToNonEmptyArray tests the ToNonEmptyArray function
|
||||
func TestToNonEmptyArray(t *testing.T) {
|
||||
t.Run("Convert non-empty slice of integers", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||
assert.Equal(t, 3, Size(nea))
|
||||
assert.Equal(t, 1, Head(nea))
|
||||
assert.Equal(t, 3, Last(nea))
|
||||
})
|
||||
|
||||
t.Run("Convert empty slice returns None", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Convert single element slice", func(t *testing.T) {
|
||||
input := []string{"hello"}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From("")))(result)
|
||||
assert.Equal(t, 1, Size(nea))
|
||||
assert.Equal(t, "hello", Head(nea))
|
||||
})
|
||||
|
||||
t.Run("Convert non-empty slice of strings", func(t *testing.T) {
|
||||
input := []string{"a", "b", "c", "d"}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From("")))(result)
|
||||
assert.Equal(t, 4, Size(nea))
|
||||
assert.Equal(t, "a", Head(nea))
|
||||
assert.Equal(t, "d", Last(nea))
|
||||
})
|
||||
|
||||
t.Run("Convert nil slice returns None", func(t *testing.T) {
|
||||
var input []int
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Convert slice with struct elements", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
input := []Person{
|
||||
{Name: "Alice", Age: 30},
|
||||
{Name: "Bob", Age: 25},
|
||||
}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(Person{})))(result)
|
||||
assert.Equal(t, 2, Size(nea))
|
||||
assert.Equal(t, "Alice", Head(nea).Name)
|
||||
})
|
||||
|
||||
t.Run("Convert slice with pointer elements", func(t *testing.T) {
|
||||
val1, val2 := 10, 20
|
||||
input := []*int{&val1, &val2}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From[*int](nil)))(result)
|
||||
assert.Equal(t, 2, Size(nea))
|
||||
assert.Equal(t, 10, *Head(nea))
|
||||
})
|
||||
|
||||
t.Run("Convert large slice", func(t *testing.T) {
|
||||
input := make([]int, 1000)
|
||||
for i := range input {
|
||||
input[i] = i
|
||||
}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||
assert.Equal(t, 1000, Size(nea))
|
||||
assert.Equal(t, 0, Head(nea))
|
||||
assert.Equal(t, 999, Last(nea))
|
||||
})
|
||||
|
||||
t.Run("Convert slice with float64 elements", func(t *testing.T) {
|
||||
input := []float64{1.5, 2.5, 3.5}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(0.0)))(result)
|
||||
assert.Equal(t, 3, Size(nea))
|
||||
assert.Equal(t, 1.5, Head(nea))
|
||||
})
|
||||
|
||||
t.Run("Convert slice with boolean elements", func(t *testing.T) {
|
||||
input := []bool{true, false, true}
|
||||
result := ToNonEmptyArray(input)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(false)))(result)
|
||||
assert.Equal(t, 3, Size(nea))
|
||||
assert.True(t, Head(nea))
|
||||
})
|
||||
}
|
||||
|
||||
// TestToNonEmptyArrayWithOption tests ToNonEmptyArray with Option operations
|
||||
func TestToNonEmptyArrayWithOption(t *testing.T) {
|
||||
t.Run("Chain with Map to process elements", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
ToNonEmptyArray[int],
|
||||
O.Map(Map(func(x int) int { return x * 2 })),
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||
assert.Equal(t, 2, Head(nea))
|
||||
assert.Equal(t, 6, Last(nea))
|
||||
})
|
||||
|
||||
t.Run("Chain with Map to get head", func(t *testing.T) {
|
||||
input := []string{"first", "second", "third"}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
ToNonEmptyArray[string],
|
||||
O.Map(Head[string]),
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(""))(result)
|
||||
assert.Equal(t, "first", value)
|
||||
})
|
||||
|
||||
t.Run("GetOrElse with default value for empty slice", func(t *testing.T) {
|
||||
input := []int{}
|
||||
defaultValue := From(42)
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
ToNonEmptyArray[int],
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 1, Size(result))
|
||||
assert.Equal(t, 42, Head(result))
|
||||
})
|
||||
|
||||
t.Run("GetOrElse with default value for non-empty slice", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
defaultValue := From(42)
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
ToNonEmptyArray[int],
|
||||
O.GetOrElse(F.Constant(defaultValue)),
|
||||
)
|
||||
|
||||
assert.Equal(t, 3, Size(result))
|
||||
assert.Equal(t, 1, Head(result))
|
||||
})
|
||||
|
||||
t.Run("Fold with Some case", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
ToNonEmptyArray[int],
|
||||
O.Fold(
|
||||
F.Constant(0),
|
||||
func(nea NonEmptyArray[int]) int { return Head(nea) },
|
||||
),
|
||||
)
|
||||
|
||||
assert.Equal(t, 1, result)
|
||||
})
|
||||
|
||||
t.Run("Fold with None case", func(t *testing.T) {
|
||||
input := []int{}
|
||||
result := F.Pipe2(
|
||||
input,
|
||||
ToNonEmptyArray[int],
|
||||
O.Fold(
|
||||
F.Constant(-1),
|
||||
func(nea NonEmptyArray[int]) int { return Head(nea) },
|
||||
),
|
||||
)
|
||||
|
||||
assert.Equal(t, -1, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToNonEmptyArrayComposition tests composing ToNonEmptyArray with other operations
|
||||
func TestToNonEmptyArrayComposition(t *testing.T) {
|
||||
t.Run("Compose with filter-like operation", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
// Filter even numbers then convert
|
||||
filtered := []int{}
|
||||
for _, x := range input {
|
||||
if x%2 == 0 {
|
||||
filtered = append(filtered, x)
|
||||
}
|
||||
}
|
||||
result := ToNonEmptyArray(filtered)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||
assert.Equal(t, 2, Size(nea))
|
||||
assert.Equal(t, 2, Head(nea))
|
||||
})
|
||||
|
||||
t.Run("Compose with map operation before conversion", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
// Map then convert
|
||||
mapped := make([]int, len(input))
|
||||
for i, x := range input {
|
||||
mapped[i] = x * 10
|
||||
}
|
||||
result := ToNonEmptyArray(mapped)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
nea := O.GetOrElse(F.Constant(From(0)))(result)
|
||||
assert.Equal(t, 10, Head(nea))
|
||||
assert.Equal(t, 30, Last(nea))
|
||||
})
|
||||
|
||||
t.Run("Chain multiple Option operations", func(t *testing.T) {
|
||||
input := []int{5, 10, 15}
|
||||
result := F.Pipe3(
|
||||
input,
|
||||
ToNonEmptyArray[int],
|
||||
O.Map(Map(func(x int) int { return x / 5 })),
|
||||
O.Map(func(nea NonEmptyArray[int]) int {
|
||||
return Head(nea) + Last(nea)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
value := O.GetOrElse(F.Constant(0))(result)
|
||||
assert.Equal(t, 4, value) // 1 + 3
|
||||
})
|
||||
}
|
||||
|
||||
// TestToNonEmptyArrayUseCases demonstrates practical use cases
|
||||
func TestToNonEmptyArrayUseCases(t *testing.T) {
|
||||
t.Run("Validate user input has at least one item", func(t *testing.T) {
|
||||
// Simulate user input
|
||||
userInput := []string{"item1", "item2"}
|
||||
|
||||
result := ToNonEmptyArray(userInput)
|
||||
if O.IsSome(result) {
|
||||
nea := O.GetOrElse(F.Constant(From("")))(result)
|
||||
firstItem := Head(nea)
|
||||
assert.Equal(t, "item1", firstItem)
|
||||
} else {
|
||||
t.Fatal("Expected Some but got None")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Process only non-empty collections", func(t *testing.T) {
|
||||
processItems := func(items []int) Option[int] {
|
||||
return F.Pipe2(
|
||||
items,
|
||||
ToNonEmptyArray[int],
|
||||
O.Map(func(nea NonEmptyArray[int]) int {
|
||||
// Safe to use Head since we know it's non-empty
|
||||
return Head(nea) * 2
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
result1 := processItems([]int{5, 10, 15})
|
||||
assert.True(t, O.IsSome(result1))
|
||||
assert.Equal(t, 10, O.GetOrElse(F.Constant(0))(result1))
|
||||
|
||||
result2 := processItems([]int{})
|
||||
assert.True(t, O.IsNone(result2))
|
||||
})
|
||||
|
||||
t.Run("Convert API response to NonEmptyArray", func(t *testing.T) {
|
||||
// Simulate API response
|
||||
type APIResponse struct {
|
||||
Items []string
|
||||
}
|
||||
|
||||
response := APIResponse{Items: []string{"data1", "data2", "data3"}}
|
||||
|
||||
result := F.Pipe2(
|
||||
response.Items,
|
||||
ToNonEmptyArray[string],
|
||||
O.Map(func(nea NonEmptyArray[string]) string {
|
||||
return "First item: " + Head(nea)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
message := O.GetOrElse(F.Constant("No items"))(result)
|
||||
assert.Equal(t, "First item: data1", message)
|
||||
})
|
||||
|
||||
t.Run("Ensure collection is non-empty before processing", func(t *testing.T) {
|
||||
calculateAverage := func(numbers []float64) Option[float64] {
|
||||
return F.Pipe2(
|
||||
numbers,
|
||||
ToNonEmptyArray[float64],
|
||||
O.Map(func(nea NonEmptyArray[float64]) float64 {
|
||||
sum := 0.0
|
||||
for _, n := range nea {
|
||||
sum += n
|
||||
}
|
||||
return sum / float64(Size(nea))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
result1 := calculateAverage([]float64{10.0, 20.0, 30.0})
|
||||
assert.True(t, O.IsSome(result1))
|
||||
assert.Equal(t, 20.0, O.GetOrElse(F.Constant(0.0))(result1))
|
||||
|
||||
result2 := calculateAverage([]float64{})
|
||||
assert.True(t, O.IsNone(result2))
|
||||
})
|
||||
|
||||
t.Run("Safe head extraction with type guarantee", func(t *testing.T) {
|
||||
getFirstOrDefault := func(items []string, defaultValue string) string {
|
||||
return F.Pipe2(
|
||||
items,
|
||||
ToNonEmptyArray[string],
|
||||
O.Fold(
|
||||
F.Constant(defaultValue),
|
||||
Head[string],
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
result1 := getFirstOrDefault([]string{"a", "b", "c"}, "default")
|
||||
assert.Equal(t, "a", result1)
|
||||
|
||||
result2 := getFirstOrDefault([]string{}, "default")
|
||||
assert.Equal(t, "default", result2)
|
||||
})
|
||||
}
|
||||
15
v2/array/nonempty/types.go
Normal file
15
v2/array/nonempty/types.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package nonempty
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
|
||||
type (
|
||||
|
||||
// NonEmptyArray represents an array with at least one element
|
||||
NonEmptyArray[A any] []A
|
||||
|
||||
Kleisli[A, B any] = func(A) NonEmptyArray[B]
|
||||
|
||||
Operator[A, B any] = Kleisli[NonEmptyArray[A], B]
|
||||
|
||||
Option[A any] = option.Option[A]
|
||||
)
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
@@ -334,7 +335,7 @@ func TestThat(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should work with string predicates", func(t *testing.T) {
|
||||
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' }
|
||||
startsWithH := func(s string) bool { return S.IsNonEmpty(s) && s[0] == 'h' }
|
||||
result := That(startsWithH)("hello")(t)
|
||||
if !result {
|
||||
t.Error("Expected That to pass for string predicate")
|
||||
@@ -484,7 +485,7 @@ func TestLocal(t *testing.T) {
|
||||
t.Run("should compose with other assertions", func(t *testing.T) {
|
||||
// Create multiple focused assertions
|
||||
nameNotEmpty := Local(func(u User) string { return u.Name })(
|
||||
That(func(name string) bool { return len(name) > 0 }),
|
||||
That(S.IsNonEmpty),
|
||||
)
|
||||
ageInRange := Local(func(u User) int { return u.Age })(
|
||||
That(func(age int) bool { return age >= 18 && age <= 100 }),
|
||||
|
||||
169
v2/cli/lens.go
169
v2/cli/lens.go
@@ -27,13 +27,15 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
C "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLensDir = "dir"
|
||||
keyVerbose = "verbose"
|
||||
lensAnnotation = "fp-go:Lens"
|
||||
keyLensDir = "dir"
|
||||
keyVerbose = "verbose"
|
||||
keyIncludeTestFile = "include-test-files"
|
||||
lensAnnotation = "fp-go:Lens"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,6 +51,13 @@ var (
|
||||
Value: false,
|
||||
Usage: "Enable verbose output",
|
||||
}
|
||||
|
||||
flagIncludeTestFiles = &C.BoolFlag{
|
||||
Name: keyIncludeTestFile,
|
||||
Aliases: []string{"t"},
|
||||
Value: false,
|
||||
Usage: "Include test files (*_test.go) when scanning for annotated types",
|
||||
}
|
||||
)
|
||||
|
||||
// structInfo holds information about a struct that needs lens generation
|
||||
@@ -67,6 +76,7 @@ type fieldInfo struct {
|
||||
BaseType string // TypeName without leading * for pointer types
|
||||
IsOptional bool // true if field is a pointer or has json omitempty tag
|
||||
IsComparable bool // true if the type is comparable (can use ==)
|
||||
IsEmbedded bool // true if this field comes from an embedded struct
|
||||
}
|
||||
|
||||
// templateData holds data for template rendering
|
||||
@@ -80,12 +90,12 @@ const lensStructTemplate = `
|
||||
type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}} __lens.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
// optional fields
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}}O __lens_option.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
}
|
||||
@@ -94,13 +104,24 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}} __lens.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
// optional fields
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
// prisms
|
||||
{{- range .Fields}}
|
||||
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
|
||||
type {{.Name}}Prisms{{.TypeParams}} struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
`
|
||||
@@ -110,15 +131,16 @@ const lensConstructorTemplate = `
|
||||
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
lens{{.Name}} := L.MakeLens(
|
||||
lens{{.Name}} := __lens.MakeLensWithName(
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||
lens{{.Name}}O := __lens_option.FromIso[{{$.Name}}{{$.TypeParamNames}}](__iso_option.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}Lenses{{.TypeParamNames}}{
|
||||
@@ -140,21 +162,23 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
lens{{.Name}} := L.MakeLensStrict(
|
||||
lens{{.Name}} := __lens.MakeLensStrictWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
|
||||
)
|
||||
{{- else}}
|
||||
lens{{.Name}} := L.MakeLensRef(
|
||||
lens{{.Name}} := __lens.MakeLensRefWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
|
||||
"(*{{$.Name}}{{$.TypeParamNames}}).{{.Name}}",
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
// optional lenses
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||
lens{{.Name}}O := __lens_option.FromIso[*{{$.Name}}{{$.TypeParamNames}}](__iso_option.FromZero[{{.TypeName}}]())(lens{{.Name}})
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}RefLenses{{.TypeParamNames}}{
|
||||
@@ -170,6 +194,47 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
|
||||
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
|
||||
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
|
||||
func(v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} {
|
||||
{{- if .IsEmbedded}}
|
||||
var result {{$.Name}}{{$.TypeParamNames}}
|
||||
result.{{.Name}} = v
|
||||
return result
|
||||
{{- else}}
|
||||
return {{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||
{{- end}}
|
||||
},
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- else}}
|
||||
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||
func(s {{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
|
||||
func(v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} {
|
||||
{{- if .IsEmbedded}}
|
||||
var result {{$.Name}}{{$.TypeParamNames}}
|
||||
result.{{.Name}} = v
|
||||
return result
|
||||
{{- else}}
|
||||
return {{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||
{{- end}}
|
||||
},
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}Prisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{.Name}}: _prism{{.Name}},
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var (
|
||||
@@ -439,7 +504,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
||||
return results
|
||||
}
|
||||
|
||||
if typeName == "" || typeIdent == nil {
|
||||
if S.IsEmpty(typeName) || typeIdent == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -494,6 +559,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
|
||||
BaseType: baseType,
|
||||
IsOptional: isOptional,
|
||||
IsComparable: isComparable,
|
||||
IsEmbedded: true,
|
||||
},
|
||||
fieldType: field.Type,
|
||||
})
|
||||
@@ -695,7 +761,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
|
||||
}
|
||||
|
||||
// generateLensHelpers scans a directory for Go files and generates lens code
|
||||
func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
func generateLensHelpers(dir, filename string, verbose, includeTestFiles bool) error {
|
||||
// Get absolute path
|
||||
absDir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
@@ -716,21 +782,34 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
log.Printf("Found %d Go files", len(files))
|
||||
}
|
||||
|
||||
// Parse all files and collect structs
|
||||
var allStructs []structInfo
|
||||
// Parse all files and collect structs, separating test and non-test files
|
||||
var regularStructs []structInfo
|
||||
var testStructs []structInfo
|
||||
var packageName string
|
||||
|
||||
for _, file := range files {
|
||||
// Skip generated files and test files
|
||||
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
|
||||
baseName := filepath.Base(file)
|
||||
|
||||
// Skip generated lens files (both regular and test)
|
||||
if strings.HasPrefix(baseName, "gen_lens") && strings.HasSuffix(baseName, ".go") {
|
||||
if verbose {
|
||||
log.Printf("Skipping file: %s", filepath.Base(file))
|
||||
log.Printf("Skipping generated lens file: %s", baseName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
isTestFile := strings.HasSuffix(file, "_test.go")
|
||||
|
||||
// Skip test files unless includeTestFiles is true
|
||||
if isTestFile && !includeTestFiles {
|
||||
if verbose {
|
||||
log.Printf("Skipping test file: %s", baseName)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if verbose {
|
||||
log.Printf("Parsing file: %s", filepath.Base(file))
|
||||
log.Printf("Parsing file: %s", baseName)
|
||||
}
|
||||
|
||||
structs, pkg, err := parseFile(file)
|
||||
@@ -740,27 +819,52 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
}
|
||||
|
||||
if verbose && len(structs) > 0 {
|
||||
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file))
|
||||
log.Printf("Found %d annotated struct(s) in %s", len(structs), baseName)
|
||||
for _, s := range structs {
|
||||
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
|
||||
}
|
||||
}
|
||||
|
||||
if packageName == "" {
|
||||
if S.IsEmpty(packageName) {
|
||||
packageName = pkg
|
||||
}
|
||||
|
||||
allStructs = append(allStructs, structs...)
|
||||
// Separate structs based on source file type
|
||||
if isTestFile {
|
||||
testStructs = append(testStructs, structs...)
|
||||
} else {
|
||||
regularStructs = append(regularStructs, structs...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(allStructs) == 0 {
|
||||
if len(regularStructs) == 0 && len(testStructs) == 0 {
|
||||
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate regular lens file if there are regular structs
|
||||
if len(regularStructs) > 0 {
|
||||
if err := generateLensFile(absDir, filename, packageName, regularStructs, verbose); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate test lens file if there are test structs
|
||||
if len(testStructs) > 0 {
|
||||
testFilename := strings.TrimSuffix(filename, ".go") + "_test.go"
|
||||
if err := generateLensFile(absDir, testFilename, packageName, testStructs, verbose); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateLensFile generates a lens file for the given structs
|
||||
func generateLensFile(absDir, filename, packageName string, structs []structInfo, verbose bool) error {
|
||||
// Collect all unique imports from all structs
|
||||
allImports := make(map[string]string) // import path -> alias
|
||||
for _, s := range allStructs {
|
||||
for _, s := range structs {
|
||||
for importPath, alias := range s.Imports {
|
||||
allImports[importPath] = alias
|
||||
}
|
||||
@@ -774,7 +878,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs))
|
||||
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(structs))
|
||||
|
||||
// Write header
|
||||
writePackage(f, packageName)
|
||||
@@ -782,10 +886,11 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
// Write imports
|
||||
f.WriteString("import (\n")
|
||||
// Standard fp-go imports always needed
|
||||
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
||||
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
|
||||
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
|
||||
f.WriteString("\t__lens \"github.com/IBM/fp-go/v2/optics/lens\"\n")
|
||||
f.WriteString("\t__option \"github.com/IBM/fp-go/v2/option\"\n")
|
||||
f.WriteString("\t__prism \"github.com/IBM/fp-go/v2/optics/prism\"\n")
|
||||
f.WriteString("\t__lens_option \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
|
||||
f.WriteString("\t__iso_option \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
|
||||
|
||||
// Add additional imports collected from field types
|
||||
for importPath, alias := range allImports {
|
||||
@@ -795,7 +900,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
|
||||
f.WriteString(")\n")
|
||||
|
||||
// Generate lens code for each struct using templates
|
||||
for _, s := range allStructs {
|
||||
for _, s := range structs {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Generate struct type
|
||||
@@ -827,12 +932,14 @@ func LensCommand() *C.Command {
|
||||
flagLensDir,
|
||||
flagFilename,
|
||||
flagVerbose,
|
||||
flagIncludeTestFiles,
|
||||
},
|
||||
Action: func(ctx *C.Context) error {
|
||||
return generateLensHelpers(
|
||||
ctx.String(keyLensDir),
|
||||
ctx.String(keyFilename),
|
||||
ctx.Bool(keyVerbose),
|
||||
ctx.Bool(keyIncludeTestFile),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -60,7 +61,7 @@ func TestHasLensAnnotation(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var doc *ast.CommentGroup
|
||||
if tt.comment != "" {
|
||||
if S.IsNonEmpty(tt.comment) {
|
||||
doc = &ast.CommentGroup{
|
||||
List: []*ast.Comment{
|
||||
{Text: tt.comment},
|
||||
@@ -289,7 +290,7 @@ func TestHasOmitEmpty(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var tag *ast.BasicLit
|
||||
if tt.tag != "" {
|
||||
if S.IsNonEmpty(tt.tag) {
|
||||
tag = &ast.BasicLit{
|
||||
Value: tt.tag,
|
||||
}
|
||||
@@ -326,7 +327,7 @@ type Other struct {
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
@@ -380,7 +381,7 @@ type Config struct {
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
@@ -440,7 +441,7 @@ type TypeTest struct {
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
@@ -514,16 +515,16 @@ func TestLensRefTemplatesWithComparable(t *testing.T) {
|
||||
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
|
||||
|
||||
// Name field - comparable, should use MakeLensStrict
|
||||
assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(",
|
||||
"comparable field Name should use MakeLensStrict in RefLenses")
|
||||
assert.Contains(t, constructorStr, "lensName := __lens.MakeLensStrictWithName(",
|
||||
"comparable field Name should use MakeLensStrictWithName in RefLenses")
|
||||
|
||||
// Age field - comparable, should use MakeLensStrict
|
||||
assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(",
|
||||
"comparable field Age should use MakeLensStrict in RefLenses")
|
||||
assert.Contains(t, constructorStr, "lensAge := __lens.MakeLensStrictWithName(",
|
||||
"comparable field Age should use MakeLensStrictWithName in RefLenses")
|
||||
|
||||
// Data field - not comparable, should use MakeLensRef
|
||||
assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(",
|
||||
"non-comparable field Data should use MakeLensRef in RefLenses")
|
||||
assert.Contains(t, constructorStr, "lensData := __lens.MakeLensRefWithName(",
|
||||
"non-comparable field Data should use MakeLensRefWithName in RefLenses")
|
||||
|
||||
}
|
||||
|
||||
@@ -542,12 +543,12 @@ type TestStruct struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
@@ -564,23 +565,23 @@ type TestStruct struct {
|
||||
// Check for expected content in RefLenses
|
||||
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
|
||||
|
||||
// Name and Count are comparable, should use MakeLensStrict
|
||||
assert.Contains(t, contentStr, "L.MakeLensStrict",
|
||||
"comparable fields should use MakeLensStrict in RefLenses")
|
||||
// Name and Count are comparable, should use MakeLensStrictWithName
|
||||
assert.Contains(t, contentStr, "__lens.MakeLensStrictWithName",
|
||||
"comparable fields should use MakeLensStrictWithName in RefLenses")
|
||||
|
||||
// Data is not comparable (slice), should use MakeLensRef
|
||||
assert.Contains(t, contentStr, "L.MakeLensRef",
|
||||
"non-comparable fields should use MakeLensRef in RefLenses")
|
||||
// Data is not comparable (slice), should use MakeLensRefWithName
|
||||
assert.Contains(t, contentStr, "__lens.MakeLensRefWithName",
|
||||
"non-comparable fields should use MakeLensRefWithName in RefLenses")
|
||||
|
||||
// Verify the pattern appears for Name field (comparable)
|
||||
namePattern := "lensName := L.MakeLensStrict("
|
||||
namePattern := "lensName := __lens.MakeLensStrictWithName("
|
||||
assert.Contains(t, contentStr, namePattern,
|
||||
"Name field should use MakeLensStrict")
|
||||
"Name field should use MakeLensStrictWithName")
|
||||
|
||||
// Verify the pattern appears for Data field (not comparable)
|
||||
dataPattern := "lensData := L.MakeLensRef("
|
||||
dataPattern := "lensData := __lens.MakeLensRefWithName("
|
||||
assert.Contains(t, contentStr, dataPattern,
|
||||
"Data field should use MakeLensRef")
|
||||
"Data field should use MakeLensRefWithName")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpers(t *testing.T) {
|
||||
@@ -597,12 +598,12 @@ type TestStruct struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
@@ -621,9 +622,9 @@ type TestStruct struct {
|
||||
assert.Contains(t, contentStr, "Code generated by go generate")
|
||||
assert.Contains(t, contentStr, "TestStructLenses")
|
||||
assert.Contains(t, contentStr, "MakeTestStructLenses")
|
||||
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, contentStr, "IO.FromZero")
|
||||
assert.Contains(t, contentStr, "__lens.Lens[TestStruct, string]")
|
||||
assert.Contains(t, contentStr, "__lens_option.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, contentStr, "__iso_option.FromZero")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
|
||||
@@ -639,12 +640,12 @@ type TestStruct struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code (should not create file)
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file does not exist
|
||||
@@ -669,10 +670,10 @@ func TestLensTemplates(t *testing.T) {
|
||||
|
||||
structStr := structBuf.String()
|
||||
assert.Contains(t, structStr, "type TestStructLenses struct")
|
||||
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]")
|
||||
assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]")
|
||||
assert.Contains(t, structStr, "Name __lens.Lens[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO __lens_option.LensO[TestStruct, string]")
|
||||
assert.Contains(t, structStr, "Value __lens.Lens[TestStruct, *int]")
|
||||
assert.Contains(t, structStr, "ValueO __lens_option.LensO[TestStruct, *int]")
|
||||
|
||||
// Test constructor template
|
||||
var constructorBuf bytes.Buffer
|
||||
@@ -686,7 +687,7 @@ func TestLensTemplates(t *testing.T) {
|
||||
assert.Contains(t, constructorStr, "NameO: lensNameO,")
|
||||
assert.Contains(t, constructorStr, "Value: lensValue,")
|
||||
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero")
|
||||
}
|
||||
|
||||
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||
@@ -707,14 +708,14 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||
|
||||
structStr := structBuf.String()
|
||||
assert.Contains(t, structStr, "type ConfigStructLenses struct")
|
||||
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]")
|
||||
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]")
|
||||
assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]")
|
||||
assert.Contains(t, structStr, "Name __lens.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "NameO __lens_option.LensO[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "Value __lens.Lens[ConfigStruct, string]")
|
||||
assert.Contains(t, structStr, "ValueO __lens_option.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Count __lens.Lens[ConfigStruct, int]")
|
||||
assert.Contains(t, structStr, "CountO __lens_option.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
|
||||
assert.Contains(t, structStr, "Pointer __lens.Lens[ConfigStruct, *string]")
|
||||
assert.Contains(t, structStr, "PointerO __lens_option.LensO[ConfigStruct, *string]")
|
||||
|
||||
// Test constructor template
|
||||
var constructorBuf bytes.Buffer
|
||||
@@ -723,9 +724,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
|
||||
|
||||
constructorStr := constructorBuf.String()
|
||||
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero[string]()")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero[int]()")
|
||||
assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero[string]()")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero[int]()")
|
||||
assert.Contains(t, constructorStr, "__iso_option.FromZero[*string]()")
|
||||
}
|
||||
|
||||
func TestLensCommandFlags(t *testing.T) {
|
||||
@@ -737,9 +738,9 @@ func TestLensCommandFlags(t *testing.T) {
|
||||
assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")
|
||||
|
||||
// Check flags
|
||||
assert.Len(t, cmd.Flags, 3)
|
||||
assert.Len(t, cmd.Flags, 4)
|
||||
|
||||
var hasDir, hasFilename, hasVerbose bool
|
||||
var hasDir, hasFilename, hasVerbose, hasIncludeTestFiles bool
|
||||
for _, flag := range cmd.Flags {
|
||||
switch flag.Names()[0] {
|
||||
case "dir":
|
||||
@@ -748,12 +749,15 @@ func TestLensCommandFlags(t *testing.T) {
|
||||
hasFilename = true
|
||||
case "verbose":
|
||||
hasVerbose = true
|
||||
case "include-test-files":
|
||||
hasIncludeTestFiles = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, hasDir, "should have dir flag")
|
||||
assert.True(t, hasFilename, "should have filename flag")
|
||||
assert.True(t, hasVerbose, "should have verbose flag")
|
||||
assert.True(t, hasIncludeTestFiles, "should have include-test-files flag")
|
||||
}
|
||||
|
||||
func TestParseFileWithEmbeddedStruct(t *testing.T) {
|
||||
@@ -776,7 +780,7 @@ type Extended struct {
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
@@ -824,12 +828,12 @@ type Person struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
@@ -849,14 +853,14 @@ type Person struct {
|
||||
assert.Contains(t, contentStr, "MakePersonLenses")
|
||||
|
||||
// Check that embedded fields are included
|
||||
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
|
||||
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
|
||||
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
|
||||
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")
|
||||
assert.Contains(t, contentStr, "Street __lens.Lens[Person, string]", "Should have lens for embedded Street field")
|
||||
assert.Contains(t, contentStr, "City __lens.Lens[Person, string]", "Should have lens for embedded City field")
|
||||
assert.Contains(t, contentStr, "Name __lens.Lens[Person, string]", "Should have lens for Name field")
|
||||
assert.Contains(t, contentStr, "Age __lens.Lens[Person, int]", "Should have lens for Age field")
|
||||
|
||||
// Check that optional lenses are also generated for embedded fields
|
||||
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
|
||||
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
|
||||
assert.Contains(t, contentStr, "StreetO __lens_option.LensO[Person, string]")
|
||||
assert.Contains(t, contentStr, "CityO __lens_option.LensO[Person, string]")
|
||||
}
|
||||
|
||||
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
|
||||
@@ -880,7 +884,7 @@ type Document struct {
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
@@ -922,7 +926,7 @@ type Container[T any] struct {
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
@@ -960,7 +964,7 @@ type Pair[K comparable, V any] struct {
|
||||
}
|
||||
`
|
||||
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse the file
|
||||
@@ -998,12 +1002,12 @@ type Box[T any] struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
@@ -1025,14 +1029,14 @@ type Box[T any] struct {
|
||||
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
|
||||
|
||||
// Check that fields use the generic type parameter
|
||||
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
|
||||
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")
|
||||
assert.Contains(t, contentStr, "Content __lens.Lens[Box[T], T]", "Should have lens for generic Content field")
|
||||
assert.Contains(t, contentStr, "Label __lens.Lens[Box[T], string]", "Should have lens for Label field")
|
||||
|
||||
// Check optional lenses - only for comparable types
|
||||
// T any is not comparable, so ContentO should NOT be generated
|
||||
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
|
||||
assert.NotContains(t, contentStr, "ContentO __lens_option.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
|
||||
// string is comparable, so LabelO should be generated
|
||||
assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens")
|
||||
assert.Contains(t, contentStr, "LabelO __lens_option.LensO[Box[T], string]", "string is comparable, should have optional lens")
|
||||
}
|
||||
|
||||
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
|
||||
@@ -1049,12 +1053,12 @@ type ComparableBox[T comparable] struct {
|
||||
`
|
||||
|
||||
testFile := filepath.Join(tmpDir, "test.go")
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0644)
|
||||
err := os.WriteFile(testFile, []byte(testCode), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Generate lens code
|
||||
outputFile := "gen.go"
|
||||
err = generateLensHelpers(tmpDir, outputFile, false)
|
||||
err = generateLensHelpers(tmpDir, outputFile, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the generated file exists
|
||||
@@ -1074,11 +1078,11 @@ type ComparableBox[T comparable] struct {
|
||||
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
|
||||
|
||||
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
|
||||
assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict")
|
||||
assert.Contains(t, contentStr, "lensKey := __lens.MakeLensStrictWithName(", "Key field with comparable constraint should use MakeLensStrictWithName")
|
||||
|
||||
// Check that Value field (string, always comparable) also uses MakeLensStrict
|
||||
assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict")
|
||||
assert.Contains(t, contentStr, "lensValue := __lens.MakeLensStrictWithName(", "Value field (string) should use MakeLensStrictWithName")
|
||||
|
||||
// Verify that MakeLensRef is NOT used (since both fields are comparable)
|
||||
assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable")
|
||||
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
// Deprecated:
|
||||
@@ -176,7 +178,7 @@ func generateTraverseTuple1(
|
||||
}
|
||||
fmt.Fprintf(f, "F%d ~func(A%d) %s", j+1, j+1, hkt(fmt.Sprintf("T%d", j+1)))
|
||||
}
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
// types
|
||||
@@ -209,7 +211,7 @@ func generateTraverseTuple1(
|
||||
fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i)
|
||||
// map
|
||||
fmt.Fprintf(f, " Map[")
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s, T1,", infix)
|
||||
} else {
|
||||
fmt.Fprintf(f, "T1,")
|
||||
@@ -231,7 +233,7 @@ func generateTraverseTuple1(
|
||||
fmt.Fprintf(f, " ")
|
||||
}
|
||||
fmt.Fprintf(f, "%s", tuple)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
fmt.Fprintf(f, ", T%d],\n", j+1)
|
||||
@@ -256,11 +258,11 @@ func generateSequenceTuple1(
|
||||
|
||||
fmt.Fprintf(f, "\n// SequenceTuple%d converts a [Tuple%d] of [%s] into an [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
|
||||
fmt.Fprintf(f, "func SequenceTuple%d[", i)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s", infix)
|
||||
}
|
||||
for j := 0; j < i; j++ {
|
||||
if infix != "" || j > 0 {
|
||||
if S.IsNonEmpty(infix) || j > 0 {
|
||||
fmt.Fprintf(f, ", ")
|
||||
}
|
||||
fmt.Fprintf(f, "T%d", j+1)
|
||||
@@ -276,7 +278,7 @@ func generateSequenceTuple1(
|
||||
fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i)
|
||||
// map
|
||||
fmt.Fprintf(f, " Map[")
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s, T1,", infix)
|
||||
} else {
|
||||
fmt.Fprintf(f, "T1,")
|
||||
@@ -298,7 +300,7 @@ func generateSequenceTuple1(
|
||||
fmt.Fprintf(f, " ")
|
||||
}
|
||||
fmt.Fprintf(f, "%s", tuple)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
fmt.Fprintf(f, ", T%d],\n", j+1)
|
||||
@@ -319,11 +321,11 @@ func generateSequenceT1(
|
||||
|
||||
fmt.Fprintf(f, "\n// SequenceT%d converts %d parameters of [%s] into a [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
|
||||
fmt.Fprintf(f, "func SequenceT%d[", i)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s", infix)
|
||||
}
|
||||
for j := 0; j < i; j++ {
|
||||
if infix != "" || j > 0 {
|
||||
if S.IsNonEmpty(infix) || j > 0 {
|
||||
fmt.Fprintf(f, ", ")
|
||||
}
|
||||
fmt.Fprintf(f, "T%d", j+1)
|
||||
@@ -339,7 +341,7 @@ func generateSequenceT1(
|
||||
fmt.Fprintf(f, " return A.SequenceT%d(\n", i)
|
||||
// map
|
||||
fmt.Fprintf(f, " Map[")
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, "%s, T1,", infix)
|
||||
} else {
|
||||
fmt.Fprintf(f, "T1,")
|
||||
@@ -361,7 +363,7 @@ func generateSequenceT1(
|
||||
fmt.Fprintf(f, " ")
|
||||
}
|
||||
fmt.Fprintf(f, "%s", tuple)
|
||||
if infix != "" {
|
||||
if S.IsNonEmpty(infix) {
|
||||
fmt.Fprintf(f, ", %s", infix)
|
||||
}
|
||||
fmt.Fprintf(f, ", T%d],\n", j+1)
|
||||
|
||||
@@ -753,3 +753,17 @@ func WithDeadline[A any](deadline time.Time) Operator[A, A] {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ import (
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[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,
|
||||
)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -72,4 +73,6 @@ type (
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
)
|
||||
|
||||
@@ -8,9 +8,10 @@ This document explains how the `Sequence*` functions in the `context/readeriores
|
||||
2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
|
||||
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
|
||||
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
|
||||
5. [Practical Benefits](#practical-benefits)
|
||||
6. [Examples](#examples)
|
||||
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
||||
5. [TraverseReader: Introducing Dependencies](#traversereader-introducing-dependencies)
|
||||
6. [Practical Benefits](#practical-benefits)
|
||||
7. [Examples](#examples)
|
||||
8. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
|
||||
|
||||
## What is Point-Free Style?
|
||||
|
||||
@@ -25,10 +26,7 @@ func double(x int) int {
|
||||
|
||||
**Point-free style (without points):**
|
||||
```go
|
||||
var double = F.Flow2(
|
||||
N.Mul(2),
|
||||
identity,
|
||||
)
|
||||
var double = N.Mul(2)
|
||||
```
|
||||
|
||||
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
|
||||
@@ -99,7 +97,7 @@ The `Sequence*` functions solve this by "flipping" or "sequencing" the nested st
|
||||
```go
|
||||
func SequenceReader[R, A any](
|
||||
ma ReaderIOResult[Reader[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
) Kleisli[R, A]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
@@ -115,7 +113,7 @@ Now `R` (the Reader's environment) comes **first**, before `context.Context`!
|
||||
```go
|
||||
func SequenceReaderIO[R, A any](
|
||||
ma ReaderIOResult[ReaderIO[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
) Kleisli[R, A]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
@@ -129,7 +127,7 @@ To: func(R) func(context.Context) func() Either[error, A]
|
||||
```go
|
||||
func SequenceReaderResult[R, A any](
|
||||
ma ReaderIOResult[ReaderResult[R, A]]
|
||||
) reader.Kleisli[context.Context, R, IOResult[A]]
|
||||
) Kleisli[R, A]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
@@ -222,6 +220,186 @@ authInfo := authService(ctx)()
|
||||
userInfo := userService(ctx)()
|
||||
```
|
||||
|
||||
## TraverseReader: Introducing Dependencies
|
||||
|
||||
While `SequenceReader` flips the parameter order of an existing nested structure, `TraverseReader` allows you to **introduce** a new Reader dependency into an existing computation.
|
||||
|
||||
### Function Signature
|
||||
|
||||
```go
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderIOResult[A]) Kleisli[R, B]
|
||||
```
|
||||
|
||||
**Type transformation:**
|
||||
```
|
||||
Input: ReaderIOResult[A] = func(context.Context) func() Either[error, A]
|
||||
With: reader.Kleisli[R, A, B] = func(A) func(R) B
|
||||
Output: Kleisli[R, B] = func(R) func(context.Context) func() Either[error, B]
|
||||
```
|
||||
|
||||
### What It Does
|
||||
|
||||
`TraverseReader` takes:
|
||||
1. A Reader-based transformation `f: func(A) func(R) B` that depends on environment `R`
|
||||
2. Returns a function that transforms `ReaderIOResult[A]` into `Kleisli[R, B]`
|
||||
|
||||
This allows you to:
|
||||
- Add environment dependencies to computations that don't have them yet
|
||||
- Transform values within a ReaderIOResult using environment-dependent logic
|
||||
- Build composable pipelines where transformations depend on configuration
|
||||
|
||||
### Key Difference from SequenceReader
|
||||
|
||||
- **SequenceReader**: Works with computations that **already contain** a Reader (`ReaderIOResult[Reader[R, A]]`)
|
||||
- Flips the order so `R` comes first
|
||||
- No transformation of the value itself
|
||||
|
||||
- **TraverseReader**: Works with computations that **don't have** a Reader yet (`ReaderIOResult[A]`)
|
||||
- Introduces a new Reader dependency via a transformation function
|
||||
- Transforms `A` to `B` using environment `R`
|
||||
|
||||
### Example: Adding Configuration to a Computation
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Original computation that just produces an int
|
||||
getValue := func(ctx context.Context) func() Either[error, int] {
|
||||
return func() Either[error, int] {
|
||||
return Right[error](10)
|
||||
}
|
||||
}
|
||||
|
||||
// A Reader-based transformation that depends on Config
|
||||
formatWithConfig := func(n int) func(Config) string {
|
||||
return func(cfg Config) string {
|
||||
result := n * cfg.Multiplier
|
||||
return fmt.Sprintf("%s: %d", cfg.Prefix, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Use TraverseReader to introduce Config dependency
|
||||
traversed := TraverseReader[Config, int, string](formatWithConfig)
|
||||
withConfig := traversed(getValue)
|
||||
|
||||
// Now we can provide Config to get the final result
|
||||
cfg := Config{Multiplier: 5, Prefix: "Result"}
|
||||
ctx := context.Background()
|
||||
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
|
||||
```
|
||||
|
||||
### Point-Free Composition with TraverseReader
|
||||
|
||||
```go
|
||||
// Build a pipeline that introduces dependencies at each stage
|
||||
var pipeline = F.Flow4(
|
||||
loadValue, // ReaderIOResult[int]
|
||||
TraverseReader(multiplyByConfig), // Kleisli[Config, int]
|
||||
applyConfig(cfg), // ReaderIOResult[int]
|
||||
Chain(TraverseReader(formatWithStyle)), // Introduce another dependency
|
||||
)
|
||||
```
|
||||
|
||||
### When to Use TraverseReader vs SequenceReader
|
||||
|
||||
**Use SequenceReader when:**
|
||||
- Your computation already returns a Reader: `ReaderIOResult[Reader[R, A]]`
|
||||
- You just want to flip the parameter order
|
||||
- No transformation of the value is needed
|
||||
|
||||
```go
|
||||
// Already have Reader[Config, int]
|
||||
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
|
||||
sequenced := SequenceReader[Config, int](computation)
|
||||
result := sequenced(cfg)(ctx)()
|
||||
```
|
||||
|
||||
**Use TraverseReader when:**
|
||||
- Your computation doesn't have a Reader yet: `ReaderIOResult[A]`
|
||||
- You want to transform the value using environment-dependent logic
|
||||
- You're introducing a new dependency into the pipeline
|
||||
|
||||
```go
|
||||
// Have ReaderIOResult[int], want to add Config dependency
|
||||
computation := getValue() // ReaderIOResult[int]
|
||||
traversed := TraverseReader[Config, int, string](formatWithConfig)
|
||||
withDep := traversed(computation)
|
||||
result := withDep(cfg)(ctx)()
|
||||
```
|
||||
|
||||
### Practical Example: Multi-Stage Processing
|
||||
|
||||
```go
|
||||
type DatabaseConfig struct {
|
||||
ConnectionString string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type FormattingConfig struct {
|
||||
DateFormat string
|
||||
Timezone string
|
||||
}
|
||||
|
||||
// Stage 1: Load raw data (no dependencies yet)
|
||||
loadData := func(ctx context.Context) func() Either[error, RawData] {
|
||||
// ... implementation
|
||||
}
|
||||
|
||||
// Stage 2: Process with database config
|
||||
processWithDB := func(raw RawData) func(DatabaseConfig) ProcessedData {
|
||||
return func(cfg DatabaseConfig) ProcessedData {
|
||||
// Use cfg.ConnectionString, cfg.Timeout
|
||||
return ProcessedData{/* ... */}
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3: Format with formatting config
|
||||
formatData := func(processed ProcessedData) func(FormattingConfig) string {
|
||||
return func(cfg FormattingConfig) string {
|
||||
// Use cfg.DateFormat, cfg.Timezone
|
||||
return "formatted result"
|
||||
}
|
||||
}
|
||||
|
||||
// Build pipeline introducing dependencies at each stage
|
||||
var pipeline = F.Flow3(
|
||||
loadData,
|
||||
TraverseReader[DatabaseConfig, RawData, ProcessedData](processWithDB),
|
||||
// Now we have Kleisli[DatabaseConfig, ProcessedData]
|
||||
applyConfig(dbConfig),
|
||||
// Now we have ReaderIOResult[ProcessedData]
|
||||
TraverseReader[FormattingConfig, ProcessedData, string](formatData),
|
||||
// Now we have Kleisli[FormattingConfig, string]
|
||||
)
|
||||
|
||||
// Execute with both configs
|
||||
result := pipeline(fmtConfig)(ctx)()
|
||||
```
|
||||
|
||||
### Combining TraverseReader and SequenceReader
|
||||
|
||||
You can combine both functions in complex pipelines:
|
||||
|
||||
```go
|
||||
// Start with nested Reader
|
||||
computation := getComputation() // ReaderIOResult[Reader[Config, User]]
|
||||
|
||||
var pipeline = F.Flow4(
|
||||
computation,
|
||||
SequenceReader[Config, User], // Flip to get Kleisli[Config, User]
|
||||
applyConfig(cfg), // Apply config, get ReaderIOResult[User]
|
||||
TraverseReader(enrichWithDatabase), // Add database dependency
|
||||
// Now have Kleisli[Database, EnrichedUser]
|
||||
)
|
||||
|
||||
result := pipeline(db)(ctx)()
|
||||
```
|
||||
|
||||
## Practical Benefits
|
||||
|
||||
### 1. **Improved Testability**
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
@@ -96,7 +95,7 @@ func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return RIOR.Bind(setter, F.Flow2(f, WithContext))
|
||||
return RIOR.Bind(setter, WithContextK(f))
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
@@ -128,6 +127,13 @@ func BindTo[S1, T any](
|
||||
return RIOR.BindTo[context.Context](setter)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToP[S1, T any](
|
||||
setter Prism[S1, T],
|
||||
) Operator[T, S1] {
|
||||
return BindTo(setter.ReverseGet)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
@@ -214,7 +220,7 @@ func ApS[S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa ReaderIOResult[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
@@ -253,10 +259,10 @@ func ApSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return RIOR.BindL(lens, F.Flow2(f, WithContext))
|
||||
return RIOR.BindL(lens, WithContextK(f))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
|
||||
@@ -289,7 +295,7 @@ func BindL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[S, S] {
|
||||
return RIOR.LetL[context.Context](lens, f)
|
||||
@@ -322,7 +328,7 @@ func LetL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return RIOR.LetToL[context.Context](lens, b)
|
||||
@@ -443,7 +449,7 @@ func BindResultK[S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindIOEitherKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f ioresult.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||
@@ -458,7 +464,7 @@ func BindIOEitherKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindIOResultKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f ioresult.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIOEither[T]))
|
||||
@@ -474,7 +480,7 @@ func BindIOResultKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindIOKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f io.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromIO[T]))
|
||||
@@ -490,7 +496,7 @@ func BindIOKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f reader.Kleisli[context.Context, T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReader[T]))
|
||||
@@ -506,7 +512,7 @@ func BindReaderKL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindReaderIOKL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f readerio.Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
|
||||
@@ -627,7 +633,7 @@ func ApResultS[S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApIOEitherSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa IOResult[T],
|
||||
) Operator[S, S] {
|
||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
||||
@@ -642,7 +648,7 @@ func ApIOEitherSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApIOResultSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa IOResult[T],
|
||||
) Operator[S, S] {
|
||||
return F.Bind2nd(F.Flow2[ReaderIOResult[S], ioresult.Operator[S, S]], ioresult.ApSL(lens, fa))
|
||||
@@ -657,7 +663,7 @@ func ApIOResultSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApIOSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromIO(fa))
|
||||
@@ -672,7 +678,7 @@ func ApIOSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa Reader[context.Context, T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromReader(fa))
|
||||
@@ -687,7 +693,7 @@ func ApReaderSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApReaderIOSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa ReaderIO[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromReaderIO(fa))
|
||||
@@ -702,7 +708,7 @@ func ApReaderIOSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApEitherSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa Result[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromEither(fa))
|
||||
@@ -717,7 +723,7 @@ func ApEitherSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApResultSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa Result[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, FromResult(fa))
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"context"
|
||||
|
||||
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
@@ -40,3 +41,11 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ import (
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
|
||||
return RIOR.SequenceReader(ma)
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[co
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R, A] {
|
||||
return RIOR.SequenceReaderIO(ma)
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kl
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
|
||||
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kleisli[R, A] {
|
||||
return RIOR.SequenceReaderEither(ma)
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ func MapTo[A, B any](b B) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
|
||||
return RIOR.MonadChain(ma, function.Flow2(f, WithContext))
|
||||
return RIOR.MonadChain(ma, WithContextK(f))
|
||||
}
|
||||
|
||||
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
|
||||
@@ -165,7 +165,7 @@ func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return RIOR.Chain(function.Flow2(f, WithContext))
|
||||
return RIOR.Chain(WithContextK(f))
|
||||
}
|
||||
|
||||
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||
@@ -179,12 +179,12 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirst(ma, function.Flow2(f, WithContext))
|
||||
return RIOR.MonadChainFirst(ma, WithContextK(f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTap(ma, function.Flow2(f, WithContext))
|
||||
return RIOR.MonadTap(ma, WithContextK(f))
|
||||
}
|
||||
|
||||
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
|
||||
@@ -197,12 +197,12 @@ func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A]
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirst(function.Flow2(f, WithContext))
|
||||
return RIOR.ChainFirst(WithContextK(f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return RIOR.Tap(function.Flow2(f, WithContext))
|
||||
return RIOR.Tap(WithContextK(f))
|
||||
}
|
||||
|
||||
// Of creates a [ReaderIOResult] that always succeeds with the given value.
|
||||
@@ -401,6 +401,11 @@ 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)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
|
||||
// The Either-returning function is executed for its validation/side effects only.
|
||||
//
|
||||
@@ -915,7 +920,7 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainLeft(fa, function.Flow2(f, WithContext))
|
||||
return RIOR.MonadChainLeft(fa, WithContextK(f))
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
@@ -923,7 +928,7 @@ func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIORe
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
|
||||
return RIOR.ChainLeft(function.Flow2(f, WithContext))
|
||||
return RIOR.ChainLeft(WithContextK(f))
|
||||
}
|
||||
|
||||
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
|
||||
@@ -936,12 +941,12 @@ func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadChainFirstLeft(ma, function.Flow2(f, WithContext))
|
||||
return RIOR.MonadChainFirstLeft(ma, WithContextK(f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
|
||||
return RIOR.MonadTapLeft(ma, function.Flow2(f, WithContext))
|
||||
return RIOR.MonadTapLeft(ma, WithContextK(f))
|
||||
}
|
||||
|
||||
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
|
||||
@@ -953,12 +958,12 @@ func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOR
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return RIOR.ChainFirstLeft[A](function.Flow2(f, WithContext))
|
||||
return RIOR.ChainFirstLeft[A](WithContextK(f))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return RIOR.TapLeft[A](function.Flow2(f, WithContext))
|
||||
return RIOR.TapLeft[A](WithContextK(f))
|
||||
}
|
||||
|
||||
// Local transforms the context.Context environment before passing it to a ReaderIOResult computation.
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
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"
|
||||
)
|
||||
@@ -36,9 +35,9 @@ import (
|
||||
//
|
||||
// # 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
|
||||
// TailRec takes a Kleisli arrow that returns Trampoline[A, B]:
|
||||
// - Bounce(A): Continue recursion with the new state A
|
||||
// - Land(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
|
||||
@@ -51,11 +50,11 @@ import (
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Either[A, B]]) that:
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Trampoline[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)
|
||||
// - Produces Trampoline[A, B] to control recursion flow (Right in the outer Either)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
@@ -93,15 +92,15 @@ import (
|
||||
//
|
||||
// # 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]] {
|
||||
// countdownStep := func(n int) readerioresult.ReaderIOResult[tailrec.Trampoline[int, string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[int, string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[int, string]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](either.Right[int]("Done!"))
|
||||
// return either.Right[error](tailrec.Land[int]("Done!"))
|
||||
// }
|
||||
// // Simulate some work
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return either.Right[error](either.Left[string](n - 1))
|
||||
// return either.Right[error](tailrec.Bounce[string](n - 1))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -120,20 +119,20 @@ import (
|
||||
// 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]] {
|
||||
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// if len(state.files) == 0 {
|
||||
// return either.Right[error](either.Right[ProcessState](state.processed))
|
||||
// return either.Right[error](tailrec.Land[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.Left[tailrec.Trampoline[ProcessState, []string]](err)
|
||||
// }
|
||||
//
|
||||
// return either.Right[error](either.Left[[]string](ProcessState{
|
||||
// return either.Right[error](tailrec.Bounce[[]string](ProcessState{
|
||||
// files: state.files[1:],
|
||||
// processed: append(state.processed, file),
|
||||
// }))
|
||||
@@ -179,6 +178,6 @@ import (
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return RIOR.TailRec(F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
@@ -23,20 +23,22 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
"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]] {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,13 +56,13 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
|
||||
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]] {
|
||||
factorialStep := func(state FactorialState) ReaderIOResult[Trampoline[FactorialState, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[FactorialState, int]] {
|
||||
return func() Either[Trampoline[FactorialState, int]] {
|
||||
if state.n <= 1 {
|
||||
return E.Right[error](E.Right[FactorialState](state.acc))
|
||||
return E.Right[error](tailrec.Land[FactorialState](state.acc))
|
||||
}
|
||||
return E.Right[error](E.Left[int](FactorialState{
|
||||
return E.Right[error](tailrec.Bounce[int](FactorialState{
|
||||
n: state.n - 1,
|
||||
acc: state.acc * state.n,
|
||||
}))
|
||||
@@ -78,16 +80,16 @@ 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]] {
|
||||
errorStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
if n == 3 {
|
||||
return E.Left[E.Either[int, string]](testErr)
|
||||
return E.Left[Trampoline[int, string]](testErr)
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,18 +106,18 @@ 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]] {
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[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](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,13 +145,13 @@ func TestTailRec_ContextCancellation(t *testing.T) {
|
||||
|
||||
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]] {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,13 +174,13 @@ 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]] {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int](0))
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](E.Left[int](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,9 +196,9 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
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]] {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
|
||||
// Add a small delay every 1000 iterations to make cancellation more likely
|
||||
@@ -205,9 +207,9 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int](0))
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](E.Left[int](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,22 +241,22 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
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 len(state.items) == 0 {
|
||||
return E.Right[error](E.Right[ProcessState](state.processed))
|
||||
processStep := func(state ProcessState) ReaderIOResult[Trampoline[ProcessState, []string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[ProcessState, []string]] {
|
||||
return func() Either[Trampoline[ProcessState, []string]] {
|
||||
if A.IsEmpty(state.items) {
|
||||
return E.Right[error](tailrec.Land[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]](
|
||||
return E.Left[Trampoline[ProcessState, []string]](
|
||||
fmt.Errorf("failed to process item: %s", item))
|
||||
}
|
||||
|
||||
return E.Right[error](E.Left[[]string](ProcessState{
|
||||
return E.Right[error](tailrec.Bounce[[]string](ProcessState{
|
||||
items: state.items[1:],
|
||||
processed: append(state.processed, item),
|
||||
errors: state.errors,
|
||||
@@ -301,18 +303,18 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
|
||||
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 len(state.files) == 0 {
|
||||
return E.Right[error](E.Right[FileProcessState](state.processed))
|
||||
processFileStep := func(state FileProcessState) ReaderIOResult[Trampoline[FileProcessState, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[FileProcessState, int]] {
|
||||
return func() Either[Trampoline[FileProcessState, int]] {
|
||||
if A.IsEmpty(state.files) {
|
||||
return E.Right[error](tailrec.Land[FileProcessState](state.processed))
|
||||
}
|
||||
|
||||
// Simulate file processing time
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
atomic.AddInt32(&processedCount, 1)
|
||||
|
||||
return E.Right[error](E.Left[int](FileProcessState{
|
||||
return E.Right[error](tailrec.Bounce[int](FileProcessState{
|
||||
files: state.files[1:],
|
||||
processed: state.processed + 1,
|
||||
}))
|
||||
@@ -355,10 +357,10 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
|
||||
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"))
|
||||
immediateStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
return E.Right[error](tailrec.Land[int]("immediate"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -373,16 +375,16 @@ 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]] {
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[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](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,17 +411,17 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
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]] {
|
||||
valueStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[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](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,5 +432,3 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
@@ -33,6 +35,7 @@ import (
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -132,4 +135,9 @@ type (
|
||||
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]
|
||||
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
)
|
||||
|
||||
@@ -17,7 +17,6 @@ package readerresult
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
G "github.com/IBM/fp-go/v2/readereither/generic"
|
||||
)
|
||||
|
||||
@@ -39,10 +38,18 @@ func Do[S any](
|
||||
return G.Do[ReaderResult[S]](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// Bind attaches the result of an EFFECTFUL computation to a context [S1] to produce a context [S2].
|
||||
// This enables sequential composition where each step can depend on the results of previous steps
|
||||
// and access the context.Context from the environment.
|
||||
//
|
||||
// IMPORTANT: Bind is for EFFECTFUL FUNCTIONS that depend on context.Context.
|
||||
// The function parameter takes state and returns a ReaderResult[T], which is effectful because
|
||||
// it depends on context.Context (can be cancelled, has deadlines, carries values).
|
||||
//
|
||||
// For PURE FUNCTIONS (side-effect free), use:
|
||||
// - BindResultK: For pure functions with errors (State -> (Value, error))
|
||||
// - Let: For pure functions without errors (State -> Value)
|
||||
//
|
||||
// The setter function takes the result of the computation and returns a function that
|
||||
// updates the context from S1 to S2.
|
||||
//
|
||||
@@ -89,7 +96,16 @@ func Bind[S1, S2, T any](
|
||||
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](
|
||||
@@ -99,7 +115,8 @@ func Let[S1, S2, T any](
|
||||
return G.Let[ReaderResult[S1], ReaderResult[S2]](setter, f)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
|
||||
// This is a PURE operation (side-effect free) that simply sets a field to a constant value.
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
@@ -114,13 +131,23 @@ func LetTo[S1, S2, T any](
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Kleisli[ReaderResult[T], S1] {
|
||||
) Operator[T, S1] {
|
||||
return G.BindTo[ReaderResult[S1], ReaderResult[T]](setter)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToP[S1, T any](
|
||||
setter Prism[S1, T],
|
||||
) Operator[T, S1] {
|
||||
return BindTo(setter.ReverseGet)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
// This allows independent EFFECTFUL computations to be combined without one depending on the result of the other.
|
||||
//
|
||||
// IMPORTANT: ApS is for EFFECTFUL FUNCTIONS that depend on context.Context.
|
||||
// The ReaderResult parameter is effectful because it depends on context.Context.
|
||||
//
|
||||
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
|
||||
// and can conceptually run in parallel.
|
||||
@@ -198,16 +225,21 @@ func ApS[S1, S2, T any](
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa ReaderResult[T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
|
||||
// It combines the lens-based field access with monadic composition, allowing you to:
|
||||
// It combines the lens-based field access with monadic composition for EFFECTFUL computations.
|
||||
//
|
||||
// IMPORTANT: BindL is for EFFECTFUL FUNCTIONS that depend on context.Context.
|
||||
// The function parameter returns a ReaderResult, which is effectful.
|
||||
//
|
||||
// It allows you to:
|
||||
// 1. Extract a field value using the lens
|
||||
// 2. Use that value in a computation that may fail
|
||||
// 2. Use that value in an effectful computation that may fail
|
||||
// 3. Update the field with the result
|
||||
//
|
||||
// Parameters:
|
||||
@@ -244,14 +276,17 @@ func ApSL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, F.Flow2(f, WithContext)))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
|
||||
// It applies a pure transformation to the focused field without any effects.
|
||||
// It applies a PURE transformation to the focused field without any effects.
|
||||
//
|
||||
// IMPORTANT: LetL is for PURE FUNCTIONS (side-effect free) that don't depend on context.Context.
|
||||
// The function parameter is a pure endomorphism (T -> T) with no errors or effects.
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
@@ -281,14 +316,14 @@ func BindL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return Let(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
|
||||
// It sets the focused field to a constant value.
|
||||
// It sets the focused field to a constant value. This is a PURE operation (side-effect free).
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A lens that focuses on a field of type T within state S
|
||||
@@ -317,7 +352,7 @@ func LetL[S, T any](
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return LetTo(lens.Set, b)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"context"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
|
||||
@@ -30,3 +31,11 @@ func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
|
||||
return ma(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
|
||||
return F.Flow2(
|
||||
f,
|
||||
WithContext,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
|
||||
// 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.
|
||||
// TailRec takes a Kleisli function that returns Trampoline[A, B] and converts it into a stack-safe,
|
||||
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Land 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
|
||||
@@ -37,9 +37,9 @@ import (
|
||||
// - 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'.
|
||||
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Trampoline[A, B].
|
||||
// When the result is Bounce(a), recursion continues with the new value 'a'.
|
||||
// When the result is Land(b), recursion terminates with the final value 'b'.
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
|
||||
@@ -48,8 +48,8 @@ import (
|
||||
// - 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'
|
||||
// - If the step returns Right[A](Bounce(a)), continues recursion with new value 'a'
|
||||
// - If the step returns Right[A](Land(b)), terminates with success value 'b'
|
||||
//
|
||||
// Example - Factorial computation with context:
|
||||
//
|
||||
@@ -58,12 +58,12 @@ import (
|
||||
// acc int
|
||||
// }
|
||||
//
|
||||
// factorialStep := func(state State) ReaderResult[either.Either[State, int]] {
|
||||
// return func(ctx context.Context) result.Result[either.Either[State, int]] {
|
||||
// factorialStep := func(state State) ReaderResult[tailrec.Trampoline[State, int]] {
|
||||
// return func(ctx context.Context) result.Result[tailrec.Trampoline[State, int]] {
|
||||
// if state.n <= 0 {
|
||||
// return result.Of(either.Right[State](state.acc))
|
||||
// return result.Of(tailrec.Land[State](state.acc))
|
||||
// }
|
||||
// return result.Of(either.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
// return result.Of(tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
@@ -80,10 +80,10 @@ import (
|
||||
// // 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] {
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return func(a A) ReaderResult[B] {
|
||||
initialReader := f(a)
|
||||
return func(ctx context.Context) Result[B] {
|
||||
return func(ctx context.Context) result.Result[B] {
|
||||
rdr := initialReader
|
||||
for {
|
||||
// short circuit
|
||||
@@ -95,11 +95,10 @@ func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
|
||||
if either.IsLeft(current) {
|
||||
return result.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return result.Of(b)
|
||||
if rec.Landed {
|
||||
return result.Of(rec.Land)
|
||||
}
|
||||
rdr = f(a)
|
||||
rdr = f(rec.Bounce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -34,12 +35,12 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
factorialStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.n <= 0 {
|
||||
return R.Of(E.Right[State](state.acc))
|
||||
return R.Of(TR.Land[State](state.acc))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
return R.Of(TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +58,12 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
curr int
|
||||
}
|
||||
|
||||
fibStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
fibStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.n <= 0 {
|
||||
return R.Of(E.Right[State](state.curr))
|
||||
return R.Of(TR.Land[State](state.curr))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
return R.Of(TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +75,12 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
|
||||
// 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]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +92,9 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
|
||||
// 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))
|
||||
immediateStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
return R.Of(TR.Land[int](n * 2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,12 +106,12 @@ func TestTailRecImmediateTermination(t *testing.T) {
|
||||
|
||||
// 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]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,12 +128,12 @@ func TestTailRecSumList(t *testing.T) {
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
if len(state.list) == 0 {
|
||||
return R.Of(E.Right[State](state.sum))
|
||||
sumStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if A.IsEmpty(state.list) {
|
||||
return R.Of(TR.Land[State](state.sum))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
return R.Of(TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,15 +145,15 @@ func TestTailRecSumList(t *testing.T) {
|
||||
|
||||
// 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]] {
|
||||
collatzStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 1 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return R.Of(E.Left[int](n / 2))
|
||||
return R.Of(TR.Bounce[int](n / 2))
|
||||
}
|
||||
return R.Of(E.Left[int](3*n + 1))
|
||||
return R.Of(TR.Bounce[int](3*n + 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,12 +170,12 @@ func TestTailRecGCD(t *testing.T) {
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
gcdStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.b == 0 {
|
||||
return R.Of(E.Right[State](state.a))
|
||||
return R.Of(TR.Land[State](state.a))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.b, state.a % state.b}))
|
||||
return R.Of(TR.Bounce[int](State{state.b, state.a % state.b}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,15 +189,15 @@ func TestTailRecGCD(t *testing.T) {
|
||||
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]] {
|
||||
errorStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n == 5 {
|
||||
return R.Left[E.Either[int, int]](expectedErr)
|
||||
return R.Left[TR.Trampoline[int, int]](expectedErr)
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,13 +215,13 @@ func TestTailRecContextCancellationImmediate(t *testing.T) {
|
||||
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]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
stepExecuted = true
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,17 +240,17 @@ 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]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
// Cancel after 3 iterations
|
||||
if executionCount == 3 {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,15 +270,15 @@ func TestTailRecContextWithTimeout(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
executionCount := 0
|
||||
slowStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
slowStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
// Simulate slow computation
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,12 +298,12 @@ func TestTailRecContextWithCause(t *testing.T) {
|
||||
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]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,16 +322,16 @@ func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
|
||||
executionCount := 0
|
||||
maxExecutions := 5
|
||||
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
if executionCount == maxExecutions {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,13 +351,13 @@ 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]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,12 +376,12 @@ func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
target int
|
||||
}
|
||||
|
||||
powerStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
powerStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.exponent >= state.target {
|
||||
return R.Of(E.Right[State](state.result))
|
||||
return R.Of(TR.Land[State](state.result))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.exponent + 1, state.result * 2, state.target}))
|
||||
return R.Of(TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,15 +399,15 @@ func TestTailRecFindInRange(t *testing.T) {
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.current >= state.max {
|
||||
return R.Of(E.Right[State](-1)) // Not found
|
||||
return R.Of(TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return R.Of(E.Right[State](state.current)) // Found
|
||||
return R.Of(TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
|
||||
return R.Of(TR.Bounce[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,15 +425,15 @@ func TestTailRecFindNotInRange(t *testing.T) {
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.current >= state.max {
|
||||
return R.Of(E.Right[State](-1)) // Not found
|
||||
return R.Of(TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return R.Of(E.Right[State](state.current)) // Found
|
||||
return R.Of(TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
|
||||
return R.Of(TR.Bounce[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,13 +450,13 @@ func TestTailRecWithContextValue(t *testing.T) {
|
||||
|
||||
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]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
multiplier := ctx.Value(multiplierKey).(int)
|
||||
return R.Of(E.Right[int](n * multiplier))
|
||||
return R.Of(TR.Land[int](n * multiplier))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,11 +475,11 @@ func TestTailRecComplexState(t *testing.T) {
|
||||
completed bool
|
||||
}
|
||||
|
||||
complexStep := func(state ComplexState) ReaderResult[E.Either[ComplexState, string]] {
|
||||
return func(ctx context.Context) Result[E.Either[ComplexState, string]] {
|
||||
complexStep := func(state ComplexState) ReaderResult[TR.Trampoline[ComplexState, string]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[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))
|
||||
return R.Of(TR.Land[ComplexState](result))
|
||||
}
|
||||
newState := ComplexState{
|
||||
counter: state.counter - 1,
|
||||
@@ -486,7 +487,7 @@ func TestTailRecComplexState(t *testing.T) {
|
||||
product: state.product * state.counter,
|
||||
completed: state.counter == 1,
|
||||
}
|
||||
return R.Of(E.Left[string](newState))
|
||||
return R.Of(TR.Bounce[string](newState))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,5 +496,3 @@ func TestTailRecComplexState(t *testing.T) {
|
||||
|
||||
assert.Equal(t, R.Of("sum=15, product=120"), result)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
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,7 +13,31 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
|
||||
// Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error.
|
||||
//
|
||||
// # Pure vs Effectful Functions
|
||||
//
|
||||
// This package distinguishes between pure (side-effect free) and effectful (side-effectful) functions:
|
||||
//
|
||||
// EFFECTFUL FUNCTIONS (depend on context.Context):
|
||||
// - ReaderResult[A]: func(context.Context) (A, error) - Effectful computation that needs context
|
||||
// - These functions are effectful because context.Context is effectful (can be cancelled, has deadlines, carries values)
|
||||
// - Use for: operations that need cancellation, timeouts, context values, or any context-dependent behavior
|
||||
// - Examples: database queries, HTTP requests, operations that respect cancellation
|
||||
//
|
||||
// PURE FUNCTIONS (side-effect free):
|
||||
// - func(State) (Value, error) - Pure computation that only depends on state, not context
|
||||
// - func(State) Value - Pure transformation without errors
|
||||
// - These functions are pure because they only read from their input state and don't depend on external context
|
||||
// - Use for: parsing, validation, calculations, data transformations that don't need context
|
||||
// - Examples: JSON parsing, input validation, mathematical computations
|
||||
//
|
||||
// The package provides different bind operations for each:
|
||||
// - Bind: For effectful ReaderResult computations (State -> ReaderResult[Value])
|
||||
// - BindResultK: For pure functions with errors (State -> (Value, error))
|
||||
// - Let: For pure functions without errors (State -> Value)
|
||||
// - BindReaderK: For context-dependent pure functions (State -> Reader[Context, Value])
|
||||
// - BindEitherK: For pure Result/Either values (State -> Result[Value])
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
@@ -21,10 +45,13 @@ import (
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -35,7 +62,10 @@ type (
|
||||
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
|
||||
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
)
|
||||
|
||||
@@ -23,14 +23,15 @@ import (
|
||||
IOR "github.com/IBM/fp-go/v2/ioresult"
|
||||
L "github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
R "github.com/IBM/fp-go/v2/record"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
|
||||
"sync"
|
||||
)
|
||||
|
||||
func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] {
|
||||
return T.MakeTuple2(p.Provides().Id(), p.Factory())
|
||||
func providerToEntry(p Provider) Entry[string, ProviderFactory] {
|
||||
return pair.MakePair(p.Provides().Id(), p.Factory())
|
||||
}
|
||||
|
||||
func itemProviderToMap(p Provider) map[string][]ProviderFactory {
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/record"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[T any] = option.Option[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Option[T any] = option.Option[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Entry[K comparable, V any] = record.Entry[K, V]
|
||||
)
|
||||
|
||||
@@ -103,11 +103,11 @@ func (t *token[T]) Unerase(val any) Result[T] {
|
||||
func (t *token[T]) ProviderFactory() Option[DIE.ProviderFactory] {
|
||||
return t.base.providerFactory
|
||||
}
|
||||
func makeTokenBase(name string, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
|
||||
func makeTokenBase(name, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
|
||||
return &tokenBase{name, id, typ, providerFactory}
|
||||
}
|
||||
|
||||
func makeToken[T any](name string, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
|
||||
func makeToken[T any](name, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
|
||||
return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/record"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[T any] = option.Option[T]
|
||||
Result[T any] = result.Result[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Option[T any] = option.Option[T]
|
||||
Result[T any] = result.Result[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Entry[K comparable, V any] = record.Entry[K, V]
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
|
||||
// Example:
|
||||
//
|
||||
// validate := func(i int, s string) either.Either[error, string] {
|
||||
// if len(s) > 0 {
|
||||
// if S.IsNonEmpty(s) {
|
||||
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
|
||||
// }
|
||||
// return either.Left[string](fmt.Errorf("empty at index %d", i))
|
||||
@@ -105,7 +105,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, E, A, B any](f func(int, A) Eithe
|
||||
// Example:
|
||||
//
|
||||
// validate := func(i int, s string) either.Either[error, string] {
|
||||
// if len(s) > 0 {
|
||||
// if S.IsNonEmpty(s) {
|
||||
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
|
||||
// }
|
||||
// return either.Left[string](fmt.Errorf("empty at index %d", i))
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
|
||||
Either[E, A any] struct {
|
||||
@@ -28,28 +24,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// String prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) String() string {
|
||||
if !s.isLeft {
|
||||
return fmt.Sprintf("Right[%T](%v)", s.r, s.r)
|
||||
}
|
||||
return fmt.Sprintf("Left[%T](%v)", s.l, s.l)
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) Format(f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 's':
|
||||
fmt.Fprint(f, s.String())
|
||||
default:
|
||||
fmt.Fprint(f, s.String())
|
||||
}
|
||||
}
|
||||
|
||||
// IsLeft tests if the Either is a Left value.
|
||||
// Rather use [Fold] or [MonadFold] if you need to access the values.
|
||||
// Inverse is [IsRight].
|
||||
|
||||
@@ -34,7 +34,7 @@ func Curry0[R any](f func() (R, error)) func() Either[error, R] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
||||
// parse := strconv.Atoi
|
||||
// curried := either.Curry1(parse)
|
||||
// result := curried("42") // Right(42)
|
||||
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] {
|
||||
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -305,7 +306,7 @@ func TestTraverseArray(t *testing.T) {
|
||||
// Test TraverseArrayWithIndex
|
||||
func TestTraverseArrayWithIndex(t *testing.T) {
|
||||
validate := func(i int, s string) Either[error, string] {
|
||||
if len(s) > 0 {
|
||||
if S.IsNonEmpty(s) {
|
||||
return Right[error](fmt.Sprintf("%d:%s", i, s))
|
||||
}
|
||||
return Left[string](fmt.Errorf("empty at index %d", i))
|
||||
@@ -334,7 +335,7 @@ func TestTraverseRecord(t *testing.T) {
|
||||
// Test TraverseRecordWithIndex
|
||||
func TestTraverseRecordWithIndex(t *testing.T) {
|
||||
validate := func(k string, v string) Either[error, string] {
|
||||
if len(v) > 0 {
|
||||
if S.IsNonEmpty(v) {
|
||||
return Right[error](k + ":" + v)
|
||||
}
|
||||
return Left[string](fmt.Errorf("empty value for key %s", k))
|
||||
@@ -373,7 +374,7 @@ func TestCurry0(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCurry1(t *testing.T) {
|
||||
parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
||||
parse := strconv.Atoi
|
||||
curried := Curry1(parse)
|
||||
result := curried("42")
|
||||
assert.Equal(t, Right[error](42), result)
|
||||
@@ -645,7 +646,7 @@ func TestAltSemigroup(t *testing.T) {
|
||||
|
||||
// Test AlternativeMonoid
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
intAdd := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
intAdd := N.MonoidSum[int]()
|
||||
m := AlternativeMonoid[error](intAdd)
|
||||
|
||||
result := m.Concat(Right[error](1), Right[error](2))
|
||||
|
||||
149
v2/either/examples_format_test.go
Normal file
149
v2/either/examples_format_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// ExampleEither_String demonstrates the fmt.Stringer interface implementation.
|
||||
func ExampleEither_String() {
|
||||
right := E.Right[error](42)
|
||||
left := E.Left[int](errors.New("something went wrong"))
|
||||
|
||||
fmt.Println(right.String())
|
||||
fmt.Println(left.String())
|
||||
|
||||
// Output:
|
||||
// Right[int](42)
|
||||
// Left[*errors.errorString](something went wrong)
|
||||
}
|
||||
|
||||
// ExampleEither_GoString demonstrates the fmt.GoStringer interface implementation.
|
||||
func ExampleEither_GoString() {
|
||||
right := E.Right[error](42)
|
||||
left := E.Left[int](errors.New("error"))
|
||||
|
||||
fmt.Printf("%#v\n", right)
|
||||
fmt.Printf("%#v\n", left)
|
||||
|
||||
// Output:
|
||||
// either.Right[error](42)
|
||||
// either.Left[int](&errors.errorString{s:"error"})
|
||||
}
|
||||
|
||||
// ExampleEither_Format demonstrates the fmt.Formatter interface implementation.
|
||||
func ExampleEither_Format() {
|
||||
result := E.Right[error](42)
|
||||
|
||||
// Different format verbs
|
||||
fmt.Printf("%%s: %s\n", result)
|
||||
fmt.Printf("%%v: %v\n", result)
|
||||
fmt.Printf("%%+v: %+v\n", result)
|
||||
fmt.Printf("%%#v: %#v\n", result)
|
||||
|
||||
// Output:
|
||||
// %s: Right[int](42)
|
||||
// %v: Right[int](42)
|
||||
// %+v: Right[int](42)
|
||||
// %#v: either.Right[error](42)
|
||||
}
|
||||
|
||||
// ExampleEither_LogValue demonstrates the slog.LogValuer interface implementation.
|
||||
func ExampleEither_LogValue() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Remove time for consistent output
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Right value
|
||||
rightResult := E.Right[error](42)
|
||||
logger.Info("computation succeeded", "result", rightResult)
|
||||
|
||||
// Left value
|
||||
leftResult := E.Left[int](errors.New("computation failed"))
|
||||
logger.Error("computation failed", "result", leftResult)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg="computation succeeded" result.right=42
|
||||
// level=ERROR msg="computation failed" result.left="computation failed"
|
||||
}
|
||||
|
||||
// ExampleEither_formatting_comparison demonstrates different formatting options.
|
||||
func ExampleEither_formatting_comparison() {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
result := E.Right[error](user)
|
||||
|
||||
fmt.Printf("String(): %s\n", result.String())
|
||||
fmt.Printf("GoString(): %s\n", result.GoString())
|
||||
fmt.Printf("%%v: %v\n", result)
|
||||
fmt.Printf("%%#v: %#v\n", result)
|
||||
|
||||
// Output:
|
||||
// String(): Right[either_test.User]({123 Alice})
|
||||
// GoString(): either.Right[error](either_test.User{ID:123, Name:"Alice"})
|
||||
// %v: Right[either_test.User]({123 Alice})
|
||||
// %#v: either.Right[error](either_test.User{ID:123, Name:"Alice"})
|
||||
}
|
||||
|
||||
// ExampleEither_LogValue_structured demonstrates structured logging with Either.
|
||||
func ExampleEither_LogValue_structured() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Simulate a computation pipeline
|
||||
compute := func(x int) E.Either[error, int] {
|
||||
if x < 0 {
|
||||
return E.Left[int](errors.New("negative input"))
|
||||
}
|
||||
return E.Right[error](x * 2)
|
||||
}
|
||||
|
||||
// Log successful computation
|
||||
result1 := compute(21)
|
||||
logger.Info("computation", "input", 21, "output", result1)
|
||||
|
||||
// Log failed computation
|
||||
result2 := compute(-5)
|
||||
logger.Error("computation", "input", -5, "output", result2)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg=computation input=21 output.right=42
|
||||
// level=ERROR msg=computation input=-5 output.left="negative input"
|
||||
}
|
||||
103
v2/either/format.go
Normal file
103
v2/either/format.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
const (
|
||||
leftGoTemplate = "either.Left[%s](%#v)"
|
||||
rightGoTemplate = "either.Right[%s](%#v)"
|
||||
|
||||
leftFmtTemplate = "Left[%T](%v)"
|
||||
rightFmtTemplate = "Right[%T](%v)"
|
||||
)
|
||||
|
||||
func goString(template string, other, v any) string {
|
||||
return fmt.Sprintf(template, formatting.TypeInfo(other), v)
|
||||
}
|
||||
|
||||
// String prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) String() string {
|
||||
if !s.isLeft {
|
||||
return fmt.Sprintf(rightFmtTemplate, s.r, s.r)
|
||||
}
|
||||
return fmt.Sprintf(leftFmtTemplate, s.l, s.l)
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Either.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// e := either.Right[error](42)
|
||||
// fmt.Printf("%s", e) // "Right[int](42)"
|
||||
// fmt.Printf("%v", e) // "Right[int](42)"
|
||||
// fmt.Printf("%#v", e) // "either.Right[error](42)"
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(s, f, c)
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Either.
|
||||
// Returns a Go-syntax representation of the Either value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// either.Right[error](42).GoString() // "either.Right[error](42)"
|
||||
// either.Left[int](errors.New("fail")).GoString() // "either.Left[int](error)"
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) GoString() string {
|
||||
if !s.isLeft {
|
||||
return goString(rightGoTemplate, new(E), s.r)
|
||||
}
|
||||
return goString(leftGoTemplate, new(A), s.l)
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Either.
|
||||
// Returns a slog.Value that represents the Either for structured logging.
|
||||
// Returns a group value with "right" key for Right values and "left" key for Left values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// result := either.Right[error](42)
|
||||
// logger.Info("result", "value", result)
|
||||
// // Logs: {"msg":"result","value":{"right":42}}
|
||||
//
|
||||
// err := either.Left[int](errors.New("failed"))
|
||||
// logger.Error("error", "value", err)
|
||||
// // Logs: {"msg":"error","value":{"left":"failed"}}
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) LogValue() slog.Value {
|
||||
if !s.isLeft {
|
||||
return slog.GroupValue(slog.Any("right", s.r))
|
||||
}
|
||||
return slog.GroupValue(slog.Any("left", s.l))
|
||||
}
|
||||
311
v2/either/format_test.go
Normal file
311
v2/either/format_test.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := e.String()
|
||||
assert.Equal(t, "Right[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
result := e.String()
|
||||
assert.Contains(t, result, "Left[*errors.errorString]")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Right with string", func(t *testing.T) {
|
||||
e := Right[error]("hello")
|
||||
result := e.String()
|
||||
assert.Equal(t, "Right[string](hello)", result)
|
||||
})
|
||||
|
||||
t.Run("Left with string", func(t *testing.T) {
|
||||
e := Left[int]("error message")
|
||||
result := e.String()
|
||||
assert.Equal(t, "Left[string](error message)", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoString(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Right")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Left")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Right with struct", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
e := Right[error](TestStruct{Name: "Alice", Age: 30})
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Right")
|
||||
assert.Contains(t, result, "Alice")
|
||||
assert.Contains(t, result, "30")
|
||||
})
|
||||
|
||||
t.Run("Left with custom error", func(t *testing.T) {
|
||||
e := Left[string]("custom error")
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Left")
|
||||
assert.Contains(t, result, "custom error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatInterface(t *testing.T) {
|
||||
t.Run("Right value with %s", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%s", e)
|
||||
assert.Equal(t, "Right[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("Left value with %s", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
result := fmt.Sprintf("%s", e)
|
||||
assert.Contains(t, result, "Left")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Right value with %v", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%v", e)
|
||||
assert.Equal(t, "Right[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("Left value with %v", func(t *testing.T) {
|
||||
e := Left[int]("error")
|
||||
result := fmt.Sprintf("%v", e)
|
||||
assert.Equal(t, "Left[string](error)", result)
|
||||
})
|
||||
|
||||
t.Run("Right value with %+v", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%+v", e)
|
||||
assert.Contains(t, result, "Right")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Right value with %#v (GoString)", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%#v", e)
|
||||
assert.Contains(t, result, "either.Right")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Left value with %#v (GoString)", func(t *testing.T) {
|
||||
e := Left[int]("error")
|
||||
result := fmt.Sprintf("%#v", e)
|
||||
assert.Contains(t, result, "either.Left")
|
||||
assert.Contains(t, result, "error")
|
||||
})
|
||||
|
||||
t.Run("Right value with %q", func(t *testing.T) {
|
||||
e := Right[error]("hello")
|
||||
result := fmt.Sprintf("%q", e)
|
||||
// Should use String() representation
|
||||
assert.Contains(t, result, "Right")
|
||||
})
|
||||
|
||||
t.Run("Right value with %T", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%T", e)
|
||||
assert.Contains(t, result, "either.Either")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogValue(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "right", attrs[0].Key)
|
||||
assert.Equal(t, int64(42), attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "left", attrs[0].Key)
|
||||
assert.NotNil(t, attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Right with string", func(t *testing.T) {
|
||||
e := Right[error]("success")
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "right", attrs[0].Key)
|
||||
assert.Equal(t, "success", attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Left with string", func(t *testing.T) {
|
||||
e := Left[int]("error message")
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "left", attrs[0].Key)
|
||||
assert.Equal(t, "error message", attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Integration with slog - Right", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
e := Right[error](42)
|
||||
logger.Info("test message", "result", e)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "test message")
|
||||
assert.Contains(t, output, "result")
|
||||
assert.Contains(t, output, "right")
|
||||
assert.Contains(t, output, "42")
|
||||
})
|
||||
|
||||
t.Run("Integration with slog - Left", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
e := Left[int]("error occurred")
|
||||
logger.Info("test message", "result", e)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "test message")
|
||||
assert.Contains(t, output, "result")
|
||||
assert.Contains(t, output, "left")
|
||||
assert.Contains(t, output, "error occurred")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatComprehensive(t *testing.T) {
|
||||
t.Run("All format verbs for Right", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"Right", "42"}},
|
||||
{"%v", []string{"Right", "42"}},
|
||||
{"%+v", []string{"Right", "42"}},
|
||||
{"%#v", []string{"either.Right", "42"}},
|
||||
{"%T", []string{"either.Either"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, e)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All format verbs for Left", func(t *testing.T) {
|
||||
e := Left[int]("error")
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"Left", "error"}},
|
||||
{"%v", []string{"Left", "error"}},
|
||||
{"%+v", []string{"Left", "error"}},
|
||||
{"%#v", []string{"either.Left", "error"}},
|
||||
{"%T", []string{"either.Either"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, e)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterfaceImplementations(t *testing.T) {
|
||||
t.Run("fmt.Stringer interface", func(t *testing.T) {
|
||||
var _ fmt.Stringer = Right[error](42)
|
||||
var _ fmt.Stringer = Left[int](errors.New("error"))
|
||||
})
|
||||
|
||||
t.Run("fmt.GoStringer interface", func(t *testing.T) {
|
||||
var _ fmt.GoStringer = Right[error](42)
|
||||
var _ fmt.GoStringer = Left[int](errors.New("error"))
|
||||
})
|
||||
|
||||
t.Run("fmt.Formatter interface", func(t *testing.T) {
|
||||
var _ fmt.Formatter = Right[error](42)
|
||||
var _ fmt.Formatter = Left[int](errors.New("error"))
|
||||
})
|
||||
|
||||
t.Run("slog.LogValuer interface", func(t *testing.T) {
|
||||
var _ slog.LogValuer = Right[error](42)
|
||||
var _ slog.LogValuer = Left[int](errors.New("error"))
|
||||
})
|
||||
}
|
||||
@@ -15,8 +15,12 @@
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
|
||||
return func(a A) Either[E, B] {
|
||||
current := f(a)
|
||||
for {
|
||||
@@ -24,11 +28,10 @@ func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
|
||||
if IsLeft(current) {
|
||||
return Left[B](e)
|
||||
}
|
||||
b, a := Unwrap(rec)
|
||||
if IsRight(rec) {
|
||||
return Right[E](b)
|
||||
if rec.Landed {
|
||||
return Right[E](rec.Land)
|
||||
}
|
||||
current = f(a)
|
||||
current = f(rec.Bounce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import (
|
||||
// increment := N.Add(1)
|
||||
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
|
||||
// // result(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
|
||||
func MonadAp[A any](fab, fa Endomorphism[A]) Endomorphism[A] {
|
||||
return MonadCompose(fab, fa)
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
|
||||
// // Compare with MonadCompose which executes RIGHT-TO-LEFT:
|
||||
// composed := endomorphism.MonadCompose(increment, double)
|
||||
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
|
||||
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
||||
func MonadChain[A any](ma, f Endomorphism[A]) Endomorphism[A] {
|
||||
return function.Flow2(ma, f)
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
||||
// log := func(x int) int { fmt.Println(x); return x }
|
||||
// chained := endomorphism.MonadChainFirst(double, log)
|
||||
// result := chained(5) // Prints 10, returns 10
|
||||
func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
||||
func MonadChainFirst[A any](ma, f Endomorphism[A]) Endomorphism[A] {
|
||||
return func(a A) A {
|
||||
result := ma(a)
|
||||
f(result) // Apply f for its effect
|
||||
|
||||
@@ -72,9 +72,7 @@ func TestFromStrictEquals(t *testing.T) {
|
||||
|
||||
func TestFromEquals(t *testing.T) {
|
||||
t.Run("case-insensitive string equality", func(t *testing.T) {
|
||||
caseInsensitiveEq := FromEquals(func(a, b string) bool {
|
||||
return strings.EqualFold(a, b)
|
||||
})
|
||||
caseInsensitiveEq := FromEquals(strings.EqualFold)
|
||||
|
||||
assert.True(t, caseInsensitiveEq.Equals("hello", "HELLO"))
|
||||
assert.True(t, caseInsensitiveEq.Equals("Hello", "hello"))
|
||||
@@ -243,9 +241,7 @@ func TestContramap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("case-insensitive name comparison", func(t *testing.T) {
|
||||
caseInsensitiveEq := FromEquals(func(a, b string) bool {
|
||||
return strings.EqualFold(a, b)
|
||||
})
|
||||
caseInsensitiveEq := FromEquals(strings.EqualFold)
|
||||
|
||||
personEqByNameCI := Contramap(func(p Person) string {
|
||||
return p.Name
|
||||
|
||||
@@ -51,7 +51,7 @@ package function
|
||||
// )
|
||||
// result := classify(5) // "positive"
|
||||
// result2 := classify(-3) // "non-positive"
|
||||
func Ternary[A, B any](pred func(A) bool, onTrue func(A) B, onFalse func(A) B) func(A) B {
|
||||
func Ternary[A, B any](pred func(A) bool, onTrue, onFalse func(A) B) func(A) B {
|
||||
return func(a A) B {
|
||||
if pred(a) {
|
||||
return onTrue(a)
|
||||
|
||||
@@ -246,7 +246,7 @@ func (builder *Builder) GetTargetURL() Result[string] {
|
||||
parseQuery,
|
||||
result.Map(F.Flow2(
|
||||
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
|
||||
(url.Values).Encode,
|
||||
url.Values.Encode,
|
||||
)),
|
||||
),
|
||||
),
|
||||
@@ -351,13 +351,13 @@ func Header(name string) Lens[*Builder, Option[string]] {
|
||||
LZ.Map(delHeader(name)),
|
||||
)
|
||||
|
||||
return L.MakeLens(get, func(b *Builder, value Option[string]) *Builder {
|
||||
return L.MakeLensWithName(get, func(b *Builder, value Option[string]) *Builder {
|
||||
cpy := b.clone()
|
||||
return F.Pipe1(
|
||||
value,
|
||||
O.Fold(del(cpy), set(cpy)),
|
||||
)
|
||||
})
|
||||
}, fmt.Sprintf("HttpHeader[%s]", name))
|
||||
}
|
||||
|
||||
// WithHeader creates a [Endomorphism] for a certain header
|
||||
|
||||
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
@@ -20,8 +20,8 @@ import (
|
||||
|
||||
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"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
@@ -41,33 +41,6 @@ import (
|
||||
// Returns:
|
||||
// - A ReaderResult[S] containing the initial state
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// Posts []Post
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do(State{}),
|
||||
// readerresult.Bind(
|
||||
// func(u User) func(State) State {
|
||||
// return func(s State) State { s.User = u; 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)
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
@@ -75,7 +48,15 @@ func Do[S any](
|
||||
return RR.Do[context.Context](empty)
|
||||
}
|
||||
|
||||
// Bind sequences a ReaderResult computation and updates the state with its result.
|
||||
// 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.
|
||||
@@ -87,22 +68,11 @@ func Do[S any](
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A function that takes the computation result and returns a state updater
|
||||
// - f: A Kleisli arrow that produces the next computation based on current state
|
||||
// - f: A Kleisli arrow that produces the next effectful computation based on current state
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// 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(s.UserID)
|
||||
// },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
@@ -112,11 +82,20 @@ func Bind[S1, S2, T any](
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
setter,
|
||||
f,
|
||||
WithContextK(f),
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a state.
|
||||
// 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
|
||||
@@ -134,17 +113,6 @@ func Bind[S1, S2, T any](
|
||||
// Returns:
|
||||
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readerresult.Let(
|
||||
// func(fullName string) func(State) State {
|
||||
// return func(s State) State { s.FullName = fullName; return s }
|
||||
// },
|
||||
// func(s State) string {
|
||||
// return s.FirstName + " " + s.LastName
|
||||
// },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
@@ -154,6 +122,7 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -170,15 +139,6 @@ func Let[S1, S2, T any](
|
||||
// Returns:
|
||||
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// readerresult.LetTo(
|
||||
// func(status string) func(State) State {
|
||||
// return func(s State) State { s.Status = status; return s }
|
||||
// },
|
||||
// "active",
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
@@ -202,19 +162,6 @@ func LetTo[S1, S2, T any](
|
||||
// Returns:
|
||||
// - An Operator that transforms ReaderResult[T] to ReaderResult[S1]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// User User
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// getUser(42),
|
||||
// readerresult.BindTo(func(u User) State {
|
||||
// return State{User: u}
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
@@ -222,46 +169,174 @@ func BindTo[S1, T any](
|
||||
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 RR.ApS[context.Context](setter, fa)
|
||||
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 L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa ReaderResult[T],
|
||||
) Operator[S, S] {
|
||||
return ApSL(lens, 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.
|
||||
//
|
||||
// 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 L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return RR.BindL(lens, f)
|
||||
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 L.Lens[S, T],
|
||||
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 L.Lens[S, T],
|
||||
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,
|
||||
@@ -270,6 +345,12 @@ func BindReaderK[S1, S2, T any](
|
||||
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,
|
||||
@@ -278,6 +359,14 @@ func BindEitherK[S1, S2, T any](
|
||||
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,
|
||||
@@ -286,6 +375,11 @@ func BindResultK[S1, S2, T any](
|
||||
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](
|
||||
@@ -294,6 +388,11 @@ func BindToReader[
|
||||
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](
|
||||
@@ -302,6 +401,11 @@ func BindToEither[
|
||||
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](
|
||||
@@ -310,6 +414,11 @@ func BindToResult[
|
||||
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](
|
||||
@@ -319,6 +428,11 @@ func ApReaderS[
|
||||
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](
|
||||
@@ -327,6 +441,11 @@ func ApResultS[
|
||||
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](
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -130,6 +130,8 @@ import (
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bracket[
|
||||
A, B, ANY any](
|
||||
|
||||
@@ -137,7 +139,7 @@ func Bracket[
|
||||
use Kleisli[A, B],
|
||||
release func(A, B, error) ReaderResult[ANY],
|
||||
) ReaderResult[B] {
|
||||
return RR.Bracket(acquire, use, release)
|
||||
return RR.Bracket(acquire, WithContextK(use), release)
|
||||
}
|
||||
|
||||
// WithResource creates a higher-order function for resource management with automatic cleanup.
|
||||
@@ -251,19 +253,21 @@ func Bracket[
|
||||
// }),
|
||||
// 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 RR.WithResource[B](onCreate, onRelease)
|
||||
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[any] {
|
||||
return func(_ context.Context) (any, error) {
|
||||
return nil, a.Close()
|
||||
func onClose[A io.Closer](a A) ReaderResult[struct{}] {
|
||||
return func(_ context.Context) (struct{}, error) {
|
||||
return struct{}{}, a.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +402,8 @@ func onClose[A io.Closer](a A) ReaderResult[any] {
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
@@ -13,40 +13,73 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package readerresult provides a ReaderResult monad that combines the Reader and Result monads.
|
||||
// Package readerresult provides a ReaderResult monad specialized for context.Context.
|
||||
//
|
||||
// A ReaderResult[R, A] represents a computation that:
|
||||
// - Depends on an environment of type R (Reader aspect)
|
||||
// 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
|
||||
//
|
||||
// This is equivalent to Reader[R, Result[A]] or Reader[R, Either[error, A]].
|
||||
// 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. Dependency injection with error handling - pass configuration/services through
|
||||
// computations that may fail
|
||||
// 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 dependencies by changing the environment value
|
||||
//
|
||||
// # Basic Example
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // Function that needs config and may fail
|
||||
// func getUser(id int) readerresult.ReaderResult[Config, User] {
|
||||
// return readerresult.Asks(func(cfg Config) result.Result[User] {
|
||||
// // Use cfg.DatabaseURL to fetch user
|
||||
// return result.Of(user)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Execute by providing the config
|
||||
// cfg := Config{DatabaseURL: "postgres://..."}
|
||||
// user, err := getUser(42)(cfg) // Returns (User, error)
|
||||
// 3. Testing - easily mock context-dependent operations
|
||||
// 4. HTTP handlers - chain request processing operations with proper context propagation
|
||||
//
|
||||
// # Composition
|
||||
//
|
||||
@@ -65,12 +98,12 @@
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// readerresult.Do[Config](State{}),
|
||||
// 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[Config, User] {
|
||||
// func(s State) readerresult.ReaderResult[User] {
|
||||
// return getUser(42)
|
||||
// },
|
||||
// ),
|
||||
@@ -78,79 +111,75 @@
|
||||
// func(posts []Post) func(State) State {
|
||||
// return func(s State) State { s.Posts = posts; return s }
|
||||
// },
|
||||
// func(s State) readerresult.ReaderResult[Config, []Post] {
|
||||
// func(s State) readerresult.ReaderResult[[]Post] {
|
||||
// return getPosts(s.User.ID)
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
// # Object-Oriented Patterns with Curry Functions
|
||||
// # Currying Functions with Context
|
||||
//
|
||||
// The Curry functions enable an interesting pattern where you can treat the Reader context (R)
|
||||
// as an object instance, effectively creating method-like functions that compose functionally.
|
||||
// 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(R, T1, T2) (A, error), the context R 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.
|
||||
// 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 R is the last curried argument:
|
||||
// Why context.Context is the last curried argument:
|
||||
//
|
||||
// - In Go, context conventionally comes first: func(ctx Context, params...) (Result, error)
|
||||
// - In curried form: Curry2(f)(param1)(param2) returns ReaderResult[R, A]
|
||||
// - The ReaderResult is then applied to R: Curry2(f)(param1)(param2)(ctx)
|
||||
// - This allows partial application of business parameters before providing the context/object
|
||||
// - 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
|
||||
//
|
||||
// Object-Oriented Example:
|
||||
// Example with database operations:
|
||||
//
|
||||
// // A service struct that acts as the Reader context
|
||||
// type UserService struct {
|
||||
// db *sql.DB
|
||||
// cache Cache
|
||||
// // 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
|
||||
// }
|
||||
//
|
||||
// // A method-like function following Go conventions (context first)
|
||||
// func (s *UserService) GetUserByID(ctx context.Context, id int) (User, error) {
|
||||
// // Use s.db and s.cache...
|
||||
// }
|
||||
//
|
||||
// func (s *UserService) UpdateUser(ctx context.Context, id int, name string) (User, error) {
|
||||
// // Use s.db and s.cache...
|
||||
// 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.Curry1((*UserService).GetUserByID)
|
||||
// updateUser := readerresult.Curry2((*UserService).UpdateUser)
|
||||
//
|
||||
// // Now compose operations that will be bound to a UserService instance
|
||||
// type Context struct {
|
||||
// Svc *UserService
|
||||
// }
|
||||
// getUser := readerresult.Curry2(fetchUser)
|
||||
// updateUserName := readerresult.Curry3(updateUser)
|
||||
//
|
||||
// // Compose operations with partial application
|
||||
// pipeline := F.Pipe2(
|
||||
// getUser(42), // ReaderResult[Context, User]
|
||||
// readerresult.Chain(func(user User) readerresult.ReaderResult[Context, User] {
|
||||
// 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 updateUser(user.ID)(newName)
|
||||
// return updateUserName(db)(user.ID)(newName) // Waiting for ctx
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// // Execute by providing the service instance as context
|
||||
// svc := &UserService{db: db, cache: cache}
|
||||
// ctx := Context{Svc: svc}
|
||||
// // 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(42)
|
||||
// 2. This returns a ReaderResult that waits for the context
|
||||
// 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/object is provided to execute everything: pipeline(ctx)
|
||||
// 4. Finally, the context is provided to execute everything: pipeline(ctx)
|
||||
//
|
||||
// This pattern is particularly useful for:
|
||||
// - Creating reusable operation pipelines independent of service instances
|
||||
// - Testing with mock service instances
|
||||
// - Dependency injection in a functional style
|
||||
// - Composing operations that share the same service context
|
||||
// - 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
|
||||
//
|
||||
@@ -166,10 +195,10 @@
|
||||
//
|
||||
// ReaderResult is related to several other monads in this library:
|
||||
//
|
||||
// - Reader[R, A] - ReaderResult without error handling
|
||||
// - Result[A] (Either[error, A]) - error handling without environment
|
||||
// - ReaderEither[R, E, A] - like ReaderResult but with custom error type E
|
||||
// - IOResult[A] - like ReaderResult but with no environment (IO with errors)
|
||||
// - 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
|
||||
//
|
||||
|
||||
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>
|
||||
}
|
||||
@@ -56,8 +56,8 @@ import (
|
||||
// result, err := sequenced(ctx)(config)
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) RR.Kleisli[context.Context, R, A] {
|
||||
return RR.SequenceReader(ma)
|
||||
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.
|
||||
@@ -102,6 +102,6 @@ func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) RR.Kleisli[context.
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderResult[A]) RR.Kleisli[context.Context, R, B] {
|
||||
) func(ReaderResult[A]) Kleisli[R, B] {
|
||||
return RR.TraverseReader[context.Context](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
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"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"
|
||||
@@ -43,12 +45,6 @@ import (
|
||||
// Returns:
|
||||
// - A ReaderResult[A] that ignores the context and returns the Result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := result.Of(42)
|
||||
// rr := readerresult.FromEither(result)
|
||||
// value, err := rr(ctx) // Returns (42, nil)
|
||||
//
|
||||
//go:inline
|
||||
func FromEither[A any](e Result[A]) ReaderResult[A] {
|
||||
return RR.FromEither[context.Context](e)
|
||||
@@ -69,14 +65,6 @@ func FromEither[A any](e Result[A]) ReaderResult[A] {
|
||||
// Returns:
|
||||
// - A ReaderResult[A] that returns the given value and error
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.FromResult(42, nil)
|
||||
// value, err := rr(ctx) // Returns (42, nil)
|
||||
//
|
||||
// rr2 := readerresult.FromResult(0, errors.New("failed"))
|
||||
// value, err := rr2(ctx) // Returns (0, error)
|
||||
//
|
||||
//go:inline
|
||||
func FromResult[A any](a A, err error) ReaderResult[A] {
|
||||
return RR.FromResult[context.Context](a, err)
|
||||
@@ -106,11 +94,6 @@ func LeftReader[A, R any](l Reader[context.Context, error]) ReaderResult[A] {
|
||||
// Returns:
|
||||
// - A ReaderResult[A] that always fails with the given error
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Left[int](errors.New("failed"))
|
||||
// value, err := rr(ctx) // Returns (0, error)
|
||||
//
|
||||
//go:inline
|
||||
func Left[A any](err error) ReaderResult[A] {
|
||||
return RR.Left[context.Context, A](err)
|
||||
@@ -130,14 +113,9 @@ func Left[A any](err error) ReaderResult[A] {
|
||||
// Returns:
|
||||
// - A ReaderResult[A] that always succeeds with the given value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Right(42)
|
||||
// value, err := rr(ctx) // Returns (42, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Right[A any](a A) ReaderResult[A] {
|
||||
return RR.Right[context.Context, A](a)
|
||||
return RR.Right[context.Context](a)
|
||||
}
|
||||
|
||||
// FromReader lifts a Reader into a ReaderResult that always succeeds.
|
||||
@@ -154,19 +132,25 @@ func Right[A any](a A) ReaderResult[A] {
|
||||
// Returns:
|
||||
// - A ReaderResult[A] that executes the Reader and always succeeds
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getConfig := func(ctx context.Context) Config {
|
||||
// return Config{Port: 8080}
|
||||
// }
|
||||
// rr := readerresult.FromReader(getConfig)
|
||||
// value, err := rr(ctx) // Returns (Config{Port: 8080}, nil)
|
||||
//
|
||||
//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
|
||||
@@ -183,14 +167,6 @@ func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
|
||||
// Returns:
|
||||
// - A ReaderResult[B] with the transformed value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Right(42)
|
||||
// mapped := readerresult.MonadMap(rr, func(n int) string {
|
||||
// return fmt.Sprintf("Value: %d", n)
|
||||
// })
|
||||
// value, err := mapped(ctx) // Returns ("Value: 42", nil)
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[A, B any](fa ReaderResult[A], f func(A) B) ReaderResult[B] {
|
||||
return RR.MonadMap(fa, f)
|
||||
@@ -210,18 +186,6 @@ func MonadMap[A, B any](fa ReaderResult[A], f func(A) B) ReaderResult[B] {
|
||||
// Returns:
|
||||
// - An Operator that transforms ReaderResult[A] to ReaderResult[B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// rr := readerresult.Right(42)
|
||||
// result := F.Pipe1(
|
||||
// rr,
|
||||
// readerresult.Map(func(n int) string {
|
||||
// return fmt.Sprintf("Value: %d", n)
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return RR.Map[context.Context](f)
|
||||
@@ -244,18 +208,9 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
// Returns:
|
||||
// - A ReaderResult[B] representing the sequenced computation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// getUser := readerresult.Right(User{ID: 1, Name: "Alice"})
|
||||
// getPosts := func(user User) readerresult.ReaderResult[[]Post] {
|
||||
// return readerresult.Right([]Post{{UserID: user.ID}})
|
||||
// }
|
||||
// result := readerresult.MonadChain(getUser, getPosts)
|
||||
// posts, err := result(ctx)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
|
||||
return RR.MonadChain(ma, f)
|
||||
return RR.MonadChain(ma, WithContextK(f))
|
||||
}
|
||||
|
||||
// Chain is the curried version of MonadChain, useful for function composition.
|
||||
@@ -272,20 +227,9 @@ func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
|
||||
// Returns:
|
||||
// - An Operator that chains ReaderResult computations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// getUser(1),
|
||||
// readerresult.Chain(func(user User) readerresult.ReaderResult[[]Post] {
|
||||
// return getPosts(user.ID)
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return RR.Chain(f)
|
||||
return RR.Chain(WithContextK(f))
|
||||
}
|
||||
|
||||
// Of creates a ReaderResult that always succeeds with the given value.
|
||||
@@ -302,14 +246,9 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
// Returns:
|
||||
// - A ReaderResult[A] that always succeeds with the given value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Of(42)
|
||||
// value, err := rr(ctx) // Returns (42, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Of[A any](a A) ReaderResult[A] {
|
||||
return RR.Of[context.Context, A](a)
|
||||
return RR.Of[context.Context](a)
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
|
||||
@@ -426,7 +365,7 @@ func MonadAp[B, A any](fab ReaderResult[func(A) B], fa ReaderResult[A]) ReaderRe
|
||||
// import F "github.com/IBM/fp-go/v2/function"
|
||||
//
|
||||
// value := readerresult.Right(32)
|
||||
// addTen := readerresult.Right(func(n int) int { return n + 10 })
|
||||
// addTen := readerresult.Right(N.Add(10))
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// addTen,
|
||||
@@ -441,7 +380,7 @@ func Ap[B, A any](fa ReaderResult[A]) Operator[func(A) B, B] {
|
||||
|
||||
//go:inline
|
||||
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
|
||||
return RR.FromPredicate[context.Context](pred, onFalse)
|
||||
return WithContextK(RR.FromPredicate[context.Context](pred, onFalse))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -456,7 +395,7 @@ func GetOrElse[A any](onLeft reader.Kleisli[context.Context, error, A]) func(Rea
|
||||
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return RR.OrElse(onLeft)
|
||||
return RR.OrElse(WithContextK(onLeft))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -472,11 +411,6 @@ func OrLeft[A any](onLeft reader.Kleisli[context.Context, error, error]) Operato
|
||||
// Returns:
|
||||
// - A ReaderResult[context.Context] that returns the environment
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Ask()
|
||||
// ctx, err := rr(context.Background()) // Returns (context.Background(), nil)
|
||||
//
|
||||
//go:inline
|
||||
func Ask() ReaderResult[context.Context] {
|
||||
return RR.Ask[context.Context]()
|
||||
@@ -496,16 +430,6 @@ func Ask() ReaderResult[context.Context] {
|
||||
// Returns:
|
||||
// - A ReaderResult[A] that extracts and returns the value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type key int
|
||||
// const userKey key = 0
|
||||
//
|
||||
// getUser := readerresult.Asks(func(ctx context.Context) User {
|
||||
// return ctx.Value(userKey).(User)
|
||||
// })
|
||||
// user, err := getUser(ctx)
|
||||
//
|
||||
//go:inline
|
||||
func Asks[A any](r Reader[context.Context, A]) ReaderResult[A] {
|
||||
return RR.Asks(r)
|
||||
@@ -513,12 +437,12 @@ func Asks[A any](r Reader[context.Context, A]) ReaderResult[A] {
|
||||
|
||||
//go:inline
|
||||
func MonadChainEitherK[A, B any](ma ReaderResult[A], f RES.Kleisli[A, B]) ReaderResult[B] {
|
||||
return RR.MonadChainEitherK[context.Context, A, B](ma, f)
|
||||
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, A, B](f)
|
||||
return RR.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -550,12 +474,6 @@ func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Opera
|
||||
// Returns:
|
||||
// - A flattened ReaderResult[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := readerresult.Right(readerresult.Right(42))
|
||||
// flattened := readerresult.Flatten(nested)
|
||||
// value, err := flattened(ctx) // Returns (42, nil)
|
||||
//
|
||||
//go:inline
|
||||
func Flatten[A any](mma ReaderResult[ReaderResult[A]]) ReaderResult[A] {
|
||||
return RR.Flatten(mma)
|
||||
@@ -585,15 +503,6 @@ func BiMap[A, B any](f Endomorphism[error], g func(A) B) Operator[A, B] {
|
||||
// Returns:
|
||||
// - A function that executes a ReaderResult[A] and returns (A, error)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rr := readerresult.Right(42)
|
||||
// execute := readerresult.Read[int](ctx)
|
||||
// value, err := execute(rr) // Returns (42, nil)
|
||||
//
|
||||
// // Or more commonly used directly:
|
||||
// value, err := rr(ctx)
|
||||
//
|
||||
//go:inline
|
||||
func Read[A any](ctx context.Context) func(ReaderResult[A]) (A, error) {
|
||||
return RR.Read[A](ctx)
|
||||
@@ -776,45 +685,6 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
|
||||
//
|
||||
// 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 := readerresult.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,
|
||||
// readerresult.WithDeadline[Data](deadline),
|
||||
// )
|
||||
// _, err := result(context.Background()) // Returns 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,
|
||||
// readerresult.WithDeadline[Data](laterDeadline),
|
||||
// )
|
||||
// _, 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)
|
||||
|
||||
@@ -29,11 +29,13 @@ import (
|
||||
)
|
||||
|
||||
// Helper types for testing
|
||||
// fp-go:Lens
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
// fp-go:Lens
|
||||
type Config struct {
|
||||
Port int
|
||||
DatabaseURL string
|
||||
@@ -950,3 +952,423 @@ func TestLocalWithTimeoutAndDeadline(t *testing.T) {
|
||||
assert.Equal(t, "A:B", value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadTraverseArray(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("applies function to each element", func(t *testing.T) {
|
||||
double := func(n int) ReaderResult[int] {
|
||||
return Right(n * 2)
|
||||
}
|
||||
numbers := []int{1, 2, 3}
|
||||
result := MonadTraverseArray(numbers, double)
|
||||
values, err := result(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, values)
|
||||
})
|
||||
|
||||
t.Run("fails on first error", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
failOnTwo := func(n int) ReaderResult[int] {
|
||||
if n == 2 {
|
||||
return Left[int](testErr)
|
||||
}
|
||||
return Right(n * 2)
|
||||
}
|
||||
numbers := []int{1, 2, 3}
|
||||
result := MonadTraverseArray(numbers, failOnTwo)
|
||||
_, err := result(ctx)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverseArrayWithIndex(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("applies function with index", func(t *testing.T) {
|
||||
addIndex := func(idx int, s string) ReaderResult[string] {
|
||||
return Right(fmt.Sprintf("%d:%s", idx, s))
|
||||
}
|
||||
items := []string{"a", "b", "c"}
|
||||
result := TraverseArrayWithIndex(addIndex)(items)
|
||||
values, err := result(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, values)
|
||||
})
|
||||
|
||||
t.Run("fails on error", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
failOnIndex := func(idx int, s string) ReaderResult[string] {
|
||||
if idx == 1 {
|
||||
return Left[string](testErr)
|
||||
}
|
||||
return Right(s)
|
||||
}
|
||||
items := []string{"a", "b", "c"}
|
||||
result := TraverseArrayWithIndex(failOnIndex)(items)
|
||||
_, err := result(ctx)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBracket(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("ensures resource cleanup on success", func(t *testing.T) {
|
||||
released := false
|
||||
result := Bracket(
|
||||
func() ReaderResult[int] {
|
||||
return Right(42)
|
||||
},
|
||||
func(resource int) ReaderResult[string] {
|
||||
return Right(fmt.Sprintf("Resource: %d", resource))
|
||||
},
|
||||
func(resource int, value string, err error) ReaderResult[any] {
|
||||
released = true
|
||||
return Right[any](nil)
|
||||
},
|
||||
)
|
||||
value, err := result(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Resource: 42", value)
|
||||
assert.True(t, released)
|
||||
})
|
||||
|
||||
t.Run("ensures resource cleanup on failure", func(t *testing.T) {
|
||||
released := false
|
||||
testErr := errors.New("use failed")
|
||||
result := Bracket(
|
||||
func() ReaderResult[int] {
|
||||
return Right(42)
|
||||
},
|
||||
func(resource int) ReaderResult[string] {
|
||||
return Left[string](testErr)
|
||||
},
|
||||
func(resource int, value string, err error) ReaderResult[any] {
|
||||
released = true
|
||||
assert.Equal(t, testErr, err)
|
||||
return Right[any](nil)
|
||||
},
|
||||
)
|
||||
_, err := result(ctx)
|
||||
assert.Equal(t, testErr, err)
|
||||
assert.True(t, released)
|
||||
})
|
||||
|
||||
t.Run("does not release if acquire fails", func(t *testing.T) {
|
||||
released := false
|
||||
testErr := errors.New("acquire failed")
|
||||
result := Bracket(
|
||||
func() ReaderResult[int] {
|
||||
return Left[int](testErr)
|
||||
},
|
||||
func(resource int) ReaderResult[string] {
|
||||
return Right("should not execute")
|
||||
},
|
||||
func(resource int, value string, err error) ReaderResult[any] {
|
||||
released = true
|
||||
return Right[any](nil)
|
||||
},
|
||||
)
|
||||
_, err := result(ctx)
|
||||
assert.Equal(t, testErr, err)
|
||||
assert.False(t, released)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithContextCancellation(t *testing.T) {
|
||||
t.Run("returns error on cancelled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
rr := WithContext(Right(42))
|
||||
_, err := rr(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("executes on valid context", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
rr := WithContext(Right(42))
|
||||
value, err := rr(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithContextK(t *testing.T) {
|
||||
t.Run("wraps Kleisli with cancellation check", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
kleisli := func(n int) ReaderResult[string] {
|
||||
return Right(fmt.Sprintf("Value: %d", n))
|
||||
}
|
||||
|
||||
wrapped := WithContextK(kleisli)
|
||||
result := wrapped(42)
|
||||
_, err := result(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("executes on valid context", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
kleisli := func(n int) ReaderResult[string] {
|
||||
return Right(fmt.Sprintf("Value: %d", n))
|
||||
}
|
||||
|
||||
wrapped := WithContextK(kleisli)
|
||||
result := wrapped(42)
|
||||
value, err := result(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Value: 42", value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUncurry1(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("converts curried to uncurried", func(t *testing.T) {
|
||||
curried := func(id int) ReaderResult[User] {
|
||||
return Right(User{ID: id, Name: "Alice"})
|
||||
}
|
||||
uncurried := Uncurry1(curried)
|
||||
user, err := uncurried(ctx, 42)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, User{ID: 42, Name: "Alice"}, user)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUncurry2(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("converts curried to uncurried", func(t *testing.T) {
|
||||
curried := func(id int) func(name string) ReaderResult[User] {
|
||||
return func(name string) ReaderResult[User] {
|
||||
return Right(User{ID: id, Name: name})
|
||||
}
|
||||
}
|
||||
uncurried := Uncurry2(curried)
|
||||
user, err := uncurried(ctx, 42, "Bob")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, User{ID: 42, Name: "Bob"}, user)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUncurry3(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("converts curried to uncurried", func(t *testing.T) {
|
||||
curried := func(a int) func(b int) func(c int) ReaderResult[int] {
|
||||
return func(b int) func(c int) ReaderResult[int] {
|
||||
return func(c int) ReaderResult[int] {
|
||||
return Right(a + b + c)
|
||||
}
|
||||
}
|
||||
}
|
||||
uncurried := Uncurry3(curried)
|
||||
result, err := uncurried(ctx, 1, 2, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrom0(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("creates lazy ReaderResult", func(t *testing.T) {
|
||||
f := func(ctx context.Context) (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
thunk := From0(f)
|
||||
rr := thunk()
|
||||
value, err := rr(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrom2(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("converts function to uncurried form", func(t *testing.T) {
|
||||
f := func(ctx context.Context, id int, name string) (User, error) {
|
||||
return User{ID: id, Name: name}, nil
|
||||
}
|
||||
updateUserRR := From2(f)
|
||||
rr := updateUserRR(42, "Charlie")
|
||||
user, err := rr(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, User{ID: 42, Name: "Charlie"}, user)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrom3(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("converts function to uncurried form", func(t *testing.T) {
|
||||
f := func(ctx context.Context, a, b, c int) (int, error) {
|
||||
return a + b + c, nil
|
||||
}
|
||||
sumRR := From3(f)
|
||||
rr := sumRR(1, 2, 3)
|
||||
result, err := rr(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("combines successful values", func(t *testing.T) {
|
||||
intMonoid := N.MonoidSum[int]()
|
||||
rrMonoid := AlternativeMonoid(intMonoid)
|
||||
|
||||
rr1 := Right(10)
|
||||
rr2 := Right(20)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
value, err := combined(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, value)
|
||||
})
|
||||
|
||||
t.Run("uses second on first failure", func(t *testing.T) {
|
||||
intMonoid := N.MonoidSum[int]()
|
||||
rrMonoid := AlternativeMonoid(intMonoid)
|
||||
|
||||
testErr := errors.New("first failed")
|
||||
rr1 := Left[int](testErr)
|
||||
rr2 := Right(42)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
value, err := combined(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAltMonoid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("uses custom zero", func(t *testing.T) {
|
||||
zero := func() ReaderResult[int] {
|
||||
return Right(0)
|
||||
}
|
||||
rrMonoid := AltMonoid(zero)
|
||||
|
||||
empty := rrMonoid.Empty()
|
||||
value, err := empty(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("tries alternatives", func(t *testing.T) {
|
||||
zero := func() ReaderResult[int] {
|
||||
return Left[int](errors.New("empty"))
|
||||
}
|
||||
rrMonoid := AltMonoid(zero)
|
||||
|
||||
testErr := errors.New("first failed")
|
||||
rr1 := Left[int](testErr)
|
||||
rr2 := Right(42)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
value, err := combined(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("combines both computations", func(t *testing.T) {
|
||||
intMonoid := N.MonoidSum[int]()
|
||||
rrMonoid := ApplicativeMonoid(intMonoid)
|
||||
|
||||
rr1 := Right(10)
|
||||
rr2 := Right(20)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
value, err := combined(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, value)
|
||||
})
|
||||
|
||||
t.Run("fails if either fails", func(t *testing.T) {
|
||||
intMonoid := N.MonoidSum[int]()
|
||||
rrMonoid := ApplicativeMonoid(intMonoid)
|
||||
|
||||
testErr := errors.New("failed")
|
||||
rr1 := Left[int](testErr)
|
||||
rr2 := Right(42)
|
||||
combined := rrMonoid.Concat(rr1, rr2)
|
||||
_, err := combined(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceT1(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("wraps single value in tuple", func(t *testing.T) {
|
||||
rr := Right(42)
|
||||
result := SequenceT1(rr)
|
||||
tuple, err := result(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, tuple.F1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceT3(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("combines three ReaderResults", func(t *testing.T) {
|
||||
rr1 := Right(1)
|
||||
rr2 := Right("two")
|
||||
rr3 := Right(3.0)
|
||||
result := SequenceT3(rr1, rr2, rr3)
|
||||
tuple, err := result(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, tuple.F1)
|
||||
assert.Equal(t, "two", tuple.F2)
|
||||
assert.Equal(t, 3.0, tuple.F3)
|
||||
})
|
||||
|
||||
t.Run("fails if any fails", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
rr1 := Right(1)
|
||||
rr2 := Left[string](testErr)
|
||||
rr3 := Right(3.0)
|
||||
result := SequenceT3(rr1, rr2, rr3)
|
||||
_, err := result(ctx)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceT4(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("combines four ReaderResults", func(t *testing.T) {
|
||||
rr1 := Right(1)
|
||||
rr2 := Right("two")
|
||||
rr3 := Right(3.0)
|
||||
rr4 := Right(true)
|
||||
result := SequenceT4(rr1, rr2, rr3, rr4)
|
||||
tuple, err := result(ctx)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, tuple.F1)
|
||||
assert.Equal(t, "two", tuple.F2)
|
||||
assert.Equal(t, 3.0, tuple.F3)
|
||||
assert.Equal(t, true, tuple.F4)
|
||||
})
|
||||
|
||||
t.Run("fails if any fails", func(t *testing.T) {
|
||||
testErr := errors.New("test error")
|
||||
rr1 := Right(1)
|
||||
rr2 := Right("two")
|
||||
rr3 := Left[float64](testErr)
|
||||
rr4 := Right(true)
|
||||
result := SequenceT4(rr1, rr2, rr3, rr4)
|
||||
_, err := result(ctx)
|
||||
assert.Equal(t, testErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"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"
|
||||
@@ -46,12 +48,24 @@ type (
|
||||
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// ReaderResult represents a computation that depends on a context.Context and produces either a value of type A or an error.
|
||||
// It combines the Reader pattern with Result (error handling), making it suitable for context-aware operations that may fail.
|
||||
ReaderResult[A any] = func(context.Context) (A, error)
|
||||
|
||||
// Monoid represents a monoid structure for ReaderResult values.
|
||||
Monoid[A any] = monoid.Monoid[ReaderResult[A]]
|
||||
|
||||
// Kleisli represents a Kleisli arrow from A to ReaderResult[B].
|
||||
// It's a function that takes a value of type A and returns a computation that produces B or an error in a context.
|
||||
Kleisli[A, B any] = Reader[A, ReaderResult[B]]
|
||||
|
||||
// Operator represents a Kleisli arrow that operates on ReaderResult values.
|
||||
// It transforms a ReaderResult[A] into a ReaderResult[B], useful for composing context-aware operations.
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
|
||||
// Lens represents an optic that focuses on a field of type A within a structure of type S.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
// Prism represents an optic that focuses on a case of type A within a sum type S.
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestMkdir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newDir := filepath.Join(tmpDir, "testdir")
|
||||
|
||||
result := Mkdir(newDir, 0755)
|
||||
result := Mkdir(newDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -43,14 +43,14 @@ func TestMkdir(t *testing.T) {
|
||||
t.Run("mkdir with existing directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
result := Mkdir(tmpDir, 0755)
|
||||
result := Mkdir(tmpDir, 0o755)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("mkdir with parent directory not existing", func(t *testing.T) {
|
||||
result := Mkdir("/non/existent/parent/child", 0755)
|
||||
result := Mkdir("/non/existent/parent/child", 0o755)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -62,7 +62,7 @@ func TestMkdirAll(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
|
||||
|
||||
result := MkdirAll(nestedDir, 0755)
|
||||
result := MkdirAll(nestedDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -88,7 +88,7 @@ func TestMkdirAll(t *testing.T) {
|
||||
t.Run("mkdirall with existing directory", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
result := MkdirAll(tmpDir, 0755)
|
||||
result := MkdirAll(tmpDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
// MkdirAll should succeed even if directory exists
|
||||
@@ -100,7 +100,7 @@ func TestMkdirAll(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newDir := filepath.Join(tmpDir, "single")
|
||||
|
||||
result := MkdirAll(newDir, 0755)
|
||||
result := MkdirAll(newDir, 0o755)
|
||||
path, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -116,11 +116,11 @@ func TestMkdirAll(t *testing.T) {
|
||||
filePath := filepath.Join(tmpDir, "file.txt")
|
||||
|
||||
// Create a file
|
||||
err := os.WriteFile(filePath, []byte("content"), 0644)
|
||||
err := os.WriteFile(filePath, []byte("content"), 0o644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to create a directory where file exists
|
||||
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755)
|
||||
result := MkdirAll(filepath.Join(filePath, "subdir"), 0o755)
|
||||
_, err = result()
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestOpen(t *testing.T) {
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write some content
|
||||
err = os.WriteFile(tmpPath, []byte("test content"), 0644)
|
||||
err = os.WriteFile(tmpPath, []byte("test content"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test Open
|
||||
@@ -127,7 +127,7 @@ func TestWriteFile(t *testing.T) {
|
||||
testPath := filepath.Join(tmpDir, "write-test.txt")
|
||||
testData := []byte("test data")
|
||||
|
||||
result := WriteFile(testPath, 0644)(testData)
|
||||
result := WriteFile(testPath, 0o644)(testData)
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -141,7 +141,7 @@ func TestWriteFile(t *testing.T) {
|
||||
|
||||
t.Run("write to invalid path", func(t *testing.T) {
|
||||
testData := []byte("test data")
|
||||
result := WriteFile("/non/existent/dir/file.txt", 0644)(testData)
|
||||
result := WriteFile("/non/existent/dir/file.txt", 0o644)(testData)
|
||||
_, err := result()
|
||||
|
||||
assert.Error(t, err)
|
||||
@@ -155,12 +155,12 @@ func TestWriteFile(t *testing.T) {
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
// Write initial content
|
||||
err = os.WriteFile(tmpPath, []byte("initial"), 0644)
|
||||
err = os.WriteFile(tmpPath, []byte("initial"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Overwrite with new content
|
||||
newData := []byte("overwritten")
|
||||
result := WriteFile(tmpPath, 0644)(newData)
|
||||
result := WriteFile(tmpPath, 0o644)(newData)
|
||||
returnedData, err := result()
|
||||
|
||||
assert.NoError(t, err)
|
||||
@@ -212,7 +212,7 @@ func TestClose(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed by attempting to write
|
||||
_, writeErr := tmpFile.Write([]byte("test"))
|
||||
_, writeErr := tmpFile.WriteString("test")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestReadAll(t *testing.T) {
|
||||
largeContent[i] = byte('A' + (i % 26))
|
||||
}
|
||||
|
||||
err := os.WriteFile(testPath, largeContent, 0644)
|
||||
err := os.WriteFile(testPath, largeContent, 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := ReadAll(Open(testPath))
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestWriteAll(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed by trying to write to it
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
_, writeErr := capturedFile.WriteString("more")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestWrite(t *testing.T) {
|
||||
|
||||
useFile := func(f *os.File) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
_, err := f.Write([]byte("data"))
|
||||
_, err := f.WriteString("data")
|
||||
return "success", err
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,7 @@ func TestWrite(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify file is closed
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
_, writeErr := capturedFile.WriteString("more")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
@@ -183,7 +183,7 @@ func TestWrite(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
|
||||
// Verify file is still closed even on error
|
||||
_, writeErr := capturedFile.Write([]byte("more"))
|
||||
_, writeErr := capturedFile.WriteString("more")
|
||||
assert.Error(t, writeErr)
|
||||
})
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ func Requester(builder *R.Builder) IOEH.Requester {
|
||||
|
||||
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
|
||||
return func() (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
req, err := http.NewRequest(method, url, http.NoBody)
|
||||
if err == nil {
|
||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -111,7 +112,7 @@ func TestChainWithIO(t *testing.T) {
|
||||
Of("test"),
|
||||
ChainIOK(func(s string) IO[bool] {
|
||||
return func() bool {
|
||||
return len(s) > 0
|
||||
return S.IsNonEmpty(s)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
)
|
||||
|
||||
// Benchmark shallow chain (1 step)
|
||||
@@ -100,7 +101,7 @@ func BenchmarkChain_RealWorld_Validation(b *testing.B) {
|
||||
|
||||
// Step 1: Validate not empty
|
||||
v1, ok1 := Chain(func(s string) (string, bool) {
|
||||
if len(s) > 0 {
|
||||
if S.IsNonEmpty(s) {
|
||||
return s, true
|
||||
}
|
||||
return "", false
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
I "github.com/IBM/fp-go/v2/iterator/iter"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -225,7 +226,7 @@ func TestTraverseIter_ComplexTransformation(t *testing.T) {
|
||||
}
|
||||
|
||||
validatePerson := func(name string) (Person, bool) {
|
||||
if name == "" {
|
||||
if S.IsEmpty(name) {
|
||||
return None[Person]()
|
||||
}
|
||||
return Some(Person{Name: name, Age: len(name)})
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[A, A] {
|
||||
func _log[A any](left, right func(string, ...any), prefix string) Operator[A, A] {
|
||||
return func(a A, aok bool) (A, bool) {
|
||||
if aok {
|
||||
right("%s: %v", prefix, a)
|
||||
|
||||
@@ -184,7 +184,7 @@ func TestStringFormat(t *testing.T) {
|
||||
|
||||
// // Test Semigroup
|
||||
// func TestSemigroup(t *testing.T) {
|
||||
// intSemigroup := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// intSemigroup := N.MonoidSum[int]()
|
||||
// optSemigroup := Semigroup[int]()(intSemigroup)
|
||||
|
||||
// AssertEq(Some(5), optSemigroup.Concat(Some(2), Some(3)))
|
||||
@@ -195,7 +195,7 @@ func TestStringFormat(t *testing.T) {
|
||||
|
||||
// // Test Monoid
|
||||
// func TestMonoid(t *testing.T) {
|
||||
// intSemigroup := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// intSemigroup := N.MonoidSum[int]()
|
||||
// optMonoid := Monoid[int]()(intSemigroup)
|
||||
|
||||
// AssertEq(Some(5), optMonoid.Concat(Some(2), Some(3)))
|
||||
@@ -204,7 +204,7 @@ func TestStringFormat(t *testing.T) {
|
||||
|
||||
// // Test ApplySemigroup
|
||||
// func TestApplySemigroup(t *testing.T) {
|
||||
// intSemigroup := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
// intSemigroup := N.MonoidSum[int]()
|
||||
// optSemigroup := ApplySemigroup(intSemigroup)
|
||||
|
||||
// AssertEq(Some(5), optSemigroup.Concat(Some(2), Some(3)))
|
||||
@@ -213,7 +213,7 @@ func TestStringFormat(t *testing.T) {
|
||||
|
||||
// // Test ApplicativeMonoid
|
||||
// func TestApplicativeMonoid(t *testing.T) {
|
||||
// intMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// intMonoid := N.MonoidSum[int]()
|
||||
// optMonoid := ApplicativeMonoid(intMonoid)
|
||||
|
||||
// AssertEq(Some(5), optMonoid.Concat(Some(2), Some(3)))
|
||||
@@ -222,7 +222,7 @@ func TestStringFormat(t *testing.T) {
|
||||
|
||||
// // Test AlternativeMonoid
|
||||
// func TestAlternativeMonoid(t *testing.T) {
|
||||
// intMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// intMonoid := N.MonoidSum[int]()
|
||||
// optMonoid := AlternativeMonoid(intMonoid)
|
||||
|
||||
// // AlternativeMonoid uses applicative semantics, so it combines values
|
||||
|
||||
@@ -42,6 +42,8 @@ import (
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
@@ -57,7 +59,7 @@ import (
|
||||
// }
|
||||
// return func(db Database) func() (string, error) {
|
||||
// return func() (string, error) {
|
||||
// if db.ConnectionString == "" {
|
||||
// if S.IsEmpty(db.ConnectionString) {
|
||||
// return "", errors.New("empty connection")
|
||||
// }
|
||||
// return fmt.Sprintf("Query on %s with timeout %d",
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -89,7 +90,7 @@ func TestSequence(t *testing.T) {
|
||||
return func() (ReaderIOResult[string, int], error) {
|
||||
return func(s string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return 0, expectedError
|
||||
}
|
||||
return x + len(s), nil
|
||||
@@ -140,7 +141,7 @@ func TestSequence(t *testing.T) {
|
||||
}
|
||||
return func(db Database) IOResult[string] {
|
||||
return func() (string, error) {
|
||||
if db.ConnectionString == "" {
|
||||
if S.IsEmpty(db.ConnectionString) {
|
||||
return "", errors.New("empty connection string")
|
||||
}
|
||||
return fmt.Sprintf("Query on %s with timeout %d",
|
||||
@@ -400,7 +401,7 @@ func TestTraverse(t *testing.T) {
|
||||
kleisli := func(a int) ReaderIOResult[string, int] {
|
||||
return func(s string) IOResult[int] {
|
||||
return func() (int, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return 0, expectedError
|
||||
}
|
||||
return a + len(s), nil
|
||||
|
||||
@@ -41,6 +41,8 @@ import (
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import S "github.com/IBM/fp-go/v2/string"
|
||||
//
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
@@ -54,7 +56,7 @@ import (
|
||||
// return nil, errors.New("invalid timeout")
|
||||
// }
|
||||
// return func(db Database) (string, error) {
|
||||
// if db.ConnectionString == "" {
|
||||
// if S.IsEmpty(db.ConnectionString) {
|
||||
// return "", errors.New("empty connection")
|
||||
// }
|
||||
// return fmt.Sprintf("Query on %s with timeout %d",
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -79,7 +80,7 @@ func TestSequence(t *testing.T) {
|
||||
|
||||
original := func(x int) (ReaderResult[string, int], error) {
|
||||
return func(s string) (int, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return 0, expectedError
|
||||
}
|
||||
return x + len(s), nil
|
||||
@@ -122,7 +123,7 @@ func TestSequence(t *testing.T) {
|
||||
return nil, errors.New("invalid timeout")
|
||||
}
|
||||
return func(db Database) (string, error) {
|
||||
if db.ConnectionString == "" {
|
||||
if S.IsEmpty(db.ConnectionString) {
|
||||
return "", errors.New("empty connection string")
|
||||
}
|
||||
return fmt.Sprintf("Query on %s with timeout %d",
|
||||
@@ -314,7 +315,7 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
kleisli := func(a int) ReaderResult[string, int] {
|
||||
return func(s string) (int, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return 0, expectedError
|
||||
}
|
||||
return a + len(s), nil
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
RES "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -292,11 +291,11 @@ func TestChainReaderK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestChainEitherK(t *testing.T) {
|
||||
parseInt := func(s string) RES.Result[int] {
|
||||
parseInt := func(s string) Result[int] {
|
||||
if s == "42" {
|
||||
return RES.Of(42)
|
||||
return result.Of(42)
|
||||
}
|
||||
return RES.Left[int](errors.New("parse error"))
|
||||
return result.Left[int](errors.New("parse error"))
|
||||
}
|
||||
|
||||
chain := ChainEitherK[MyContext](parseInt)
|
||||
|
||||
@@ -129,7 +129,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
// Example - Annotate with index:
|
||||
//
|
||||
// annotate := func(i int, s string) (string, error) {
|
||||
// if len(s) == 0 {
|
||||
// if S.IsEmpty(s) {
|
||||
// return "", fmt.Errorf("empty string at index %d", i)
|
||||
// }
|
||||
// return fmt.Sprintf("[%d]=%s", i, s), nil
|
||||
@@ -170,7 +170,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) (B, erro
|
||||
// Example - Validate with position info:
|
||||
//
|
||||
// check := func(i int, s string) (string, error) {
|
||||
// if len(s) == 0 {
|
||||
// if S.IsEmpty(s) {
|
||||
// return "", fmt.Errorf("empty value at position %d", i)
|
||||
// }
|
||||
// return strings.ToUpper(s), nil
|
||||
|
||||
@@ -21,15 +21,14 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTraverseArrayG_Success tests successful traversal of an array with all valid elements
|
||||
func TestTraverseArrayG_Success(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"})
|
||||
|
||||
@@ -39,9 +38,7 @@ func TestTraverseArrayG_Success(t *testing.T) {
|
||||
|
||||
// TestTraverseArrayG_Error tests that traversal short-circuits on first error
|
||||
func TestTraverseArrayG_Error(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "bad", "3"})
|
||||
|
||||
@@ -51,9 +48,7 @@ func TestTraverseArrayG_Error(t *testing.T) {
|
||||
|
||||
// TestTraverseArrayG_EmptyArray tests traversal of an empty array
|
||||
func TestTraverseArrayG_EmptyArray(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{})
|
||||
|
||||
@@ -64,9 +59,7 @@ func TestTraverseArrayG_EmptyArray(t *testing.T) {
|
||||
|
||||
// TestTraverseArrayG_SingleElement tests traversal with a single element
|
||||
func TestTraverseArrayG_SingleElement(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
result, err := TraverseArrayG[[]string, []int](parse)([]string{"42"})
|
||||
|
||||
@@ -96,9 +89,7 @@ func TestTraverseArrayG_CustomSliceType(t *testing.T) {
|
||||
type StringSlice []string
|
||||
type IntSlice []int
|
||||
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
input := StringSlice{"1", "2", "3"}
|
||||
result, err := TraverseArrayG[StringSlice, IntSlice](parse)(input)
|
||||
@@ -178,7 +169,7 @@ func TestTraverseArray_EmptyArray(t *testing.T) {
|
||||
// TestTraverseArray_DifferentTypes tests transformation between different types
|
||||
func TestTraverseArray_DifferentTypes(t *testing.T) {
|
||||
toLength := func(s string) (int, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return 0, errors.New("empty string")
|
||||
}
|
||||
return len(s), nil
|
||||
@@ -193,7 +184,7 @@ func TestTraverseArray_DifferentTypes(t *testing.T) {
|
||||
// TestTraverseArrayWithIndexG_Success tests successful indexed traversal
|
||||
func TestTraverseArrayWithIndexG_Success(t *testing.T) {
|
||||
annotate := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return "", fmt.Errorf("empty string at index %d", i)
|
||||
}
|
||||
return fmt.Sprintf("[%d]=%s", i, s), nil
|
||||
@@ -208,7 +199,7 @@ func TestTraverseArrayWithIndexG_Success(t *testing.T) {
|
||||
// TestTraverseArrayWithIndexG_Error tests error handling with index
|
||||
func TestTraverseArrayWithIndexG_Error(t *testing.T) {
|
||||
annotate := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return "", fmt.Errorf("empty string at index %d", i)
|
||||
}
|
||||
return fmt.Sprintf("[%d]=%s", i, s), nil
|
||||
@@ -284,7 +275,7 @@ func TestTraverseArrayWithIndexG_CustomSliceType(t *testing.T) {
|
||||
// TestTraverseArrayWithIndex_Success tests successful indexed traversal
|
||||
func TestTraverseArrayWithIndex_Success(t *testing.T) {
|
||||
check := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return "", fmt.Errorf("empty value at position %d", i)
|
||||
}
|
||||
return fmt.Sprintf("%d_%s", i, s), nil
|
||||
@@ -299,7 +290,7 @@ func TestTraverseArrayWithIndex_Success(t *testing.T) {
|
||||
// TestTraverseArrayWithIndex_Error tests error with position info
|
||||
func TestTraverseArrayWithIndex_Error(t *testing.T) {
|
||||
check := func(i int, s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
if S.IsEmpty(s) {
|
||||
return "", fmt.Errorf("empty value at position %d", i)
|
||||
}
|
||||
return s, nil
|
||||
|
||||
@@ -30,7 +30,7 @@ func Curry0[R any](f func() (R, error)) func() (R, error) {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// parse := func(s string) (int, error) { return strconv.Atoi(s) }
|
||||
// parse := strconv.Atoi
|
||||
// curried := either.Curry1(parse)
|
||||
// result := curried("42") // Right(42)
|
||||
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) (R, error) {
|
||||
|
||||
@@ -65,6 +65,6 @@ func bodyRequest(method string) func(string) func([]byte) (*http.Request, error)
|
||||
|
||||
func noBodyRequest(method string) func(string) (*http.Request, error) {
|
||||
return func(url string) (*http.Request, error) {
|
||||
return http.NewRequest(method, url, nil)
|
||||
return http.NewRequest(method, url, http.NoBody)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
L "github.com/IBM/fp-go/v2/logging"
|
||||
)
|
||||
|
||||
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[A, A] {
|
||||
func _log[A any](left, right func(string, ...any), prefix string) Operator[A, A] {
|
||||
return func(a A, err error) (A, error) {
|
||||
if err != nil {
|
||||
left("%s: %v", prefix, err)
|
||||
|
||||
@@ -21,15 +21,14 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTraverseRecordG_Success tests successful traversal of a map
|
||||
func TestTraverseRecordG_Success(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
input := map[string]string{"a": "1", "b": "2", "c": "3"}
|
||||
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
|
||||
@@ -42,9 +41,7 @@ func TestTraverseRecordG_Success(t *testing.T) {
|
||||
|
||||
// TestTraverseRecordG_Error tests that traversal short-circuits on error
|
||||
func TestTraverseRecordG_Error(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
input := map[string]string{"a": "1", "b": "bad", "c": "3"}
|
||||
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
|
||||
@@ -55,9 +52,7 @@ func TestTraverseRecordG_Error(t *testing.T) {
|
||||
|
||||
// TestTraverseRecordG_EmptyMap tests traversal of an empty map
|
||||
func TestTraverseRecordG_EmptyMap(t *testing.T) {
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
input := map[string]string{}
|
||||
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
|
||||
@@ -72,9 +67,7 @@ func TestTraverseRecordG_CustomMapType(t *testing.T) {
|
||||
type StringMap map[string]string
|
||||
type IntMap map[string]int
|
||||
|
||||
parse := func(s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parse := strconv.Atoi
|
||||
|
||||
input := StringMap{"x": "10", "y": "20"}
|
||||
result, err := TraverseRecordG[StringMap, IntMap](parse)(input)
|
||||
@@ -128,7 +121,7 @@ func TestTraverseRecord_ValidationError(t *testing.T) {
|
||||
// TestTraverseRecordWithIndexG_Success tests successful indexed traversal
|
||||
func TestTraverseRecordWithIndexG_Success(t *testing.T) {
|
||||
annotate := func(k string, v string) (string, error) {
|
||||
if len(v) == 0 {
|
||||
if S.IsEmpty(v) {
|
||||
return "", fmt.Errorf("empty value for key %s", k)
|
||||
}
|
||||
return fmt.Sprintf("%s=%s", k, v), nil
|
||||
@@ -145,7 +138,7 @@ func TestTraverseRecordWithIndexG_Success(t *testing.T) {
|
||||
// TestTraverseRecordWithIndexG_Error tests error handling with key
|
||||
func TestTraverseRecordWithIndexG_Error(t *testing.T) {
|
||||
annotate := func(k string, v string) (string, error) {
|
||||
if len(v) == 0 {
|
||||
if S.IsEmpty(v) {
|
||||
return "", fmt.Errorf("empty value for key %s", k)
|
||||
}
|
||||
return v, nil
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
STR "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -43,7 +44,7 @@ func makeErrorListSemigroup() S.Semigroup[error] {
|
||||
var msgs []string
|
||||
if strings.HasPrefix(msg1, "[") && strings.HasSuffix(msg1, "]") {
|
||||
trimmed := strings.Trim(msg1, "[]")
|
||||
if trimmed != "" {
|
||||
if STR.IsNonEmpty(trimmed) {
|
||||
msgs = strings.Split(trimmed, ", ")
|
||||
}
|
||||
} else {
|
||||
@@ -52,7 +53,7 @@ func makeErrorListSemigroup() S.Semigroup[error] {
|
||||
|
||||
if strings.HasPrefix(msg2, "[") && strings.HasSuffix(msg2, "]") {
|
||||
trimmed := strings.Trim(msg2, "[]")
|
||||
if trimmed != "" {
|
||||
if STR.IsNonEmpty(trimmed) {
|
||||
msgs = append(msgs, strings.Split(trimmed, ", ")...)
|
||||
}
|
||||
} else {
|
||||
@@ -160,7 +161,7 @@ func TestApV_StringTransformation(t *testing.T) {
|
||||
sg := makeErrorConcatSemigroup()
|
||||
apv := ApV[string, string](sg)
|
||||
|
||||
toUpper := func(s string) string { return strings.ToUpper(s) }
|
||||
toUpper := strings.ToUpper
|
||||
|
||||
value, verr := Right("hello")
|
||||
fn, ferr := Right(toUpper)
|
||||
|
||||
@@ -166,7 +166,7 @@ func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(idx int, a A) B
|
||||
}
|
||||
|
||||
func ConstNil[GA ~[]A, A any]() GA {
|
||||
return (GA)(nil)
|
||||
return GA(nil)
|
||||
}
|
||||
|
||||
func Concat[GT ~[]T, T any](left, right GT) GT {
|
||||
@@ -184,3 +184,16 @@ func Concat[GT ~[]T, T any](left, right GT) GT {
|
||||
copy(buf[copy(buf, left):], right)
|
||||
return buf
|
||||
}
|
||||
|
||||
func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
l := len(as)
|
||||
if l <= 1 {
|
||||
return as
|
||||
}
|
||||
ras := make(GT, l)
|
||||
l1 := l - 1
|
||||
for i := range l {
|
||||
ras[i] = as[l1-i]
|
||||
}
|
||||
return ras
|
||||
}
|
||||
|
||||
78
v2/internal/formatting/type.go
Normal file
78
v2/internal/formatting/type.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package formatting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Formattable is a composite interface that combines multiple formatting capabilities
|
||||
// from the Go standard library. Types implementing this interface can be formatted
|
||||
// in various contexts including string conversion, custom formatting, Go syntax
|
||||
// representation, and structured logging.
|
||||
//
|
||||
// This interface is particularly useful for types that need to provide consistent
|
||||
// formatting across different output contexts, such as logging, debugging, and
|
||||
// user-facing displays.
|
||||
//
|
||||
// Embedded Interfaces:
|
||||
//
|
||||
// - fmt.Stringer: Provides String() string method for basic string representation
|
||||
// - fmt.Formatter: Provides Format(f fmt.State, verb rune) for custom formatting with verbs like %v, %s, %+v, etc.
|
||||
// - fmt.GoStringer: Provides GoString() string method for Go-syntax representation (used with %#v)
|
||||
// - slog.LogValuer: Provides LogValue() slog.Value for structured logging with the slog package
|
||||
//
|
||||
// Example Implementation:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// // String provides a simple string representation
|
||||
// func (u User) String() string {
|
||||
// return fmt.Sprintf("User(%s)", u.Name)
|
||||
// }
|
||||
//
|
||||
// // Format provides custom formatting based on the verb
|
||||
// func (u User) Format(f fmt.State, verb rune) {
|
||||
// switch verb {
|
||||
// case 'v':
|
||||
// if f.Flag('+') {
|
||||
// fmt.Fprintf(f, "User{ID: %d, Name: %s}", u.ID, u.Name)
|
||||
// } else {
|
||||
// fmt.Fprint(f, u.String())
|
||||
// }
|
||||
// case 's':
|
||||
// fmt.Fprint(f, u.String())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // GoString provides Go-syntax representation
|
||||
// func (u User) GoString() string {
|
||||
// return fmt.Sprintf("User{ID: %d, Name: %q}", u.ID, u.Name)
|
||||
// }
|
||||
//
|
||||
// // LogValue provides structured logging representation
|
||||
// func (u User) LogValue() slog.Value {
|
||||
// return slog.GroupValue(
|
||||
// slog.Int("id", u.ID),
|
||||
// slog.String("name", u.Name),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// user := User{ID: 1, Name: "Alice"}
|
||||
// fmt.Println(user) // Output: User(Alice)
|
||||
// fmt.Printf("%+v\n", user) // Output: User{ID: 1, Name: Alice}
|
||||
// fmt.Printf("%#v\n", user) // Output: User{ID: 1, Name: "Alice"}
|
||||
// slog.Info("user", "user", user) // Structured log with id and name fields
|
||||
Formattable interface {
|
||||
fmt.Stringer
|
||||
fmt.Formatter
|
||||
fmt.GoStringer
|
||||
slog.LogValuer
|
||||
}
|
||||
)
|
||||
123
v2/internal/formatting/utils.go
Normal file
123
v2/internal/formatting/utils.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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 formatting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FmtString implements the fmt.Formatter interface for Formattable types.
|
||||
// It handles various format verbs to provide consistent string formatting
|
||||
// across different output contexts.
|
||||
//
|
||||
// Supported format verbs:
|
||||
// - %v: Uses String() representation (default format)
|
||||
// - %+v: Uses String() representation (verbose format)
|
||||
// - %#v: Uses GoString() representation (Go-syntax format)
|
||||
// - %s: Uses String() representation (string format)
|
||||
// - %q: Uses quoted String() representation (quoted string format)
|
||||
// - default: Uses String() representation for any other verb
|
||||
//
|
||||
// The function delegates to the appropriate method of the Formattable interface
|
||||
// based on the format verb and flags provided by fmt.State.
|
||||
//
|
||||
// Parameters:
|
||||
// - stg: The Formattable value to format
|
||||
// - f: The fmt.State that provides formatting context and flags
|
||||
// - c: The format verb (rune) being used
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// type MyType struct {
|
||||
// value int
|
||||
// }
|
||||
//
|
||||
// func (m MyType) Format(f fmt.State, verb rune) {
|
||||
// formatting.FmtString(m, f, verb)
|
||||
// }
|
||||
//
|
||||
// func (m MyType) String() string {
|
||||
// return fmt.Sprintf("MyType(%d)", m.value)
|
||||
// }
|
||||
//
|
||||
// func (m MyType) GoString() string {
|
||||
// return fmt.Sprintf("MyType{value: %d}", m.value)
|
||||
// }
|
||||
//
|
||||
// // Usage:
|
||||
// mt := MyType{value: 42}
|
||||
// fmt.Printf("%v\n", mt) // Output: MyType(42)
|
||||
// fmt.Printf("%#v\n", mt) // Output: MyType{value: 42}
|
||||
// fmt.Printf("%s\n", mt) // Output: MyType(42)
|
||||
// fmt.Printf("%q\n", mt) // Output: "MyType(42)"
|
||||
func FmtString(stg Formattable, f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 'v':
|
||||
if f.Flag('#') {
|
||||
// %#v uses GoString representation
|
||||
fmt.Fprint(f, stg.GoString())
|
||||
} else {
|
||||
// %v and %+v use String representation
|
||||
fmt.Fprint(f, stg.String())
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprint(f, stg.String())
|
||||
case 'q':
|
||||
fmt.Fprintf(f, "%q", stg.String())
|
||||
default:
|
||||
fmt.Fprint(f, stg.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TypeInfo returns a string representation of the type of the given value.
|
||||
// It uses reflection to determine the type and removes the leading asterisk (*)
|
||||
// from pointer types to provide a cleaner type name.
|
||||
//
|
||||
// This function is useful for generating human-readable type information in
|
||||
// string representations, particularly for generic types where the concrete
|
||||
// type needs to be displayed.
|
||||
//
|
||||
// Parameters:
|
||||
// - v: The value whose type information should be extracted
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the type name, with pointer prefix removed
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // For non-pointer types
|
||||
// TypeInfo(42) // Returns: "int"
|
||||
// TypeInfo("hello") // Returns: "string"
|
||||
// TypeInfo([]int{1, 2, 3}) // Returns: "[]int"
|
||||
//
|
||||
// // For pointer types (asterisk is removed)
|
||||
// var ptr *int
|
||||
// TypeInfo(ptr) // Returns: "int" (not "*int")
|
||||
//
|
||||
// // For custom types
|
||||
// type MyStruct struct{ Name string }
|
||||
// TypeInfo(MyStruct{}) // Returns: "formatting.MyStruct"
|
||||
// TypeInfo(&MyStruct{}) // Returns: "formatting.MyStruct" (not "*formatting.MyStruct")
|
||||
//
|
||||
// // For interface types
|
||||
// var err error = fmt.Errorf("test")
|
||||
// TypeInfo(err) // Returns: "errors.errorString"
|
||||
func TypeInfo(v any) string {
|
||||
// Remove the leading * from pointer type
|
||||
return strings.TrimPrefix(reflect.TypeOf(v).String(), "*")
|
||||
}
|
||||
369
v2/internal/formatting/utils_test.go
Normal file
369
v2/internal/formatting/utils_test.go
Normal file
@@ -0,0 +1,369 @@
|
||||
// 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 formatting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// mockFormattable is a test implementation of the Formattable interface
|
||||
type mockFormattable struct {
|
||||
stringValue string
|
||||
goStringValue string
|
||||
}
|
||||
|
||||
func (m mockFormattable) String() string {
|
||||
return m.stringValue
|
||||
}
|
||||
|
||||
func (m mockFormattable) GoString() string {
|
||||
return m.goStringValue
|
||||
}
|
||||
|
||||
func (m mockFormattable) Format(f fmt.State, verb rune) {
|
||||
FmtString(m, f, verb)
|
||||
}
|
||||
|
||||
func (m mockFormattable) LogValue() slog.Value {
|
||||
return slog.StringValue(m.stringValue)
|
||||
}
|
||||
|
||||
func TestFmtString(t *testing.T) {
|
||||
t.Run("format with %v verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%v", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for %v")
|
||||
})
|
||||
|
||||
t.Run("format with %+v verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%+v", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for %+v")
|
||||
})
|
||||
|
||||
t.Run("format with %#v verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%#v", mock)
|
||||
assert.Equal(t, "test.GoString", result, "Should use GoString() for %#v")
|
||||
})
|
||||
|
||||
t.Run("format with %s verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for %s")
|
||||
})
|
||||
|
||||
t.Run("format with %q verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%q", mock)
|
||||
assert.Equal(t, `"test value"`, result, "Should use quoted String() for %q")
|
||||
})
|
||||
|
||||
t.Run("format with unsupported verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
// Using %d which is not a typical string verb
|
||||
result := fmt.Sprintf("%d", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for unsupported verbs")
|
||||
})
|
||||
|
||||
t.Run("format with special characters in string", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test\nvalue\twith\rspecial",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "test\nvalue\twith\rspecial", result)
|
||||
})
|
||||
|
||||
t.Run("format with empty string", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "",
|
||||
goStringValue: "",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("format with unicode characters", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "Hello 世界 🌍",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "Hello 世界 🌍", result)
|
||||
})
|
||||
|
||||
t.Run("format with %q and special characters", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test\nvalue",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%q", mock)
|
||||
assert.Equal(t, `"test\nvalue"`, result, "Should properly escape special characters in quoted format")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTypeInfo(t *testing.T) {
|
||||
t.Run("basic types", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
expected string
|
||||
}{
|
||||
{"int", 42, "int"},
|
||||
{"string", "hello", "string"},
|
||||
{"bool", true, "bool"},
|
||||
{"float64", 3.14, "float64"},
|
||||
{"float32", float32(3.14), "float32"},
|
||||
{"int64", int64(42), "int64"},
|
||||
{"int32", int32(42), "int32"},
|
||||
{"uint", uint(42), "uint"},
|
||||
{"byte", byte(42), "uint8"},
|
||||
{"rune", rune('a'), "int32"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := TypeInfo(tt.value)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pointer types", func(t *testing.T) {
|
||||
var intPtr *int
|
||||
result := TypeInfo(intPtr)
|
||||
assert.Equal(t, "int", result, "Should remove leading * from pointer type")
|
||||
|
||||
var strPtr *string
|
||||
result = TypeInfo(strPtr)
|
||||
assert.Equal(t, "string", result, "Should remove leading * from pointer type")
|
||||
})
|
||||
|
||||
t.Run("slice types", func(t *testing.T) {
|
||||
result := TypeInfo([]int{1, 2, 3})
|
||||
assert.Equal(t, "[]int", result)
|
||||
|
||||
result = TypeInfo([]string{"a", "b"})
|
||||
assert.Equal(t, "[]string", result)
|
||||
|
||||
result = TypeInfo([][]int{{1, 2}, {3, 4}})
|
||||
assert.Equal(t, "[][]int", result)
|
||||
})
|
||||
|
||||
t.Run("map types", func(t *testing.T) {
|
||||
result := TypeInfo(map[string]int{"a": 1})
|
||||
assert.Equal(t, "map[string]int", result)
|
||||
|
||||
result = TypeInfo(map[int]string{1: "a"})
|
||||
assert.Equal(t, "map[int]string", result)
|
||||
})
|
||||
|
||||
t.Run("struct types", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
result := TypeInfo(TestStruct{})
|
||||
assert.Equal(t, "formatting.TestStruct", result)
|
||||
|
||||
result = TypeInfo(&TestStruct{})
|
||||
assert.Equal(t, "formatting.TestStruct", result, "Should remove leading * from pointer to struct")
|
||||
})
|
||||
|
||||
t.Run("interface types", func(t *testing.T) {
|
||||
var err error = fmt.Errorf("test error")
|
||||
result := TypeInfo(err)
|
||||
assert.Contains(t, result, "errors", "Should contain package name")
|
||||
assert.NotContains(t, result, "*", "Should not contain pointer prefix")
|
||||
})
|
||||
|
||||
t.Run("channel types", func(t *testing.T) {
|
||||
ch := make(chan int)
|
||||
result := TypeInfo(ch)
|
||||
assert.Equal(t, "chan int", result)
|
||||
|
||||
ch2 := make(chan string, 10)
|
||||
result = TypeInfo(ch2)
|
||||
assert.Equal(t, "chan string", result)
|
||||
})
|
||||
|
||||
t.Run("function types", func(t *testing.T) {
|
||||
fn := func(int) string { return "" }
|
||||
result := TypeInfo(fn)
|
||||
assert.Equal(t, "func(int) string", result)
|
||||
})
|
||||
|
||||
t.Run("array types", func(t *testing.T) {
|
||||
arr := [3]int{1, 2, 3}
|
||||
result := TypeInfo(arr)
|
||||
assert.Equal(t, "[3]int", result)
|
||||
})
|
||||
|
||||
t.Run("complex types", func(t *testing.T) {
|
||||
type ComplexStruct struct {
|
||||
Data map[string][]int
|
||||
}
|
||||
result := TypeInfo(ComplexStruct{})
|
||||
assert.Equal(t, "formatting.ComplexStruct", result)
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var ptr *int
|
||||
result := TypeInfo(ptr)
|
||||
assert.Equal(t, "int", result, "Should handle nil pointer correctly")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTypeInfoWithCustomTypes(t *testing.T) {
|
||||
t.Run("custom type with methods", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := TypeInfo(mock)
|
||||
assert.Equal(t, "formatting.mockFormattable", result)
|
||||
})
|
||||
|
||||
t.Run("pointer to custom type", func(t *testing.T) {
|
||||
mock := &mockFormattable{
|
||||
stringValue: "test",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := TypeInfo(mock)
|
||||
assert.Equal(t, "formatting.mockFormattable", result, "Should remove pointer prefix")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFmtStringIntegration(t *testing.T) {
|
||||
t.Run("integration with fmt.Printf", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "integration test",
|
||||
goStringValue: "mock.GoString",
|
||||
}
|
||||
|
||||
// Test various format combinations
|
||||
tests := []struct {
|
||||
format string
|
||||
expected string
|
||||
}{
|
||||
{"%v", "integration test"},
|
||||
{"%+v", "integration test"},
|
||||
{"%#v", "mock.GoString"},
|
||||
{"%s", "integration test"},
|
||||
{"%q", `"integration test"`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.format, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.format, mock)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("integration with fmt.Fprintf", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "buffer test",
|
||||
goStringValue: "mock.GoString",
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
n, err := fmt.Fprintf((*mockWriter)(&buf), "%s", mock)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, n, 0)
|
||||
assert.Equal(t, "buffer test", string(buf))
|
||||
})
|
||||
}
|
||||
|
||||
// mockWriter is a simple writer for testing fmt.Fprintf
|
||||
type mockWriter []byte
|
||||
|
||||
func (m *mockWriter) Write(p []byte) (n int, err error) {
|
||||
*m = append(*m, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func BenchmarkFmtString(b *testing.B) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "benchmark test value",
|
||||
goStringValue: "mock.GoString",
|
||||
}
|
||||
|
||||
b.Run("format with %v", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%v", mock)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("format with %#v", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%#v", mock)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("format with %s", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%s", mock)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("format with %q", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%q", mock)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkTypeInfo(b *testing.B) {
|
||||
values := []any{
|
||||
42,
|
||||
"string",
|
||||
[]int{1, 2, 3},
|
||||
map[string]int{"a": 1},
|
||||
mockFormattable{},
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
b.Run(fmt.Sprintf("%T", v), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TypeInfo(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,8 @@ import (
|
||||
"time"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -330,7 +328,7 @@ func TestPrintf(t *testing.T) {
|
||||
|
||||
// Test ApplySemigroup
|
||||
func TestApplySemigroup(t *testing.T) {
|
||||
intAdd := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
intAdd := N.MonoidSum[int]()
|
||||
ioSemigroup := ApplySemigroup(intAdd)
|
||||
|
||||
result := ioSemigroup.Concat(Of(10), Of(32))
|
||||
@@ -339,7 +337,7 @@ func TestApplySemigroup(t *testing.T) {
|
||||
|
||||
// Test ApplicativeMonoid
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
intAdd := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
intAdd := N.MonoidSum[int]()
|
||||
ioMonoid := ApplicativeMonoid(intAdd)
|
||||
|
||||
result := ioMonoid.Concat(Of(10), Of(32))
|
||||
|
||||
@@ -45,7 +45,7 @@ func Requester(builder *R.Builder) IOEH.Requester {
|
||||
|
||||
withoutBody := F.Curry2(func(url string, method string) IOEither[*http.Request] {
|
||||
return ioeither.TryCatchError(func() (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
req, err := http.NewRequest(method, url, http.NoBody)
|
||||
if err == nil {
|
||||
H.Monoid.Concat(req.Header, builder.GetHeaders())
|
||||
}
|
||||
|
||||
@@ -17,15 +17,16 @@ package ioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// TailRec creates a tail-recursive computation in the IOEither monad.
|
||||
// It enables writing recursive algorithms that don't overflow the call stack by using
|
||||
// trampolining - a technique where recursive calls are converted into iterations.
|
||||
//
|
||||
// The function takes a step function that returns either:
|
||||
// - Left(A): Continue recursion with a new value of type A
|
||||
// - Right(B): Terminate recursion with a final result of type B
|
||||
// The function takes a step function that returns a Trampoline:
|
||||
// - Bounce(A): Continue recursion with a new value of type A
|
||||
// - Land(B): Terminate recursion with a final result of type B
|
||||
//
|
||||
// This is particularly useful for implementing recursive algorithms like:
|
||||
// - Iterative calculations (factorial, fibonacci, etc.)
|
||||
@@ -34,7 +35,7 @@ import (
|
||||
// - Processing collections with early termination
|
||||
//
|
||||
// The recursion is stack-safe because each step returns a value that indicates
|
||||
// whether to continue (Left) or stop (Right), rather than making direct recursive calls.
|
||||
// whether to continue (Bounce) or stop (Land), rather than making direct recursive calls.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type that may occur during computation
|
||||
@@ -43,7 +44,7 @@ import (
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A step function that takes the current state (A) and returns an IOEither
|
||||
// containing either Left(A) to continue with a new state, or Right(B) to
|
||||
// containing either Bounce(A) to continue with a new state, or Land(B) to
|
||||
// terminate with a final result
|
||||
//
|
||||
// Returns:
|
||||
@@ -57,13 +58,13 @@ import (
|
||||
// result int
|
||||
// }
|
||||
//
|
||||
// factorial := TailRec(func(state FactState) IOEither[error, Either[FactState, int]] {
|
||||
// factorial := TailRec(func(state FactState) IOEither[error, tailrec.Trampoline[FactState, int]] {
|
||||
// if state.n <= 1 {
|
||||
// // Terminate with final result
|
||||
// return Of[error](either.Right[FactState](state.result))
|
||||
// return Of[error](tailrec.Land[FactState](state.result))
|
||||
// }
|
||||
// // Continue with next iteration
|
||||
// return Of[error](either.Left[int](FactState{
|
||||
// return Of[error](tailrec.Bounce[int](FactState{
|
||||
// n: state.n - 1,
|
||||
// result: state.result * state.n,
|
||||
// }))
|
||||
@@ -78,36 +79,35 @@ import (
|
||||
// sum int
|
||||
// }
|
||||
//
|
||||
// processItems := TailRec(func(state ProcessState) IOEither[error, Either[ProcessState, int]] {
|
||||
// processItems := TailRec(func(state ProcessState) IOEither[error, tailrec.Trampoline[ProcessState, int]] {
|
||||
// if len(state.items) == 0 {
|
||||
// return Of[error](either.Right[ProcessState](state.sum))
|
||||
// return Of[error](tailrec.Land[ProcessState](state.sum))
|
||||
// }
|
||||
// val, err := strconv.Atoi(state.items[0])
|
||||
// if err != nil {
|
||||
// return Left[Either[ProcessState, int]](err)
|
||||
// return Left[tailrec.Trampoline[ProcessState, int]](err)
|
||||
// }
|
||||
// return Of[error](either.Left[int](ProcessState{
|
||||
// return Of[error](tailrec.Bounce[int](ProcessState{
|
||||
// items: state.items[1:],
|
||||
// sum: state.sum + val,
|
||||
// }))
|
||||
// })
|
||||
//
|
||||
// result := processItems(ProcessState{items: []string{"1", "2", "3"}, sum: 0})() // Right(6)
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
|
||||
return func(a A) IOEither[E, B] {
|
||||
initial := f(a)
|
||||
return func() Either[E, B] {
|
||||
return func() either.Either[E, B] {
|
||||
current := initial()
|
||||
for {
|
||||
r, e := either.Unwrap(current)
|
||||
if either.IsLeft(current) {
|
||||
return either.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(r)
|
||||
if either.IsRight(r) {
|
||||
return either.Right[E](b)
|
||||
if r.Landed {
|
||||
return either.Right[E](r.Land)
|
||||
}
|
||||
current = f(a)()
|
||||
current = f(r.Bounce)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -31,13 +33,13 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
result int
|
||||
}
|
||||
|
||||
factorial := TailRec(func(state FactState) IOEither[error, E.Either[FactState, int]] {
|
||||
factorial := TailRec(func(state FactState) IOEither[error, TR.Trampoline[FactState, int]] {
|
||||
if state.n <= 1 {
|
||||
// Terminate with final result
|
||||
return Of[error](E.Right[FactState](state.result))
|
||||
return Of[error](TR.Land[FactState](state.result))
|
||||
}
|
||||
// Continue with next iteration
|
||||
return Of[error](E.Left[int](FactState{
|
||||
return Of[error](TR.Bounce[int](FactState{
|
||||
n: state.n - 1,
|
||||
result: state.result * state.n,
|
||||
}))
|
||||
@@ -72,11 +74,11 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
curr int
|
||||
}
|
||||
|
||||
fibonacci := TailRec(func(state FibState) IOEither[error, E.Either[FibState, int]] {
|
||||
fibonacci := TailRec(func(state FibState) IOEither[error, TR.Trampoline[FibState, int]] {
|
||||
if state.n == 0 {
|
||||
return Of[error](E.Right[FibState](state.curr))
|
||||
return Of[error](TR.Land[FibState](state.curr))
|
||||
}
|
||||
return Of[error](E.Left[int](FibState{
|
||||
return Of[error](TR.Bounce[int](FibState{
|
||||
n: state.n - 1,
|
||||
prev: state.curr,
|
||||
curr: state.prev + state.curr,
|
||||
@@ -106,11 +108,11 @@ func TestTailRecSumList(t *testing.T) {
|
||||
sum int
|
||||
}
|
||||
|
||||
sumList := TailRec(func(state SumState) IOEither[error, E.Either[SumState, int]] {
|
||||
if len(state.items) == 0 {
|
||||
return Of[error](E.Right[SumState](state.sum))
|
||||
sumList := TailRec(func(state SumState) IOEither[error, TR.Trampoline[SumState, int]] {
|
||||
if A.IsEmpty(state.items) {
|
||||
return Of[error](TR.Land[SumState](state.sum))
|
||||
}
|
||||
return Of[error](E.Left[int](SumState{
|
||||
return Of[error](TR.Bounce[int](SumState{
|
||||
items: state.items[1:],
|
||||
sum: state.sum + state.items[0],
|
||||
}))
|
||||
@@ -140,14 +142,14 @@ func TestTailRecWithError(t *testing.T) {
|
||||
}
|
||||
|
||||
// Divide n by 2 repeatedly until it reaches 1, fail if we encounter an odd number > 1
|
||||
divideByTwo := TailRec(func(state DivState) IOEither[error, E.Either[DivState, int]] {
|
||||
divideByTwo := TailRec(func(state DivState) IOEither[error, TR.Trampoline[DivState, int]] {
|
||||
if state.n == 1 {
|
||||
return Of[error](E.Right[DivState](state.result))
|
||||
return Of[error](TR.Land[DivState](state.result))
|
||||
}
|
||||
if state.n%2 != 0 {
|
||||
return Left[E.Either[DivState, int]](fmt.Errorf("cannot divide odd number %d", state.n))
|
||||
return Left[TR.Trampoline[DivState, int]](fmt.Errorf("cannot divide odd number %d", state.n))
|
||||
}
|
||||
return Of[error](E.Left[int](DivState{
|
||||
return Of[error](TR.Bounce[int](DivState{
|
||||
n: state.n / 2,
|
||||
result: state.result + 1,
|
||||
}))
|
||||
@@ -182,11 +184,11 @@ func TestTailRecWithError(t *testing.T) {
|
||||
|
||||
// TestTailRecCountdown tests a simple countdown
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdown := TailRec(func(n int) IOEither[error, E.Either[int, string]] {
|
||||
countdown := TailRec(func(n int) IOEither[error, TR.Trampoline[int, string]] {
|
||||
if n <= 0 {
|
||||
return Of[error](E.Right[int]("Done!"))
|
||||
return Of[error](TR.Land[int]("Done!"))
|
||||
}
|
||||
return Of[error](E.Left[string](n - 1))
|
||||
return Of[error](TR.Bounce[string](n - 1))
|
||||
})
|
||||
|
||||
t.Run("countdown from 5", func(t *testing.T) {
|
||||
@@ -208,11 +210,11 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
// Count down from a large number - this would overflow the stack with regular recursion
|
||||
largeCountdown := TailRec(func(n int) IOEither[error, E.Either[int, int]] {
|
||||
largeCountdown := TailRec(func(n int) IOEither[error, TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return Of[error](E.Right[int](0))
|
||||
return Of[error](TR.Land[int](0))
|
||||
}
|
||||
return Of[error](E.Left[int](n - 1))
|
||||
return Of[error](TR.Bounce[int](n - 1))
|
||||
})
|
||||
|
||||
t.Run("large iteration count", func(t *testing.T) {
|
||||
@@ -230,14 +232,14 @@ func TestTailRecFindInList(t *testing.T) {
|
||||
index int
|
||||
}
|
||||
|
||||
findInList := TailRec(func(state FindState) IOEither[error, E.Either[FindState, int]] {
|
||||
if len(state.items) == 0 {
|
||||
return Left[E.Either[FindState, int]](errors.New("not found"))
|
||||
findInList := TailRec(func(state FindState) IOEither[error, TR.Trampoline[FindState, int]] {
|
||||
if A.IsEmpty(state.items) {
|
||||
return Left[TR.Trampoline[FindState, int]](errors.New("not found"))
|
||||
}
|
||||
if state.items[0] == state.target {
|
||||
return Of[error](E.Right[FindState](state.index))
|
||||
return Of[error](TR.Land[FindState](state.index))
|
||||
}
|
||||
return Of[error](E.Left[int](FindState{
|
||||
return Of[error](TR.Bounce[int](FindState{
|
||||
items: state.items[1:],
|
||||
target: state.target,
|
||||
index: state.index + 1,
|
||||
@@ -283,5 +285,3 @@ func TestTailRecFindInList(t *testing.T) {
|
||||
assert.True(t, E.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// Do creates an empty context of type [S] to be used with the [Bind] operation.
|
||||
@@ -75,7 +74,7 @@ func Do[S any](
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
) Operator[S1, S2] {
|
||||
return chain.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
@@ -88,7 +87,7 @@ func Bind[S1, S2, T any](
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
) Operator[S1, S2] {
|
||||
return functor.Let(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -100,7 +99,7 @@ func Let[S1, S2, T any](
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
) Operator[S1, S2] {
|
||||
return functor.LetTo(
|
||||
Map[S1, S2],
|
||||
setter,
|
||||
@@ -111,13 +110,20 @@ func LetTo[S1, S2, T any](
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Kleisli[IOOption[T], S1] {
|
||||
) Operator[T, S1] {
|
||||
return chain.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindToP[S1, T any](
|
||||
setter Prism[S1, T],
|
||||
) Operator[T, S1] {
|
||||
return BindTo(setter.ReverseGet)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
// the context and the value concurrently (using Applicative rather than Monad).
|
||||
// This allows independent computations to be combined without one depending on the result of the other.
|
||||
@@ -154,7 +160,7 @@ func BindTo[S1, T any](
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOOption[T],
|
||||
) Kleisli[IOOption[S1], S2] {
|
||||
) Operator[S1, S2] {
|
||||
return apply.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
@@ -187,9 +193,9 @@ func ApS[S1, S2, T any](
|
||||
// iooption.ApSL(ageLens, iooption.Some(30)),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
fa IOOption[T],
|
||||
) Kleisli[IOOption[S], S] {
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
@@ -222,9 +228,9 @@ func ApSL[S, T any](
|
||||
// iooption.BindL(valueLens, increment),
|
||||
// ) // IOOption[Counter{Value: 43}]
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[IOOption[S], S] {
|
||||
) Operator[S, S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
@@ -255,9 +261,9 @@ func BindL[S, T any](
|
||||
// iooption.LetL(valueLens, double),
|
||||
// ) // IOOption[Counter{Value: 42}]
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
f func(T) T,
|
||||
) Kleisli[IOOption[S], S] {
|
||||
) Operator[S, S] {
|
||||
return Let(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
@@ -286,8 +292,8 @@ func LetL[S, T any](
|
||||
// iooption.LetToL(debugLens, false),
|
||||
// ) // IOOption[Config{Debug: false, Timeout: 30}]
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Kleisli[IOOption[S], S] {
|
||||
) Operator[S, S] {
|
||||
return LetTo(lens.Set, b)
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ func FromEither[E, A any](e Either[E, A]) IOOption[A] {
|
||||
}
|
||||
|
||||
// MonadAlt identifies an associative operation on a type constructor
|
||||
func MonadAlt[A any](first IOOption[A], second IOOption[A]) IOOption[A] {
|
||||
func MonadAlt[A any](first, second IOOption[A]) IOOption[A] {
|
||||
return optiont.MonadAlt(
|
||||
io.MonadOf[Option[A]],
|
||||
io.MonadChain[Option[A], Option[A]],
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
package iooption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// TailRec creates a tail-recursive computation in the IOOption monad.
|
||||
// It enables writing recursive algorithms that don't overflow the call stack by using
|
||||
// an iterative loop - a technique where recursive calls are converted into iterations.
|
||||
//
|
||||
// The function takes a step function that returns an IOOption containing either:
|
||||
// The function takes a step function that returns an IOOption containing a Trampoline:
|
||||
// - None: Terminate recursion with no result
|
||||
// - Some(Left(A)): Continue recursion with a new value of type A
|
||||
// - Some(Right(B)): Terminate recursion with a final result of type B
|
||||
// - Some(Bounce(A)): Continue recursion with a new value of type A
|
||||
// - Some(Land(B)): Terminate recursion with a final result of type B
|
||||
//
|
||||
// This is particularly useful for implementing recursive algorithms that may fail at any step:
|
||||
// - Iterative calculations that may not produce a result
|
||||
@@ -45,8 +45,8 @@ import (
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A step function that takes the current state (A) and returns an IOOption
|
||||
// containing either None (failure), Some(Left(A)) to continue with a new state,
|
||||
// or Some(Right(B)) to terminate with a final result
|
||||
// containing either None (failure), Some(Bounce(A)) to continue with a new state,
|
||||
// or Some(Land(B)) to terminate with a final result
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow (function from A to IOOption[B]) that executes the
|
||||
@@ -59,17 +59,17 @@ import (
|
||||
// result int
|
||||
// }
|
||||
//
|
||||
// factorial := TailRec[any](func(state FactState) IOOption[Either[FactState, int]] {
|
||||
// factorial := TailRec[any](func(state FactState) IOOption[tailrec.Trampoline[FactState, int]] {
|
||||
// if state.n < 0 {
|
||||
// // Negative numbers have no factorial
|
||||
// return None[Either[FactState, int]]()
|
||||
// return None[tailrec.Trampoline[FactState, int]]()
|
||||
// }
|
||||
// if state.n <= 1 {
|
||||
// // Terminate with final result
|
||||
// return Of(either.Right[FactState](state.result))
|
||||
// return Of(tailrec.Land[FactState](state.result))
|
||||
// }
|
||||
// // Continue with next iteration
|
||||
// return Of(either.Left[int](FactState{
|
||||
// return Of(tailrec.Bounce[int](FactState{
|
||||
// n: state.n - 1,
|
||||
// result: state.result * state.n,
|
||||
// }))
|
||||
@@ -86,14 +86,14 @@ import (
|
||||
// steps int
|
||||
// }
|
||||
//
|
||||
// safeDivide := TailRec[any](func(state DivState) IOOption[Either[DivState, int]] {
|
||||
// safeDivide := TailRec[any](func(state DivState) IOOption[tailrec.Trampoline[DivState, int]] {
|
||||
// if state.denominator == 0 {
|
||||
// return None[Either[DivState, int]]() // Division by zero
|
||||
// return None[tailrec.Trampoline[DivState, int]]() // Division by zero
|
||||
// }
|
||||
// if state.numerator < state.denominator {
|
||||
// return Of(either.Right[DivState](state.steps))
|
||||
// return Of(tailrec.Land[DivState](state.steps))
|
||||
// }
|
||||
// return Of(either.Left[int](DivState{
|
||||
// return Of(tailrec.Bounce[int](DivState{
|
||||
// numerator: state.numerator - state.denominator,
|
||||
// denominator: state.denominator,
|
||||
// steps: state.steps + 1,
|
||||
@@ -102,21 +102,20 @@ import (
|
||||
//
|
||||
// result := safeDivide(DivState{numerator: 10, denominator: 3, steps: 0})() // Some(3)
|
||||
// result := safeDivide(DivState{numerator: 10, denominator: 0, steps: 0})() // None
|
||||
func TailRec[E, A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[E, A, B any](f Kleisli[A, tailrec.Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return func(a A) IOOption[B] {
|
||||
initial := f(a)
|
||||
return func() Option[B] {
|
||||
return func() option.Option[B] {
|
||||
current := initial()
|
||||
for {
|
||||
r, ok := option.Unwrap(current)
|
||||
if !ok {
|
||||
return option.None[B]()
|
||||
}
|
||||
b, a := either.Unwrap(r)
|
||||
if either.IsRight(r) {
|
||||
return option.Some(b)
|
||||
if r.Landed {
|
||||
return option.Some(r.Land)
|
||||
}
|
||||
current = f(a)()
|
||||
current = f(r.Bounce)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ package iooption
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -30,17 +30,17 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
result int
|
||||
}
|
||||
|
||||
factorial := TailRec[any](func(state FactState) IOOption[E.Either[FactState, int]] {
|
||||
factorial := TailRec[any](func(state FactState) IOOption[TR.Trampoline[FactState, int]] {
|
||||
if state.n < 0 {
|
||||
// Negative numbers have no factorial
|
||||
return None[E.Either[FactState, int]]()
|
||||
return None[TR.Trampoline[FactState, int]]()
|
||||
}
|
||||
if state.n <= 1 {
|
||||
// Terminate with final result
|
||||
return Of(E.Right[FactState](state.result))
|
||||
return Of(TR.Land[FactState](state.result))
|
||||
}
|
||||
// Continue with next iteration
|
||||
return Of(E.Left[int](FactState{
|
||||
return Of(TR.Bounce[int](FactState{
|
||||
n: state.n - 1,
|
||||
result: state.result * state.n,
|
||||
}))
|
||||
@@ -80,14 +80,14 @@ func TestTailRecSafeDivision(t *testing.T) {
|
||||
steps int
|
||||
}
|
||||
|
||||
safeDivide := TailRec[any](func(state DivState) IOOption[E.Either[DivState, int]] {
|
||||
safeDivide := TailRec[any](func(state DivState) IOOption[TR.Trampoline[DivState, int]] {
|
||||
if state.denominator == 0 {
|
||||
return None[E.Either[DivState, int]]() // Division by zero
|
||||
return None[TR.Trampoline[DivState, int]]() // Division by zero
|
||||
}
|
||||
if state.numerator < state.denominator {
|
||||
return Of(E.Right[DivState](state.steps))
|
||||
return Of(TR.Land[DivState](state.steps))
|
||||
}
|
||||
return Of(E.Left[int](DivState{
|
||||
return Of(TR.Bounce[int](DivState{
|
||||
numerator: state.numerator - state.denominator,
|
||||
denominator: state.denominator,
|
||||
steps: state.steps + 1,
|
||||
@@ -123,14 +123,14 @@ func TestTailRecFindInRange(t *testing.T) {
|
||||
max int
|
||||
}
|
||||
|
||||
findInRange := TailRec[any](func(state FindState) IOOption[E.Either[FindState, int]] {
|
||||
findInRange := TailRec[any](func(state FindState) IOOption[TR.Trampoline[FindState, int]] {
|
||||
if state.current > state.max {
|
||||
return None[E.Either[FindState, int]]() // Not found
|
||||
return None[TR.Trampoline[FindState, int]]() // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return Of(E.Right[FindState](state.current))
|
||||
return Of(TR.Land[FindState](state.current))
|
||||
}
|
||||
return Of(E.Left[int](FindState{
|
||||
return Of(TR.Bounce[int](FindState{
|
||||
current: state.current + 1,
|
||||
target: state.target,
|
||||
max: state.max,
|
||||
@@ -166,14 +166,14 @@ func TestTailRecSumUntilLimit(t *testing.T) {
|
||||
limit int
|
||||
}
|
||||
|
||||
sumUntilLimit := TailRec[any](func(state SumState) IOOption[E.Either[SumState, int]] {
|
||||
sumUntilLimit := TailRec[any](func(state SumState) IOOption[TR.Trampoline[SumState, int]] {
|
||||
if state.sum > state.limit {
|
||||
return None[E.Either[SumState, int]]() // Exceeded limit
|
||||
return None[TR.Trampoline[SumState, int]]() // Exceeded limit
|
||||
}
|
||||
if state.current <= 0 {
|
||||
return Of(E.Right[SumState](state.sum))
|
||||
return Of(TR.Land[SumState](state.sum))
|
||||
}
|
||||
return Of(E.Left[int](SumState{
|
||||
return Of(TR.Bounce[int](SumState{
|
||||
current: state.current - 1,
|
||||
sum: state.sum + state.current,
|
||||
limit: state.limit,
|
||||
@@ -198,14 +198,14 @@ func TestTailRecSumUntilLimit(t *testing.T) {
|
||||
|
||||
// TestTailRecCountdown tests a simple countdown with optional result
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdown := TailRec[any](func(n int) IOOption[E.Either[int, string]] {
|
||||
countdown := TailRec[any](func(n int) IOOption[TR.Trampoline[int, string]] {
|
||||
if n < 0 {
|
||||
return None[E.Either[int, string]]() // Negative not allowed
|
||||
return None[TR.Trampoline[int, string]]() // Negative not allowed
|
||||
}
|
||||
if n == 0 {
|
||||
return Of(E.Right[int]("Done!"))
|
||||
return Of(TR.Land[int]("Done!"))
|
||||
}
|
||||
return Of(E.Left[string](n - 1))
|
||||
return Of(TR.Bounce[string](n - 1))
|
||||
})
|
||||
|
||||
t.Run("countdown from 5", func(t *testing.T) {
|
||||
@@ -227,14 +227,14 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
// Count down from a large number - this would overflow the stack with regular recursion
|
||||
largeCountdown := TailRec[any](func(n int) IOOption[E.Either[int, int]] {
|
||||
largeCountdown := TailRec[any](func(n int) IOOption[TR.Trampoline[int, int]] {
|
||||
if n < 0 {
|
||||
return None[E.Either[int, int]]()
|
||||
return None[TR.Trampoline[int, int]]()
|
||||
}
|
||||
if n == 0 {
|
||||
return Of(E.Right[int](0))
|
||||
return Of(TR.Land[int](0))
|
||||
}
|
||||
return Of(E.Left[int](n - 1))
|
||||
return Of(TR.Bounce[int](n - 1))
|
||||
})
|
||||
|
||||
t.Run("large iteration count", func(t *testing.T) {
|
||||
@@ -252,14 +252,14 @@ func TestTailRecValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
// Validate all items are positive, return count if valid
|
||||
validatePositive := TailRec[any](func(state ValidationState) IOOption[E.Either[ValidationState, int]] {
|
||||
validatePositive := TailRec[any](func(state ValidationState) IOOption[TR.Trampoline[ValidationState, int]] {
|
||||
if state.index >= len(state.items) {
|
||||
return Of(E.Right[ValidationState](state.index))
|
||||
return Of(TR.Land[ValidationState](state.index))
|
||||
}
|
||||
if state.items[state.index] <= 0 {
|
||||
return None[E.Either[ValidationState, int]]() // Invalid item
|
||||
return None[TR.Trampoline[ValidationState, int]]() // Invalid item
|
||||
}
|
||||
return Of(E.Left[int](ValidationState{
|
||||
return Of(TR.Bounce[int](ValidationState{
|
||||
items: state.items,
|
||||
index: state.index + 1,
|
||||
}))
|
||||
@@ -294,17 +294,17 @@ func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
}
|
||||
|
||||
// Count steps to reach 1 in Collatz sequence
|
||||
collatz := TailRec[any](func(state CollatzState) IOOption[E.Either[CollatzState, int]] {
|
||||
collatz := TailRec[any](func(state CollatzState) IOOption[TR.Trampoline[CollatzState, int]] {
|
||||
if state.n <= 0 {
|
||||
return None[E.Either[CollatzState, int]]() // Invalid input
|
||||
return None[TR.Trampoline[CollatzState, int]]() // Invalid input
|
||||
}
|
||||
if state.n == 1 {
|
||||
return Of(E.Right[CollatzState](state.steps))
|
||||
return Of(TR.Land[CollatzState](state.steps))
|
||||
}
|
||||
if state.n%2 == 0 {
|
||||
return Of(E.Left[int](CollatzState{n: state.n / 2, steps: state.steps + 1}))
|
||||
return Of(TR.Bounce[int](CollatzState{n: state.n / 2, steps: state.steps + 1}))
|
||||
}
|
||||
return Of(E.Left[int](CollatzState{n: 3*state.n + 1, steps: state.steps + 1}))
|
||||
return Of(TR.Bounce[int](CollatzState{n: 3*state.n + 1, steps: state.steps + 1}))
|
||||
})
|
||||
|
||||
t.Run("collatz for 1", func(t *testing.T) {
|
||||
@@ -332,5 +332,3 @@ func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user