mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-04 11:33:51 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49deb57d24 | ||
|
|
abb55ddbd0 | ||
|
|
f6b01dffdc | ||
|
|
43b666edbb | ||
|
|
e42d765852 | ||
|
|
d2da8a32b4 | ||
|
|
7484af664b | ||
|
|
ae38e3f8f4 | ||
|
|
e0f854bda3 | ||
|
|
34786c3cd8 | ||
|
|
a7aa7e3560 | ||
|
|
ff2a4299b2 | ||
|
|
edd66d63e6 | ||
|
|
909aec8eba | ||
|
|
da0344f9bd | ||
|
|
cd79dd56b9 |
1
v2/.bob/mcp.json
Normal file
1
v2/.bob/mcp.json
Normal file
@@ -0,0 +1 @@
|
||||
{"mcpServers":{}}
|
||||
@@ -460,8 +460,11 @@ func process() IOResult[string] {
|
||||
- **Either** - Type-safe error handling with left/right values
|
||||
- **Result** - Simplified Either with error as left type (recommended for error handling)
|
||||
- **IO** - Lazy evaluation and side effect management
|
||||
- **IOOption** - Combine IO with Option for optional values with side effects
|
||||
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
|
||||
- **Reader** - Dependency injection pattern
|
||||
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
|
||||
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
|
||||
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
|
||||
@@ -239,6 +239,16 @@ func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B {
|
||||
}
|
||||
|
||||
// Append adds an element to the end of an array, returning a new array.
|
||||
// This is a non-curried version that takes both the array and element as parameters.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// arr := []int{1, 2, 3}
|
||||
// result := array.Append(arr, 4)
|
||||
// // result: []int{1, 2, 3, 4}
|
||||
// // arr: []int{1, 2, 3} (unchanged)
|
||||
//
|
||||
// For a curried version, see Push.
|
||||
//
|
||||
//go:inline
|
||||
func Append[A any](as []A, a A) []A {
|
||||
|
||||
@@ -16,308 +16,88 @@
|
||||
package array
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReplicate(t *testing.T) {
|
||||
result := Replicate(3, "a")
|
||||
assert.Equal(t, []string{"a", "a", "a"}, result)
|
||||
|
||||
empty := Replicate(0, 42)
|
||||
assert.Equal(t, []int{}, empty)
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadMap(src, N.Mul(2))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestMonadMapRef(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadMapRef(src, func(x *int) int { return *x * 2 })
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestMapWithIndex(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
mapper := MapWithIndex(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := mapper(src)
|
||||
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result)
|
||||
}
|
||||
|
||||
func TestMapRef(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
mapper := MapRef(func(x *int) int { return *x * 2 })
|
||||
result := mapper(src)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestFilterWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
filter := FilterWithIndex(func(i, x int) bool {
|
||||
return i%2 == 0 && x > 2
|
||||
})
|
||||
result := filter(src)
|
||||
assert.Equal(t, []int{3, 5}, result)
|
||||
}
|
||||
|
||||
func TestFilterRef(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
filter := FilterRef(func(x *int) bool { return *x > 2 })
|
||||
result := filter(src)
|
||||
assert.Equal(t, []int{3, 4, 5}, result)
|
||||
}
|
||||
|
||||
func TestMonadFilterMap(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4}
|
||||
result := MonadFilterMap(src, func(x int) O.Option[string] {
|
||||
if x%2 == 0 {
|
||||
return O.Some(fmt.Sprintf("even:%d", x))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
assert.Equal(t, []string{"even:2", "even:4"}, result)
|
||||
}
|
||||
|
||||
func TestMonadFilterMapWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4}
|
||||
result := MonadFilterMapWithIndex(src, func(i, x int) O.Option[string] {
|
||||
if i%2 == 0 {
|
||||
return O.Some(fmt.Sprintf("%d:%d", i, x))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
assert.Equal(t, []string{"0:1", "2:3"}, result)
|
||||
}
|
||||
|
||||
func TestFilterMapWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4}
|
||||
filter := FilterMapWithIndex(func(i, x int) O.Option[string] {
|
||||
if i%2 == 0 {
|
||||
return O.Some(fmt.Sprintf("%d:%d", i, x))
|
||||
}
|
||||
return O.None[string]()
|
||||
})
|
||||
result := filter(src)
|
||||
assert.Equal(t, []string{"0:1", "2:3"}, result)
|
||||
}
|
||||
|
||||
func TestFilterMapRef(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
filter := FilterMapRef(
|
||||
func(x *int) bool { return *x > 2 },
|
||||
func(x *int) string { return fmt.Sprintf("val:%d", *x) },
|
||||
)
|
||||
result := filter(src)
|
||||
assert.Equal(t, []string{"val:3", "val:4", "val:5"}, result)
|
||||
}
|
||||
|
||||
func TestReduceWithIndex(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
reducer := ReduceWithIndex(func(i, acc, x int) int {
|
||||
return acc + i + x
|
||||
// TestMonadReduceWithIndex tests the MonadReduceWithIndex function
|
||||
func TestMonadReduceWithIndex(t *testing.T) {
|
||||
// Test with integers - sum with index multiplication
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
result := MonadReduceWithIndex(numbers, func(idx, acc, val int) int {
|
||||
return acc + (val * idx)
|
||||
}, 0)
|
||||
result := reducer(src)
|
||||
assert.Equal(t, 9, result) // 0 + (0+1) + (1+2) + (2+3) = 9
|
||||
}
|
||||
// Expected: 0*1 + 1*2 + 2*3 + 3*4 + 4*5 = 0 + 2 + 6 + 12 + 20 = 40
|
||||
assert.Equal(t, 40, result)
|
||||
|
||||
func TestReduceRightWithIndex(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
reducer := ReduceRightWithIndex(func(i int, x, acc string) string {
|
||||
return fmt.Sprintf("%s%d:%s", acc, i, x)
|
||||
// Test with empty array
|
||||
empty := []int{}
|
||||
result2 := MonadReduceWithIndex(empty, func(idx, acc, val int) int {
|
||||
return acc + val
|
||||
}, 10)
|
||||
assert.Equal(t, 10, result2)
|
||||
|
||||
// Test with strings - concatenate with index
|
||||
words := []string{"a", "b", "c"}
|
||||
result3 := MonadReduceWithIndex(words, func(idx int, acc, val string) string {
|
||||
return acc + val + string(rune('0'+idx))
|
||||
}, "")
|
||||
result := reducer(src)
|
||||
assert.Equal(t, "2:c1:b0:a", result)
|
||||
assert.Equal(t, "a0b1c2", result3)
|
||||
}
|
||||
|
||||
func TestReduceRef(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
reducer := ReduceRef(func(acc int, x *int) int {
|
||||
return acc + *x
|
||||
}, 0)
|
||||
result := reducer(src)
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
func TestZero(t *testing.T) {
|
||||
result := Zero[int]()
|
||||
assert.Equal(t, []int{}, result)
|
||||
assert.True(t, IsEmpty(result))
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
result := MonadChain(src, func(x int) []int {
|
||||
return []int{x, x * 10}
|
||||
})
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
chain := Chain(func(x int) []int {
|
||||
return []int{x, x * 10}
|
||||
})
|
||||
result := chain(src)
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fns := []func(int) int{
|
||||
N.Mul(2),
|
||||
N.Add(10),
|
||||
}
|
||||
values := []int{1, 2}
|
||||
result := MonadAp(fns, values)
|
||||
assert.Equal(t, []int{2, 4, 11, 12}, result)
|
||||
}
|
||||
|
||||
func TestMatchLeft(t *testing.T) {
|
||||
matcher := MatchLeft(
|
||||
func() string { return "empty" },
|
||||
func(head int, tail []int) string {
|
||||
return fmt.Sprintf("head:%d,tail:%v", head, tail)
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, "empty", matcher([]int{}))
|
||||
assert.Equal(t, "head:1,tail:[2 3]", matcher([]int{1, 2, 3}))
|
||||
}
|
||||
|
||||
func TestTail(t *testing.T) {
|
||||
assert.Equal(t, O.None[[]int](), Tail([]int{}))
|
||||
assert.Equal(t, O.Some([]int{2, 3}), Tail([]int{1, 2, 3}))
|
||||
assert.Equal(t, O.Some([]int{}), Tail([]int{1}))
|
||||
}
|
||||
|
||||
func TestFirst(t *testing.T) {
|
||||
assert.Equal(t, O.None[int](), First([]int{}))
|
||||
assert.Equal(t, O.Some(1), First([]int{1, 2, 3}))
|
||||
}
|
||||
|
||||
func TestLast(t *testing.T) {
|
||||
assert.Equal(t, O.None[int](), Last([]int{}))
|
||||
assert.Equal(t, O.Some(3), Last([]int{1, 2, 3}))
|
||||
assert.Equal(t, O.Some(1), Last([]int{1}))
|
||||
}
|
||||
|
||||
func TestUpsertAt(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
upsert := UpsertAt(99)
|
||||
|
||||
result1 := upsert(src)
|
||||
assert.Equal(t, []int{1, 2, 3, 99}, result1)
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
assert.Equal(t, 0, Size([]int{}))
|
||||
assert.Equal(t, 3, Size([]int{1, 2, 3}))
|
||||
}
|
||||
|
||||
func TestMonadPartition(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
result := MonadPartition(src, func(x int) bool { return x > 2 })
|
||||
assert.Equal(t, []int{1, 2}, result.F1)
|
||||
assert.Equal(t, []int{3, 4, 5}, result.F2)
|
||||
}
|
||||
|
||||
func TestIsNil(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.True(t, IsNil(nilSlice))
|
||||
assert.False(t, IsNil([]int{}))
|
||||
assert.False(t, IsNil([]int{1}))
|
||||
}
|
||||
|
||||
func TestIsNonNil(t *testing.T) {
|
||||
var nilSlice []int
|
||||
assert.False(t, IsNonNil(nilSlice))
|
||||
assert.True(t, IsNonNil([]int{}))
|
||||
assert.True(t, IsNonNil([]int{1}))
|
||||
}
|
||||
|
||||
func TestConstNil(t *testing.T) {
|
||||
result := ConstNil[int]()
|
||||
assert.True(t, IsNil(result))
|
||||
}
|
||||
|
||||
func TestSliceRight(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
slicer := SliceRight[int](2)
|
||||
result := slicer(src)
|
||||
assert.Equal(t, []int{3, 4, 5}, result)
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
copied := Copy(src)
|
||||
assert.Equal(t, src, copied)
|
||||
// Verify it's a different slice
|
||||
copied[0] = 99
|
||||
assert.Equal(t, 1, src[0])
|
||||
assert.Equal(t, 99, copied[0])
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
cloner := Clone(N.Mul(2))
|
||||
result := cloner(src)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestFoldMapWithIndex(t *testing.T) {
|
||||
src := []string{"a", "b", "c"}
|
||||
folder := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := folder(src)
|
||||
assert.Equal(t, "0:a1:b2:c", result)
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
src := []int{1, 2, 3, 4, 5}
|
||||
folder := Fold(N.MonoidSum[int]())
|
||||
result := folder(src)
|
||||
assert.Equal(t, 15, result)
|
||||
}
|
||||
|
||||
func TestPush(t *testing.T) {
|
||||
src := []int{1, 2, 3}
|
||||
pusher := Push(4)
|
||||
result := pusher(src)
|
||||
// TestAppend tests the Append function
|
||||
func TestAppend(t *testing.T) {
|
||||
// Test appending to non-empty array
|
||||
arr := []int{1, 2, 3}
|
||||
result := Append(arr, 4)
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
// Verify original array is unchanged
|
||||
assert.Equal(t, []int{1, 2, 3}, arr)
|
||||
|
||||
// Test appending to empty array
|
||||
empty := []int{}
|
||||
result2 := Append(empty, 1)
|
||||
assert.Equal(t, []int{1}, result2)
|
||||
|
||||
// Test appending strings
|
||||
words := []string{"hello", "world"}
|
||||
result3 := Append(words, "!")
|
||||
assert.Equal(t, []string{"hello", "world", "!"}, result3)
|
||||
|
||||
// Test appending to nil array
|
||||
var nilArr []int
|
||||
result4 := Append(nilArr, 42)
|
||||
assert.Equal(t, []int{42}, result4)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fns := []func(int) string{
|
||||
func(x int) string { return fmt.Sprintf("a%d", x) },
|
||||
func(x int) string { return fmt.Sprintf("b%d", x) },
|
||||
}
|
||||
result := MonadFlap(fns, 5)
|
||||
assert.Equal(t, []string{"a5", "b5"}, result)
|
||||
}
|
||||
// TestStrictEquals tests the StrictEquals function
|
||||
func TestStrictEquals(t *testing.T) {
|
||||
eq := StrictEquals[int]()
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
fns := []func(int) string{
|
||||
func(x int) string { return fmt.Sprintf("a%d", x) },
|
||||
func(x int) string { return fmt.Sprintf("b%d", x) },
|
||||
}
|
||||
flapper := Flap[string](5)
|
||||
result := flapper(fns)
|
||||
assert.Equal(t, []string{"a5", "b5"}, result)
|
||||
}
|
||||
// Test equal arrays
|
||||
arr1 := []int{1, 2, 3}
|
||||
arr2 := []int{1, 2, 3}
|
||||
assert.True(t, eq.Equals(arr1, arr2))
|
||||
|
||||
func TestPrepend(t *testing.T) {
|
||||
src := []int{2, 3, 4}
|
||||
prepender := Prepend(1)
|
||||
result := prepender(src)
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
// Test different arrays
|
||||
arr3 := []int{1, 2, 4}
|
||||
assert.False(t, eq.Equals(arr1, arr3))
|
||||
|
||||
// Test different lengths
|
||||
arr4 := []int{1, 2}
|
||||
assert.False(t, eq.Equals(arr1, arr4))
|
||||
|
||||
// Test empty arrays
|
||||
empty1 := []int{}
|
||||
empty2 := []int{}
|
||||
assert.True(t, eq.Equals(empty1, empty2))
|
||||
|
||||
// Test with strings
|
||||
strEq := StrictEquals[string]()
|
||||
words1 := []string{"hello", "world"}
|
||||
words2 := []string{"hello", "world"}
|
||||
words3 := []string{"hello", "there"}
|
||||
assert.True(t, strEq.Equals(words1, words2))
|
||||
assert.False(t, strEq.Equals(words1, words3))
|
||||
}
|
||||
|
||||
@@ -63,17 +63,26 @@ func Bind[S1, S2, T any](
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value T rather than []T.
|
||||
// This is useful when you need to compute a derived value from the current context
|
||||
// without introducing additional array elements.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := array.Let(
|
||||
// func(sum int) func(s struct{ X int }) struct{ X, Sum int } {
|
||||
// return func(s struct{ X int }) struct{ X, Sum int } {
|
||||
// return struct{ X, Sum int }{s.X, sum}
|
||||
// }
|
||||
// },
|
||||
// func(s struct{ X int }) int { return s.X * 2 },
|
||||
// type State1 struct{ X int }
|
||||
// type State2 struct{ X, Double int }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// []State1{{X: 5}, {X: 10}},
|
||||
// array.Let(
|
||||
// func(double int) func(s State1) State2 {
|
||||
// return func(s State1) State2 {
|
||||
// return State2{X: s.X, Double: double}
|
||||
// }
|
||||
// },
|
||||
// func(s State1) int { return s.X * 2 },
|
||||
// ),
|
||||
// )
|
||||
// // result: []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
@@ -84,18 +93,25 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
// This is useful for adding constant values to the context.
|
||||
// This is useful for adding constant values to the context without computation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := array.LetTo(
|
||||
// func(name string) func(s struct{ X int }) struct{ X int; Name string } {
|
||||
// return func(s struct{ X int }) struct{ X int; Name string } {
|
||||
// return struct{ X int; Name string }{s.X, name}
|
||||
// }
|
||||
// },
|
||||
// "constant",
|
||||
// type State1 struct{ X int }
|
||||
// type State2 struct{ X int; Name string }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// []State1{{X: 1}, {X: 2}},
|
||||
// array.LetTo(
|
||||
// func(name string) func(s State1) State2 {
|
||||
// return func(s State1) State2 {
|
||||
// return State2{X: s.X, Name: name}
|
||||
// }
|
||||
// },
|
||||
// "constant",
|
||||
// ),
|
||||
// )
|
||||
// // result: []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
@@ -107,15 +123,19 @@ func LetTo[S1, S2, T any](
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically the first operation after Do to start building the context.
|
||||
// It transforms each element of type T into a state of type S1.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct{ X int }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// []int{1, 2, 3},
|
||||
// array.BindTo(func(x int) struct{ X int } {
|
||||
// return struct{ X int }{x}
|
||||
// array.BindTo(func(x int) State {
|
||||
// return State{X: x}
|
||||
// }),
|
||||
// )
|
||||
// // result: []State{{X: 1}, {X: 2}, {X: 3}}
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
|
||||
@@ -22,57 +22,176 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestState1 struct {
|
||||
X int
|
||||
}
|
||||
|
||||
type TestState2 struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
// TestLet tests the Let function
|
||||
func TestLet(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(TestState1{}),
|
||||
type State1 struct {
|
||||
X int
|
||||
}
|
||||
type State2 struct {
|
||||
X int
|
||||
Double int
|
||||
}
|
||||
|
||||
// Test Let with pure computation
|
||||
result := F.Pipe1(
|
||||
[]State1{{X: 5}, {X: 10}},
|
||||
Let(
|
||||
func(y int) func(s TestState1) TestState2 {
|
||||
return func(s TestState1) TestState2 {
|
||||
return TestState2{X: s.X, Y: y}
|
||||
func(double int) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Double: double}
|
||||
}
|
||||
},
|
||||
func(s TestState1) int { return s.X * 2 },
|
||||
func(s State1) int { return s.X * 2 },
|
||||
),
|
||||
Map(func(s TestState2) int { return s.X + s.Y }),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{0}, result)
|
||||
expected := []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Test Let with empty array
|
||||
empty := []State1{}
|
||||
result2 := F.Pipe1(
|
||||
empty,
|
||||
Let(
|
||||
func(double int) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Double: double}
|
||||
}
|
||||
},
|
||||
func(s State1) int { return s.X * 2 },
|
||||
),
|
||||
)
|
||||
assert.Equal(t, []State2{}, result2)
|
||||
}
|
||||
|
||||
// TestLetTo tests the LetTo function
|
||||
func TestLetTo(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(TestState1{X: 5}),
|
||||
type State1 struct {
|
||||
X int
|
||||
}
|
||||
type State2 struct {
|
||||
X int
|
||||
Name string
|
||||
}
|
||||
|
||||
// Test LetTo with constant value
|
||||
result := F.Pipe1(
|
||||
[]State1{{X: 1}, {X: 2}},
|
||||
LetTo(
|
||||
func(y int) func(s TestState1) TestState2 {
|
||||
return func(s TestState1) TestState2 {
|
||||
return TestState2{X: s.X, Y: y}
|
||||
func(name string) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Name: name}
|
||||
}
|
||||
},
|
||||
42,
|
||||
"constant",
|
||||
),
|
||||
Map(func(s TestState2) int { return s.X + s.Y }),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{47}, result)
|
||||
expected := []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Test LetTo with different constant
|
||||
result2 := F.Pipe1(
|
||||
[]State1{{X: 10}},
|
||||
LetTo(
|
||||
func(name string) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Name: name}
|
||||
}
|
||||
},
|
||||
"test",
|
||||
),
|
||||
)
|
||||
|
||||
expected2 := []State2{{X: 10, Name: "test"}}
|
||||
assert.Equal(t, expected2, result2)
|
||||
}
|
||||
|
||||
// TestBindTo tests the BindTo function
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
X int
|
||||
}
|
||||
|
||||
// Test BindTo with integers
|
||||
result := F.Pipe1(
|
||||
[]int{1, 2, 3},
|
||||
BindTo(func(x int) TestState1 {
|
||||
return TestState1{X: x}
|
||||
BindTo(func(x int) State {
|
||||
return State{X: x}
|
||||
}),
|
||||
)
|
||||
|
||||
expected := []TestState1{{X: 1}, {X: 2}, {X: 3}}
|
||||
expected := []State{{X: 1}, {X: 2}, {X: 3}}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// Test BindTo with strings
|
||||
type StringState struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
result2 := F.Pipe1(
|
||||
[]string{"hello", "world"},
|
||||
BindTo(func(s string) StringState {
|
||||
return StringState{Value: s}
|
||||
}),
|
||||
)
|
||||
|
||||
expected2 := []StringState{{Value: "hello"}, {Value: "world"}}
|
||||
assert.Equal(t, expected2, result2)
|
||||
|
||||
// Test BindTo with empty array
|
||||
empty := []int{}
|
||||
result3 := F.Pipe1(
|
||||
empty,
|
||||
BindTo(func(x int) State {
|
||||
return State{X: x}
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, []State{}, result3)
|
||||
}
|
||||
|
||||
// TestDoWithLetAndBindTo tests combining Do, Let, LetTo, and BindTo
|
||||
func TestDoWithLetAndBindTo(t *testing.T) {
|
||||
type State1 struct {
|
||||
X int
|
||||
}
|
||||
type State2 struct {
|
||||
X int
|
||||
Double int
|
||||
}
|
||||
type State3 struct {
|
||||
X int
|
||||
Double int
|
||||
Name string
|
||||
}
|
||||
|
||||
// Test complex pipeline
|
||||
result := F.Pipe3(
|
||||
[]int{5, 10},
|
||||
BindTo(func(x int) State1 {
|
||||
return State1{X: x}
|
||||
}),
|
||||
Let(
|
||||
func(double int) func(s State1) State2 {
|
||||
return func(s State1) State2 {
|
||||
return State2{X: s.X, Double: double}
|
||||
}
|
||||
},
|
||||
func(s State1) int { return s.X * 2 },
|
||||
),
|
||||
LetTo(
|
||||
func(name string) func(s State2) State3 {
|
||||
return func(s State2) State3 {
|
||||
return State3{X: s.X, Double: s.Double, Name: name}
|
||||
}
|
||||
},
|
||||
"result",
|
||||
),
|
||||
)
|
||||
|
||||
expected := []State3{
|
||||
{X: 5, Double: 10, Name: "result"},
|
||||
{X: 10, Double: 20, Name: "result"},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
137
v2/array/coverage.out
Normal file
137
v2/array/coverage.out
Normal file
@@ -0,0 +1,137 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/array/any.go:34.65,36.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/any.go:48.51,50.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:30.33,32.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:37.52,39.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:44.39,46.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:52.50,54.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:58.54,61.23 3 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:61.23,63.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:64.2,64.11 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:70.62,72.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:83.48,85.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:89.52,91.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:93.55,96.23 3 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:96.23,98.14 2 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:98.14,100.4 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:102.2,102.15 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:105.75,108.23 3 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:108.23,110.14 2 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:110.14,112.4 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:114.2,114.15 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:120.54,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:127.68,129.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:132.58,134.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:140.67,142.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:148.78,150.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:155.65,157.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:162.76,164.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:169.69,171.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:174.80,175.26 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:175.26,177.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:180.64,182.25 2 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:182.25,184.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:185.2,185.16 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:189.65,191.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:194.79,196.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:206.62,208.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:214.76,216.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:221.67,223.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:229.81,231.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:235.66,236.24 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:236.24,238.3 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:254.37,256.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:261.34,263.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:266.37,268.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:273.25,275.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:280.24,282.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:287.25,289.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:295.56,297.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:308.54,310.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:316.53,318.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:324.50,326.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:331.76,333.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:338.83,340.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:346.38,348.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:354.36,356.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:362.37,364.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:370.36,372.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:375.49,376.26 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:376.26,380.35 4 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:380.35,385.4 4 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:386.3,386.16 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:395.50,397.26 2 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:397.26,398.18 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:398.18,400.4 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:401.3,401.25 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:406.60,407.36 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:407.36,409.3 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:419.36,421.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:424.49,426.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:432.49,434.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:440.42,442.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:447.30,449.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:456.78,458.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:464.75,466.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:469.32,471.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:474.35,476.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:479.28,481.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:486.50,488.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:493.29,495.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:500.47,502.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:507.67,509.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:514.81,516.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:521.45,523.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:528.38,530.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:605.43,607.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:613.52,615.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:621.49,623.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:628.44,630.2 1 0
|
||||
github.com/IBM/fp-go/v2/array/array.go:714.33,716.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:780.53,781.26 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:781.26,782.47 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:782.47,782.67 1 1
|
||||
github.com/IBM/fp-go/v2/array/array.go:839.31,841.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:36.7,38.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:60.20,62.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:91.20,93.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:120.20,122.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:143.19,145.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/bind.go:166.20,168.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/eq.go:35.37,37.49 2 1
|
||||
github.com/IBM/fp-go/v2/array/eq.go:37.49,39.3 1 1
|
||||
github.com/IBM/fp-go/v2/array/eq.go:43.45,45.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:33.65,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:48.79,50.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:68.78,70.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:76.89,78.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:89.64,91.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:97.78,99.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:105.77,107.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/find.go:113.88,115.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/magma.go:38.50,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monad.go:39.65,41.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:35.36,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:48.42,50.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:52.45,54.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:68.45,73.48 3 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:73.48,75.3 1 1
|
||||
github.com/IBM/fp-go/v2/array/monoid.go:77.2,77.12 1 1
|
||||
github.com/IBM/fp-go/v2/array/sequence.go:27.19,29.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sequence.go:69.22,71.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sequence.go:92.53,98.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sort.go:35.47,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sort.go:65.68,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/sort.go:96.51,98.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:66.34,68.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:83.24,86.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:94.39,96.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:105.29,108.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:110.142,117.46 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:117.46,118.54 1 1
|
||||
github.com/IBM/fp-go/v2/array/traverse.go:118.54,125.4 1 1
|
||||
github.com/IBM/fp-go/v2/array/uniq.go:20.43,22.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/uniq.go:49.60,51.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/zip.go:38.73,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/zip.go:58.55,60.2 1 1
|
||||
github.com/IBM/fp-go/v2/array/zip.go:81.62,83.2 1 1
|
||||
78
v2/array/sequence_extended_test.go
Normal file
78
v2/array/sequence_extended_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package array
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSequenceWithOption tests the generic Sequence function with Option monad
|
||||
func TestSequenceWithOption(t *testing.T) {
|
||||
// Test with Option monad - all Some values
|
||||
opts := From(
|
||||
O.Some(1),
|
||||
O.Some(2),
|
||||
O.Some(3),
|
||||
)
|
||||
|
||||
// Use the Sequence function with Option's applicative monoid
|
||||
monoid := O.ApplicativeMonoid(Monoid[int]())
|
||||
seq := Sequence(O.Map(Of[int]), monoid)
|
||||
result := seq(opts)
|
||||
|
||||
assert.Equal(t, O.Of(From(1, 2, 3)), result)
|
||||
|
||||
// Test with Option monad - contains None
|
||||
optsWithNone := From(
|
||||
O.Some(1),
|
||||
O.None[int](),
|
||||
O.Some(3),
|
||||
)
|
||||
|
||||
result2 := seq(optsWithNone)
|
||||
assert.True(t, O.IsNone(result2))
|
||||
|
||||
// Test with empty array
|
||||
empty := Empty[Option[int]]()
|
||||
result3 := seq(empty)
|
||||
assert.Equal(t, O.Some(Empty[int]()), result3)
|
||||
}
|
||||
|
||||
// TestMonadSequence tests the MonadSequence function
|
||||
func TestMonadSequence(t *testing.T) {
|
||||
// Test with Option monad
|
||||
opts := From(
|
||||
O.Some("hello"),
|
||||
O.Some("world"),
|
||||
)
|
||||
|
||||
monoid := O.ApplicativeMonoid(Monoid[string]())
|
||||
result := MonadSequence(O.Map(Of[string]), monoid, opts)
|
||||
|
||||
assert.Equal(t, O.Of(From("hello", "world")), result)
|
||||
|
||||
// Test with None in the array
|
||||
optsWithNone := From(
|
||||
O.Some("hello"),
|
||||
O.None[string](),
|
||||
)
|
||||
|
||||
result2 := MonadSequence(O.Map(Of[string]), monoid, optsWithNone)
|
||||
assert.Equal(t, O.None[[]string](), result2)
|
||||
}
|
||||
164
v2/array/traverse_extended_test.go
Normal file
164
v2/array/traverse_extended_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package array
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestMonadTraverse tests the MonadTraverse function
|
||||
func TestMonadTraverse(t *testing.T) {
|
||||
// Test converting integers to strings via Option
|
||||
numbers := []int{1, 2, 3}
|
||||
|
||||
result := MonadTraverse(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, []string{"1", "2", "3"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
|
||||
// Test with a function that can return None
|
||||
result2 := MonadTraverse(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(n int) O.Option[string] {
|
||||
if n == 2 {
|
||||
return O.None[string]()
|
||||
}
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsNone(result2))
|
||||
|
||||
// Test with empty array
|
||||
empty := []int{}
|
||||
result3 := MonadTraverse(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
empty,
|
||||
func(n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result3))
|
||||
assert.Equal(t, []string{}, O.GetOrElse(func() []string { return nil })(result3))
|
||||
}
|
||||
|
||||
// TestTraverseWithIndex tests the TraverseWithIndex function
|
||||
func TestTraverseWithIndex(t *testing.T) {
|
||||
// Test with index-aware transformation
|
||||
words := []string{"a", "b", "c"}
|
||||
|
||||
traverser := TraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
func(idx int, s string) O.Option[string] {
|
||||
return O.Some(s + strconv.Itoa(idx))
|
||||
},
|
||||
)
|
||||
|
||||
result := traverser(words)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, []string{"a0", "b1", "c2"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
|
||||
// Test with conditional None based on index
|
||||
traverser2 := TraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
func(idx int, s string) O.Option[string] {
|
||||
if idx == 1 {
|
||||
return O.None[string]()
|
||||
}
|
||||
return O.Some(s)
|
||||
},
|
||||
)
|
||||
|
||||
result2 := traverser2(words)
|
||||
assert.True(t, O.IsNone(result2))
|
||||
}
|
||||
|
||||
// TestMonadTraverseWithIndex tests the MonadTraverseWithIndex function
|
||||
func TestMonadTraverseWithIndex(t *testing.T) {
|
||||
// Test with index-aware transformation
|
||||
numbers := []int{10, 20, 30}
|
||||
|
||||
result := MonadTraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(idx, n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n * idx))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
// Expected: [10*0, 20*1, 30*2] = ["0", "20", "60"]
|
||||
assert.Equal(t, []string{"0", "20", "60"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
|
||||
// Test with None at specific index
|
||||
result2 := MonadTraverseWithIndex(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
numbers,
|
||||
func(idx, n int) O.Option[string] {
|
||||
if idx == 2 {
|
||||
return O.None[string]()
|
||||
}
|
||||
return O.Some(strconv.Itoa(n))
|
||||
},
|
||||
)
|
||||
|
||||
assert.True(t, O.IsNone(result2))
|
||||
}
|
||||
|
||||
// TestMakeTraverseType tests the MakeTraverseType function
|
||||
func TestMakeTraverseType(t *testing.T) {
|
||||
// Create a traverse type for Option
|
||||
traverseType := MakeTraverseType[int, string, O.Option[string], O.Option[[]string], O.Option[func(string) []string]]()
|
||||
|
||||
// Use it to traverse an array
|
||||
numbers := []int{1, 2, 3}
|
||||
result := traverseType(
|
||||
O.Of[[]string],
|
||||
O.Map[[]string, func(string) []string],
|
||||
O.Ap[[]string, string],
|
||||
)(func(n int) O.Option[string] {
|
||||
return O.Some(strconv.Itoa(n * 2))
|
||||
})(numbers)
|
||||
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, []string{"2", "4", "6"}, O.GetOrElse(func() []string { return []string{} })(result))
|
||||
}
|
||||
@@ -87,7 +87,9 @@ type templateData struct {
|
||||
}
|
||||
|
||||
const lensStructTemplate = `
|
||||
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
|
||||
// {{.Name}}Lenses provides [lenses] for accessing fields of [{{.Name}}]
|
||||
//
|
||||
// [lenses]: __lens.Lens
|
||||
type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
@@ -101,7 +103,10 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
|
||||
// {{.Name}}RefLenses provides [lenses] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
|
||||
//
|
||||
//
|
||||
// [lenses]: __lens.Lens
|
||||
type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||
// mandatory fields
|
||||
{{- range .Fields}}
|
||||
@@ -112,23 +117,32 @@ type {{.Name}}RefLenses{{.TypeParams}} struct {
|
||||
{{- if .IsComparable}}
|
||||
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
// prisms
|
||||
{{- range .Fields}}
|
||||
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
|
||||
// {{.Name}}Prisms provides [prisms] for accessing fields of [{{.Name}}]
|
||||
//
|
||||
// [prisms]: __prism.Prism
|
||||
type {{.Name}}Prisms{{.TypeParams}} struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
|
||||
// {{.Name}}RefPrisms provides [prisms] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
|
||||
//
|
||||
// [prisms]: __prism.Prism
|
||||
type {{.Name}}RefPrisms{{.TypeParams}} struct {
|
||||
{{- range .Fields}}
|
||||
{{.Name}} __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
|
||||
{{- end}}
|
||||
}
|
||||
`
|
||||
|
||||
const lensConstructorTemplate = `
|
||||
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
|
||||
// Make{{.Name}}Lenses creates a new [{{.Name}}Lenses] with [lenses] for all fields
|
||||
//
|
||||
// [lenses]:__lens.Lens
|
||||
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
@@ -158,7 +172,9 @@ func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
|
||||
// Make{{.Name}}RefLenses creates a new [{{.Name}}RefLenses] with [lenses] for all fields
|
||||
//
|
||||
// [lenses]:__lens.Lens
|
||||
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
|
||||
// mandatory lenses
|
||||
{{- range .Fields}}
|
||||
@@ -196,7 +212,9 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
|
||||
// Make{{.Name}}Prisms creates a new [{{.Name}}Prisms] with [prisms] for all fields
|
||||
//
|
||||
// [prisms]:__prism.Prism
|
||||
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
@@ -236,6 +254,49 @@ func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
|
||||
// Make{{.Name}}RefPrisms creates a new [{{.Name}}RefPrisms] with [prisms] for all fields
|
||||
//
|
||||
// [prisms]:__prism.Prism
|
||||
func Make{{.Name}}RefPrisms{{.TypeParams}}() {{.Name}}RefPrisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{- if .IsComparable}}
|
||||
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
|
||||
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
|
||||
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
|
||||
{{- if .IsEmbedded}}
|
||||
var result {{$.Name}}{{$.TypeParamNames}}
|
||||
result.{{.Name}} = v
|
||||
return &result
|
||||
{{- else}}
|
||||
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||
{{- end}}
|
||||
},
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- else}}
|
||||
_prism{{.Name}} := __prism.MakePrismWithName(
|
||||
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
|
||||
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
|
||||
{{- if .IsEmbedded}}
|
||||
var result {{$.Name}}{{$.TypeParamNames}}
|
||||
result.{{.Name}} = v
|
||||
return &result
|
||||
{{- else}}
|
||||
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
|
||||
{{- end}}
|
||||
},
|
||||
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
|
||||
)
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
return {{.Name}}RefPrisms{{.TypeParamNames}} {
|
||||
{{- range .Fields}}
|
||||
{{.Name}}: _prism{{.Name}},
|
||||
{{- end}}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var (
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
RIOE.Map(func(r *http.Request) *url.URL {
|
||||
return r.URL
|
||||
}),
|
||||
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[any] {
|
||||
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[Void] {
|
||||
return IO.FromImpure(func() {
|
||||
q := u.Query()
|
||||
assert.Equal(t, "10", q.Get("limit"))
|
||||
|
||||
7
v2/context/readerioresult/http/builder/types.go
Normal file
7
v2/context/readerioresult/http/builder/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package builder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
type (
|
||||
Void = function.Void
|
||||
)
|
||||
@@ -158,7 +158,7 @@ func MakeClient(httpClient *http.Client) Client {
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// fullResp := ReadFullResponse(client)(request)
|
||||
// result := fullResp(t.Context())()
|
||||
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
func ReadFullResponse(client Client) RIOE.Operator[*http.Request, H.FullResponse] {
|
||||
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do(req),
|
||||
@@ -195,7 +195,7 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
|
||||
// request := MakeGetRequest("https://api.example.com/data")
|
||||
// readBytes := ReadAll(client)
|
||||
// result := readBytes(request)(t.Context())()
|
||||
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
func ReadAll(client Client) RIOE.Operator[*http.Request, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
RIOE.Map(H.Body),
|
||||
@@ -219,7 +219,7 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// request := MakeGetRequest("https://api.example.com/text")
|
||||
// readText := ReadText(client)
|
||||
// result := readText(request)(t.Context())()
|
||||
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
func ReadText(client Client) RIOE.Operator[*http.Request, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
RIOE.Map(B.ToString),
|
||||
@@ -231,7 +231,7 @@ func ReadText(client Client) RIOE.Kleisli[Requester, string] {
|
||||
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
|
||||
// but will be removed in a future version. The capitalized version follows Go naming
|
||||
// conventions for acronyms.
|
||||
func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
func ReadJson[A any](client Client) RIOE.Operator[*http.Request, A] {
|
||||
return ReadJSON[A](client)
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
// 3. Reads the response body as bytes
|
||||
//
|
||||
// This function is used internally by ReadJSON to ensure proper JSON response handling.
|
||||
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
func readJSON(client Client) RIOE.Operator[*http.Request, []byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
RIOE.ChainFirstEitherK(F.Flow2(
|
||||
@@ -278,7 +278,7 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
|
||||
// request := MakeGetRequest("https://api.example.com/user/1")
|
||||
// readUser := ReadJSON[User](client)
|
||||
// result := readUser(request)(t.Context())()
|
||||
func ReadJSON[A any](client Client) RIOE.Kleisli[Requester, A] {
|
||||
func ReadJSON[A any](client Client) RIOE.Operator[*http.Request, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
RIOE.ChainEitherK(J.Unmarshal[A]),
|
||||
|
||||
@@ -65,7 +65,7 @@ var (
|
||||
// This function assumes the context contains logging information; it will panic if not present.
|
||||
getLoggingContext = F.Flow3(
|
||||
loggingContextValue,
|
||||
option.ToType[loggingContext],
|
||||
option.InstanceOf[loggingContext],
|
||||
option.GetOrElse(getDefaultLoggingContext),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -222,7 +222,7 @@ func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOResult[A])
|
||||
return function.Pipe3(
|
||||
ma,
|
||||
ioresult.Swap[A],
|
||||
ioeither.ChainFirstIOK[A](func(err error) func() any {
|
||||
ioeither.ChainFirstIOK[A](func(err error) func() Void {
|
||||
return io.FromImpure(func() { cancel(err) })
|
||||
}),
|
||||
ioeither.Swap[A],
|
||||
|
||||
@@ -48,7 +48,7 @@ func WithLock[A any](lock ReaderIOResult[context.CancelFunc]) Operator[A, A] {
|
||||
function.Constant1[context.CancelFunc, ReaderIOResult[A]],
|
||||
WithResource[A](lock, function.Flow2(
|
||||
io.FromImpure[context.CancelFunc],
|
||||
FromIO[any],
|
||||
FromIO[Void],
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioref"
|
||||
@@ -152,4 +153,6 @@ type (
|
||||
IORef[A any] = ioref.IORef[A]
|
||||
|
||||
State[S, A any] = state.State[S, A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
168
v2/context/readerreaderioresult/promap.go
Normal file
168
v2/context/readerreaderioresult/promap.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// LocalIOK transforms the outer environment of a ReaderReaderIOResult using an IO-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through an effectful computation before
|
||||
// passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// This is useful when the outer environment transformation itself requires IO effects,
|
||||
// such as reading from a file, making a network call, or accessing system resources,
|
||||
// but these effects cannot fail (or failures are not relevant).
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
|
||||
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalIOK[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// LocalIOEitherK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through an effectful computation that can fail before
|
||||
// passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// This is useful when the outer environment transformation itself requires IO effects that can fail,
|
||||
// such as reading from a file that might not exist, making a network call that might timeout,
|
||||
// or parsing data that might be invalid.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOEitherK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalIOEitherK[context.Context, A](f)
|
||||
}
|
||||
|
||||
// LocalIOResultK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
|
||||
// This is a type-safe alias for LocalIOEitherK specialized for Result types (which use error as the error type).
|
||||
//
|
||||
// It allows you to modify the outer environment through an effectful computation that can fail before
|
||||
// passing it to the ReaderReaderIOResult.
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalIOEitherK[context.Context, A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalResultK[A, R1, R2 any](f result.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalEitherK[context.Context, A](f)
|
||||
}
|
||||
|
||||
// LocalReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
|
||||
// It allows you to modify the outer environment through a computation that depends on the inner context
|
||||
// and can perform IO effects that may fail.
|
||||
//
|
||||
// This is useful when the outer environment transformation requires access to the inner context (e.g., context.Context)
|
||||
// and may perform IO operations that can fail, such as database queries, API calls, or file operations.
|
||||
//
|
||||
// The transformation happens in three stages:
|
||||
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderIOEitherK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderIOEitherK[A](f)
|
||||
}
|
||||
|
||||
// LocalReaderIOResultK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
|
||||
// This is a type-safe alias for LocalReaderIOEitherK specialized for Result types (which use error as the error type).
|
||||
//
|
||||
// It allows you to modify the outer environment through a computation that depends on the inner context
|
||||
// and can perform IO effects that may fail.
|
||||
//
|
||||
// The transformation happens in three stages:
|
||||
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
|
||||
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
|
||||
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type produced by the ReaderReaderIOResult
|
||||
// - R1: The original outer environment type expected by the ReaderReaderIOResult
|
||||
// - R2: The new input outer environment type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderIOEitherK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalReaderReaderIOEitherK[A, R1, R2 any](f Kleisli[R2, R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.LocalReaderReaderIOEitherK[A](f)
|
||||
}
|
||||
428
v2/context/readerreaderioresult/promap_test.go
Normal file
428
v2/context/readerreaderioresult/promap_test.go
Normal file
@@ -0,0 +1,428 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type SimpleConfig struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestLocalIOK tests LocalIOK functionality
|
||||
func TestLocalIOK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic IO transformation", func(t *testing.T) {
|
||||
// IO effect that loads config from a path
|
||||
loadConfig := func(path string) io.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
// Simulate loading config
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOK
|
||||
adapted := LocalIOK[string](loadConfig)(useConfig)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
})
|
||||
|
||||
t.Run("IO transformation with side effects", func(t *testing.T) {
|
||||
var loadLog []string
|
||||
|
||||
loadData := func(key string) io.IO[int] {
|
||||
return func() int {
|
||||
loadLog = append(loadLog, "Loading: "+key)
|
||||
return len(key) * 10
|
||||
}
|
||||
}
|
||||
|
||||
processData := func(n int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Processed: %d", n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string](loadData)(processData)
|
||||
res := adapted("test")(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of("Processed: 40"), res)
|
||||
assert.Equal(t, []string{"Loading: test"}, loadLog)
|
||||
})
|
||||
|
||||
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
|
||||
loadConfig := func(path string) io.IO[SimpleConfig] {
|
||||
return func() SimpleConfig {
|
||||
return SimpleConfig{Port: 8080}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that returns an error
|
||||
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Left[string](errors.New("operation failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOK[string](loadConfig)(failingOperation)
|
||||
res := adapted("config.json")(ctx)()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOEitherK tests LocalIOEitherK functionality
|
||||
func TestLocalIOEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic IOResult transformation", func(t *testing.T) {
|
||||
// IOResult effect that loads config from a path (can fail)
|
||||
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOEitherK
|
||||
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("error propagation from environment transformation", func(t *testing.T) {
|
||||
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
return result.Left[SimpleConfig](errors.New("file not found"))
|
||||
}
|
||||
}
|
||||
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
|
||||
res := adapted("missing.json")(ctx)()
|
||||
|
||||
// Error from loadConfig should propagate
|
||||
assert.True(t, result.IsLeft(res))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalIOResultK tests LocalIOResultK functionality
|
||||
func TestLocalIOResultK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic IOResult transformation", func(t *testing.T) {
|
||||
// IOResult effect that loads config from a path (can fail)
|
||||
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalIOResultK
|
||||
adapted := LocalIOResultK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
|
||||
// First transformation: string -> int (can fail)
|
||||
parseID := func(s string) ioresult.IOResult[int] {
|
||||
return func() result.Result[int] {
|
||||
if s == "" {
|
||||
return result.Left[int](errors.New("empty string"))
|
||||
}
|
||||
return result.Of(len(s) * 10)
|
||||
}
|
||||
}
|
||||
|
||||
// Second transformation: int -> SimpleConfig (can fail)
|
||||
loadConfig := func(id int) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if id < 0 {
|
||||
return result.Left[SimpleConfig](errors.New("invalid ID"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8000 + id})
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose transformations
|
||||
step1 := LocalIOResultK[string](loadConfig)(formatConfig)
|
||||
step2 := LocalIOResultK[string](parseID)(step1)
|
||||
|
||||
// Success case
|
||||
res := step2("test")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8040"), res)
|
||||
|
||||
// Failure in first transformation
|
||||
resErr1 := step2("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr1))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalReaderIOEitherK tests LocalReaderIOEitherK functionality
|
||||
func TestLocalReaderIOEitherK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
|
||||
// ReaderIOResult effect that loads config from a path (can fail, uses context)
|
||||
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
|
||||
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
// Could use context here for cancellation, logging, etc.
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalReaderIOEitherK
|
||||
adapted := LocalReaderIOEitherK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("context propagation", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
const key ctxKey = "test-key"
|
||||
|
||||
// ReaderIOResult that reads from context
|
||||
loadFromContext := func(path string) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
if val := ctx.Value(key); val != nil {
|
||||
return result.Of(val.(string))
|
||||
}
|
||||
return result.Left[string](errors.New("key not found in context"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the loaded value
|
||||
useValue := func(val string) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of("Loaded: " + val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapted := LocalReaderIOEitherK[string](loadFromContext)(useValue)
|
||||
|
||||
// With context value
|
||||
ctxWithValue := context.WithValue(ctx, key, "test-value")
|
||||
res := adapted("ignored")(ctxWithValue)()
|
||||
assert.Equal(t, result.Of("Loaded: test-value"), res)
|
||||
|
||||
// Without context value
|
||||
resErr := adapted("ignored")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalReaderIOResultK tests LocalReaderIOResultK functionality
|
||||
func TestLocalReaderIOResultK(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
|
||||
// ReaderIOResult effect that loads config from a path (can fail, uses context)
|
||||
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
|
||||
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if path == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 8080})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReaderReaderIOResult that uses the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose using LocalReaderIOResultK
|
||||
adapted := LocalReaderIOResultK[string](loadConfig)(useConfig)
|
||||
|
||||
// Success case
|
||||
res := adapted("config.json")(ctx)()
|
||||
assert.Equal(t, result.Of("Port: 8080"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := adapted("")(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
|
||||
t.Run("real-world: load and validate config with context", func(t *testing.T) {
|
||||
type ConfigFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// Read file with context (can fail, uses context for cancellation)
|
||||
readFile := func(cf ConfigFile) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result.Left[string](ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
if cf.Path == "" {
|
||||
return result.Left[string](errors.New("empty path"))
|
||||
}
|
||||
return result.Of(`{"port":9000}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse config with context (can fail)
|
||||
parseConfig := func(content string) readerioresult.ReaderIOResult[SimpleConfig] {
|
||||
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
|
||||
return func() result.Result[SimpleConfig] {
|
||||
if content == "" {
|
||||
return result.Left[SimpleConfig](errors.New("empty content"))
|
||||
}
|
||||
return result.Of(SimpleConfig{Port: 9000})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the config
|
||||
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) ioresult.IOResult[string] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compose the pipeline
|
||||
step1 := LocalReaderIOResultK[string](parseConfig)(useConfig)
|
||||
step2 := LocalReaderIOResultK[string](readFile)(step1)
|
||||
|
||||
// Success case
|
||||
res := step2(ConfigFile{Path: "app.json"})(ctx)()
|
||||
assert.Equal(t, result.Of("Using port: 9000"), res)
|
||||
|
||||
// Failure case
|
||||
resErr := step2(ConfigFile{Path: ""})(ctx)()
|
||||
assert.True(t, result.IsLeft(resErr))
|
||||
})
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
|
||||
@@ -170,6 +171,15 @@ func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B]
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
|
||||
return fromeither.ChainEitherK(
|
||||
Chain[R, A, B],
|
||||
FromEither[R, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
|
||||
// Useful for validation or side effects that may fail.
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
@@ -837,14 +847,6 @@ func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
|
||||
return RRIOE.MapLeft[R, context.Context, A](f)
|
||||
}
|
||||
|
||||
// Local modifies the outer environment before passing it to a computation.
|
||||
// Useful for providing different configurations to sub-computations.
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
|
||||
return RRIOE.Local[context.Context, error, A](f)
|
||||
}
|
||||
|
||||
// Read provides a specific outer environment value to a computation.
|
||||
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
|
||||
//
|
||||
@@ -892,3 +894,8 @@ func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]
|
||||
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
|
||||
return reader.Map[R](RIOE.Delay[A](delay))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Defer[R, A any](fa Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.Defer(fa)
|
||||
}
|
||||
|
||||
9
v2/context/readerreaderioresult/traverse.go
Normal file
9
v2/context/readerreaderioresult/traverse.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
|
||||
)
|
||||
|
||||
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
|
||||
return RRIOE.TraverseArray(f)
|
||||
}
|
||||
@@ -30,8 +30,8 @@ import (
|
||||
|
||||
type (
|
||||
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
|
||||
InjectableFactory = func(Dependency) IOResult[any]
|
||||
ProviderFactory = func(InjectableFactory) IOResult[any]
|
||||
InjectableFactory = ReaderIOResult[Dependency, any]
|
||||
ProviderFactory = ReaderIOResult[InjectableFactory, any]
|
||||
|
||||
paramIndex = map[int]int
|
||||
paramValue = map[int]any
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/record"
|
||||
)
|
||||
|
||||
@@ -12,4 +13,5 @@ type (
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Entry[K comparable, V any] = record.Entry[K, V]
|
||||
ReaderIOResult[R, T any] = readerioresult.ReaderIOResult[R, T]
|
||||
)
|
||||
|
||||
264
v2/effect/bind.go
Normal file
264
v2/effect/bind.go
Normal file
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Do[C, S any](
|
||||
empty S,
|
||||
) Effect[C, S] {
|
||||
return readerreaderioresult.Of[C](empty)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Bind[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.Bind(setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Let[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.Let[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LetTo[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.LetTo[C](setter, b)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindTo[C, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[C, T, S1] {
|
||||
return readerreaderioresult.BindTo[C](setter)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Effect[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApS(setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Effect[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApSL(lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) Effect[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindL(lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LetL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f func(T) T,
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.LetL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LetToL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.LetToL[C](lens, b)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOEitherK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioeither.Kleisli[error, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindIOEitherK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOResultK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f ioresult.Kleisli[S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindIOResultK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f io.Kleisli[S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindIOK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f reader.Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindReaderK(setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderIOK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f readerio.Kleisli[C, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindReaderIOK(setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindEitherK[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f either.Kleisli[error, S1, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.BindEitherK[C](setter, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOEitherKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f ioeither.Kleisli[error, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindIOEitherKL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindIOKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f io.Kleisli[T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindIOKL[C](lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f reader.Kleisli[C, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindReaderKL(lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func BindReaderIOKL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
f readerio.Kleisli[C, T, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.BindReaderIOKL(lens, f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOEitherS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IOEither[error, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApIOEitherS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa IO[T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApIOS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Reader[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApReaderS(setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderIOS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderIO[C, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApReaderIOS(setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApEitherS[C, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Either[error, T],
|
||||
) Operator[C, S1, S2] {
|
||||
return readerreaderioresult.ApEitherS[C](setter, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOEitherSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa IOEither[error, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApIOEitherSL[C](lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApIOSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa IO[T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApIOSL[C](lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Reader[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApReaderSL(lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApReaderIOSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa ReaderIO[C, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApReaderIOSL(lens, fa)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ApEitherSL[C, S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Either[error, T],
|
||||
) Operator[C, S, S] {
|
||||
return readerreaderioresult.ApEitherSL[C](lens, fa)
|
||||
}
|
||||
768
v2/effect/bind_test.go
Normal file
768
v2/effect/bind_test.go
Normal file
@@ -0,0 +1,768 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type BindState struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates effect with initial state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 30}
|
||||
eff := Do[TestContext](initial)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, initial, result)
|
||||
})
|
||||
|
||||
t.Run("creates effect with empty struct", func(t *testing.T) {
|
||||
type Empty struct{}
|
||||
eff := Do[TestContext](Empty{})
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Empty{}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
t.Run("binds effect result to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("chains multiple binds", func(t *testing.T) {
|
||||
initial := BindState{}
|
||||
|
||||
eff := Bind(
|
||||
func(email string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, string] {
|
||||
return Of[TestContext]("alice@example.com")
|
||||
},
|
||||
)(Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext](30)
|
||||
},
|
||||
)(Bind(
|
||||
func(name string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Name = name
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, string] {
|
||||
return Of[TestContext]("Alice")
|
||||
},
|
||||
)(Do[TestContext](initial))))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
assert.Equal(t, "alice@example.com", result.Email)
|
||||
})
|
||||
|
||||
t.Run("propagates errors", func(t *testing.T) {
|
||||
expectedErr := errors.New("bind error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
t.Run("computes value and binds to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := Let[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) int {
|
||||
return len(s.Name) * 10
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 50, result.Age) // len("Alice") * 10
|
||||
})
|
||||
|
||||
t.Run("chains with Bind", func(t *testing.T) {
|
||||
initial := BindState{Name: "Bob"}
|
||||
|
||||
eff := Let[TestContext](
|
||||
func(email string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) string {
|
||||
return s.Name + "@example.com"
|
||||
},
|
||||
)(Bind(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) Effect[TestContext, int] {
|
||||
return Of[TestContext](25)
|
||||
},
|
||||
)(Do[TestContext](initial)))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Bob", result.Name)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
assert.Equal(t, "Bob@example.com", result.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
t.Run("binds constant value to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := LetTo[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
42,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 42, result.Age)
|
||||
})
|
||||
|
||||
t.Run("chains multiple LetTo", func(t *testing.T) {
|
||||
initial := BindState{}
|
||||
|
||||
eff := LetTo[TestContext](
|
||||
func(email string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
"test@example.com",
|
||||
)(LetTo[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
30,
|
||||
)(LetTo[TestContext](
|
||||
func(name string) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Name = name
|
||||
return s
|
||||
}
|
||||
},
|
||||
"Alice",
|
||||
)(Do[TestContext](initial))))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
assert.Equal(t, "test@example.com", result.Email)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
t.Run("wraps value in state", func(t *testing.T) {
|
||||
type SimpleState struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
eff := BindTo[TestContext](func(v int) SimpleState {
|
||||
return SimpleState{Value: v}
|
||||
})(Of[TestContext](42))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result.Value)
|
||||
})
|
||||
|
||||
t.Run("starts a bind chain", func(t *testing.T) {
|
||||
type State struct {
|
||||
X int
|
||||
Y string
|
||||
}
|
||||
|
||||
eff := Let[TestContext](
|
||||
func(y string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Y = y
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s State) string {
|
||||
return "computed"
|
||||
},
|
||||
)(BindTo[TestContext](func(x int) State {
|
||||
return State{X: x}
|
||||
})(Of[TestContext](10)))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result.X)
|
||||
assert.Equal(t, "computed", result.Y)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
t.Run("applies effect and binds result to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
ageEffect := Of[TestContext](30)
|
||||
|
||||
eff := ApS(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
ageEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from applied effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("aps error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
ageEffect := Fail[TestContext, int](expectedErr)
|
||||
|
||||
eff := ApS(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
ageEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOK(t *testing.T) {
|
||||
t.Run("binds IO operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) io.IO[int] {
|
||||
return func() int {
|
||||
return 30
|
||||
}
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOEitherK(t *testing.T) {
|
||||
t.Run("binds successful IOEither to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) ioeither.IOEither[error, int] {
|
||||
return ioeither.Of[error](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("propagates IOEither error", func(t *testing.T) {
|
||||
expectedErr := errors.New("ioeither error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) ioeither.IOEither[error, int] {
|
||||
return ioeither.Left[int](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindIOResultK(t *testing.T) {
|
||||
t.Run("binds successful IOResult to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindIOResultK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) ioresult.IOResult[int] {
|
||||
return ioresult.Of(30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindReaderK(t *testing.T) {
|
||||
t.Run("binds Reader operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindReaderK(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) reader.Reader[TestContext, int] {
|
||||
return func(ctx TestContext) int {
|
||||
return 30
|
||||
}
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindReaderIOK(t *testing.T) {
|
||||
t.Run("binds ReaderIO operation to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindReaderIOK(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) readerio.ReaderIO[TestContext, int] {
|
||||
return func(ctx TestContext) io.IO[int] {
|
||||
return func() int {
|
||||
return 30
|
||||
}
|
||||
}
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindEitherK(t *testing.T) {
|
||||
t.Run("binds successful Either to state", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) either.Either[error, int] {
|
||||
return either.Of[error](30)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("propagates Either error", func(t *testing.T) {
|
||||
expectedErr := errors.New("either error")
|
||||
initial := BindState{Name: "Alice"}
|
||||
|
||||
eff := BindEitherK[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s BindState) either.Either[error, int] {
|
||||
return either.Left[int](expectedErr)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLensOperations(t *testing.T) {
|
||||
// Create lenses for BindState
|
||||
nameLens := lens.MakeLens(
|
||||
func(s BindState) string { return s.Name },
|
||||
func(s BindState, name string) BindState {
|
||||
s.Name = name
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
ageLens := lens.MakeLens(
|
||||
func(s BindState) int { return s.Age },
|
||||
func(s BindState, age int) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
},
|
||||
)
|
||||
|
||||
t.Run("ApSL applies effect using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
ageEffect := Of[TestContext](30)
|
||||
|
||||
eff := ApSL(ageLens, ageEffect)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("BindL binds effect using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
|
||||
eff := BindL(
|
||||
ageLens,
|
||||
func(age int) Effect[TestContext, int] {
|
||||
return Of[TestContext](age + 5)
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("LetL computes value using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
|
||||
eff := LetL[TestContext](
|
||||
ageLens,
|
||||
func(age int) int {
|
||||
return age * 2
|
||||
},
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 50, result.Age)
|
||||
})
|
||||
|
||||
t.Run("LetToL sets constant using lens", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice", Age: 25}
|
||||
|
||||
eff := LetToL[TestContext](ageLens, 100)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 100, result.Age)
|
||||
})
|
||||
|
||||
t.Run("chains lens operations", func(t *testing.T) {
|
||||
initial := BindState{}
|
||||
|
||||
eff := LetToL[TestContext](
|
||||
ageLens,
|
||||
30,
|
||||
)(LetToL[TestContext](
|
||||
nameLens,
|
||||
"Bob",
|
||||
)(Do[TestContext](initial)))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Bob", result.Name)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApOperations(t *testing.T) {
|
||||
t.Run("ApIOS applies IO effect", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
ioEffect := func() int { return 30 }
|
||||
|
||||
eff := ApIOS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
ioEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("ApReaderS applies Reader effect", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
readerEffect := func(ctx TestContext) int { return 30 }
|
||||
|
||||
eff := ApReaderS(
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
readerEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
|
||||
t.Run("ApEitherS applies Either effect", func(t *testing.T) {
|
||||
initial := BindState{Name: "Alice"}
|
||||
eitherEffect := either.Of[error](30)
|
||||
|
||||
eff := ApEitherS[TestContext](
|
||||
func(age int) func(BindState) BindState {
|
||||
return func(s BindState) BindState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
eitherEffect,
|
||||
)(Do[TestContext](initial))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Age)
|
||||
})
|
||||
}
|
||||
|
||||
func TestComplexBindChain(t *testing.T) {
|
||||
t.Run("builds complex state with multiple operations", func(t *testing.T) {
|
||||
type ComplexState struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
IsAdmin bool
|
||||
Score int
|
||||
}
|
||||
|
||||
eff := LetTo[TestContext](
|
||||
func(score int) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.Score = score
|
||||
return s
|
||||
}
|
||||
},
|
||||
100,
|
||||
)(Let[TestContext](
|
||||
func(isAdmin bool) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.IsAdmin = isAdmin
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s ComplexState) bool {
|
||||
return s.Age >= 18
|
||||
},
|
||||
)(Let[TestContext](
|
||||
func(email string) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.Email = email
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s ComplexState) string {
|
||||
return s.Name + "@example.com"
|
||||
},
|
||||
)(Bind(
|
||||
func(age int) func(ComplexState) ComplexState {
|
||||
return func(s ComplexState) ComplexState {
|
||||
s.Age = age
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s ComplexState) Effect[TestContext, int] {
|
||||
return Of[TestContext](25)
|
||||
},
|
||||
)(BindTo[TestContext](func(name string) ComplexState {
|
||||
return ComplexState{Name: name}
|
||||
})(Of[TestContext]("Alice"))))))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Alice", result.Name)
|
||||
assert.Equal(t, 25, result.Age)
|
||||
assert.Equal(t, "Alice@example.com", result.Email)
|
||||
assert.True(t, result.IsAdmin)
|
||||
assert.Equal(t, 100, result.Score)
|
||||
})
|
||||
}
|
||||
110
v2/effect/dependencies.go
Normal file
110
v2/effect/dependencies.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
return readerreaderioresult.Local[A](acc)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
|
||||
return readerreaderioresult.Local[A](acc)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalIOK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalIOResultK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalResultK[A](f)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderIOResultK[A](f)
|
||||
}
|
||||
|
||||
// LocalEffectK transforms the context of an Effect using an Effect-returning function.
|
||||
// This is the most powerful context transformation function, allowing the transformation
|
||||
// itself to be effectful (can fail, perform I/O, and access the outer context).
|
||||
//
|
||||
// LocalEffectK takes a Kleisli arrow that:
|
||||
// - Accepts the outer context C2
|
||||
// - Returns an Effect that produces the inner context C1
|
||||
// - Can fail with an error during context transformation
|
||||
// - Can perform I/O operations during transformation
|
||||
//
|
||||
// This is useful when:
|
||||
// - Context transformation requires I/O (e.g., loading config from a file)
|
||||
// - Context transformation can fail (e.g., validating or parsing context)
|
||||
// - Context transformation needs to access the outer context
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type produced by the effect
|
||||
// - C1: The inner context type (required by the original effect)
|
||||
// - C2: The outer context type (provided to the transformed effect)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow (C2 -> Effect[C2, C1]) that transforms C2 to C1 effectfully
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms Effect[C1, A] to Effect[C2, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// ConfigPath string
|
||||
// }
|
||||
//
|
||||
// // Effect that needs DatabaseConfig
|
||||
// dbEffect := effect.Of[DatabaseConfig, string]("query result")
|
||||
//
|
||||
// // Transform AppConfig to DatabaseConfig effectfully
|
||||
// // (e.g., load config from file, which can fail)
|
||||
// loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
// return effect.Chain[AppConfig](func(_ AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
// // Simulate loading config from file (can fail)
|
||||
// return effect.Of[AppConfig, DatabaseConfig](DatabaseConfig{
|
||||
// ConnectionString: "loaded from " + app.ConfigPath,
|
||||
// })
|
||||
// })(effect.Of[AppConfig, AppConfig](app))
|
||||
// }
|
||||
//
|
||||
// // Apply the transformation
|
||||
// transform := effect.LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
|
||||
// appEffect := transform(dbEffect)
|
||||
//
|
||||
// // Run with AppConfig
|
||||
// ioResult := effect.Provide(AppConfig{ConfigPath: "/etc/app.conf"})(appEffect)
|
||||
// readerResult := effect.RunSync(ioResult)
|
||||
// result, err := readerResult(context.Background())
|
||||
//
|
||||
// Comparison with other Local functions:
|
||||
// - Local/Contramap: Pure context transformation (C2 -> C1)
|
||||
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
|
||||
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
|
||||
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
|
||||
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
|
||||
//
|
||||
//go:inline
|
||||
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
|
||||
}
|
||||
620
v2/effect/dependencies_test.go
Normal file
620
v2/effect/dependencies_test.go
Normal file
@@ -0,0 +1,620 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type OuterContext struct {
|
||||
Value string
|
||||
Number int
|
||||
}
|
||||
|
||||
type InnerContext struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
t.Run("transforms context for inner effect", func(t *testing.T) {
|
||||
// Create an effect that uses InnerContext
|
||||
innerEffect := Of[InnerContext]("result")
|
||||
|
||||
// Transform OuterContext to InnerContext
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
// Apply Local to transform the context
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("allows accessing outer context fields", func(t *testing.T) {
|
||||
// Create an effect that reads from InnerContext
|
||||
innerEffect := Chain(func(_ string) Effect[InnerContext, string] {
|
||||
return Of[InnerContext]("inner value")
|
||||
})(Of[InnerContext]("start"))
|
||||
|
||||
// Transform context
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value + " transformed"}
|
||||
}
|
||||
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
// Run with OuterContext
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "original",
|
||||
Number: 100,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "inner value", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from inner effect", func(t *testing.T) {
|
||||
expectedErr := assert.AnError
|
||||
innerEffect := Fail[InnerContext, string](expectedErr)
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
kleisli := Local[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Local transformations", func(t *testing.T) {
|
||||
type Level1 struct {
|
||||
A string
|
||||
}
|
||||
type Level2 struct {
|
||||
B string
|
||||
}
|
||||
type Level3 struct {
|
||||
C string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
level3Effect := Of[Level3]("deep result")
|
||||
|
||||
// Transform Level2 -> Level3
|
||||
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
|
||||
return Level3{C: l2.B + "-c"}
|
||||
})
|
||||
|
||||
// Transform Level1 -> Level2
|
||||
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
|
||||
return Level2{B: l1.A + "-b"}
|
||||
})
|
||||
|
||||
// Compose transformations
|
||||
level2Effect := local23(level3Effect)
|
||||
level1Effect := local12(level2Effect)
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "deep result", result)
|
||||
})
|
||||
|
||||
t.Run("works with complex context transformations", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Database string
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
DB DatabaseConfig
|
||||
APIKey string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
// Effect that needs only DatabaseConfig
|
||||
dbEffect := Of[DatabaseConfig]("connected")
|
||||
|
||||
// Extract DB config from AppConfig
|
||||
accessor := func(app AppConfig) DatabaseConfig {
|
||||
return app.DB
|
||||
}
|
||||
|
||||
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
|
||||
appEffect := kleisli(dbEffect)
|
||||
|
||||
// Run with full AppConfig
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
DB: DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Database: "mydb",
|
||||
},
|
||||
APIKey: "secret",
|
||||
Timeout: 30,
|
||||
})(appEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "connected", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContramap(t *testing.T) {
|
||||
t.Run("is equivalent to Local", func(t *testing.T) {
|
||||
innerEffect := Of[InnerContext](42)
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
// Test Local
|
||||
localKleisli := Local[OuterContext, InnerContext, int](accessor)
|
||||
localEffect := localKleisli(innerEffect)
|
||||
|
||||
// Test Contramap
|
||||
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
|
||||
contramapEffect := contramapKleisli(innerEffect)
|
||||
|
||||
outerCtx := OuterContext{Value: "test", Number: 100}
|
||||
|
||||
// Run both
|
||||
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
|
||||
localReader := RunSync(localIO)
|
||||
localResult, localErr := localReader(context.Background())
|
||||
|
||||
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
|
||||
contramapReader := RunSync(contramapIO)
|
||||
contramapResult, contramapErr := contramapReader(context.Background())
|
||||
|
||||
assert.NoError(t, localErr)
|
||||
assert.NoError(t, contramapErr)
|
||||
assert.Equal(t, localResult, contramapResult)
|
||||
})
|
||||
|
||||
t.Run("transforms context correctly", func(t *testing.T) {
|
||||
innerEffect := Of[InnerContext]("success")
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value + " modified"}
|
||||
}
|
||||
|
||||
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, string](OuterContext{
|
||||
Value: "original",
|
||||
Number: 50,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
})
|
||||
|
||||
t.Run("handles errors from inner effect", func(t *testing.T) {
|
||||
expectedErr := assert.AnError
|
||||
innerEffect := Fail[InnerContext, int](expectedErr)
|
||||
|
||||
accessor := func(outer OuterContext) InnerContext {
|
||||
return InnerContext{Value: outer.Value}
|
||||
}
|
||||
|
||||
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
|
||||
outerEffect := kleisli(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterContext, int](OuterContext{
|
||||
Value: "test",
|
||||
Number: 42,
|
||||
})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalAndContramapInteroperability(t *testing.T) {
|
||||
t.Run("can be used interchangeably", func(t *testing.T) {
|
||||
type Config1 struct {
|
||||
Value string
|
||||
}
|
||||
type Config2 struct {
|
||||
Data string
|
||||
}
|
||||
type Config3 struct {
|
||||
Info string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
effect3 := Of[Config3]("result")
|
||||
|
||||
// Use Local for first transformation
|
||||
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
|
||||
return Config3{Info: c2.Data}
|
||||
})
|
||||
|
||||
// Use Contramap for second transformation
|
||||
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
|
||||
return Config2{Data: c1.Value}
|
||||
})
|
||||
|
||||
// Compose them
|
||||
effect2 := local23(effect3)
|
||||
effect1 := contramap12(effect2)
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalEffectK(t *testing.T) {
|
||||
t.Run("transforms context using effectful function", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// Effect that needs DatabaseConfig
|
||||
dbEffect := Of[DatabaseConfig]("query result")
|
||||
|
||||
// Transform AppConfig to DatabaseConfig effectfully
|
||||
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
return Of[AppConfig](DatabaseConfig{
|
||||
ConnectionString: "loaded from " + app.ConfigPath,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply the transformation
|
||||
transform := LocalEffectK[string](loadConfig)
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
// Run with AppConfig
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
ConfigPath: "/etc/app.conf",
|
||||
})(appEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "query result", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from context transformation", func(t *testing.T) {
|
||||
type InnerCtx struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type OuterCtx struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
innerEffect := Of[InnerCtx]("success")
|
||||
|
||||
expectedErr := assert.AnError
|
||||
// Context transformation that fails
|
||||
failingTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Fail[OuterCtx, InnerCtx](expectedErr)
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string](failingTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates errors from inner effect", func(t *testing.T) {
|
||||
type InnerCtx struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type OuterCtx struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
expectedErr := assert.AnError
|
||||
innerEffect := Fail[InnerCtx, string](expectedErr)
|
||||
|
||||
// Successful context transformation
|
||||
transform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Of[OuterCtx](InnerCtx{Value: outer.Path})
|
||||
}
|
||||
|
||||
transformK := LocalEffectK[string](transform)
|
||||
outerEffect := transformK(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("allows effectful context transformation with IO operations", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
type AppContext struct {
|
||||
ConfigFile string
|
||||
}
|
||||
|
||||
// Effect that uses Config
|
||||
configEffect := Chain(func(cfg Config) Effect[Config, string] {
|
||||
return Of[Config]("processed: " + cfg.Data)
|
||||
})(readerreaderioresult.Ask[Config]())
|
||||
|
||||
// Effectful transformation that simulates loading config
|
||||
loadConfigEffect := func(app AppContext) Effect[AppContext, Config] {
|
||||
// Simulate IO operation (e.g., reading file)
|
||||
return Of[AppContext](Config{
|
||||
Data: "loaded from " + app.ConfigFile,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string](loadConfigEffect)
|
||||
appEffect := transform(configEffect)
|
||||
|
||||
ioResult := Provide[AppContext, string](AppContext{
|
||||
ConfigFile: "config.json",
|
||||
})(appEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "processed: loaded from config.json", result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple LocalEffectK transformations", func(t *testing.T) {
|
||||
type Level1 struct {
|
||||
A string
|
||||
}
|
||||
type Level2 struct {
|
||||
B string
|
||||
}
|
||||
type Level3 struct {
|
||||
C string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
level3Effect := Of[Level3]("deep result")
|
||||
|
||||
// Transform Level2 -> Level3 effectfully
|
||||
transform23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2](Level3{C: l2.B + "-c"})
|
||||
})
|
||||
|
||||
// Transform Level1 -> Level2 effectfully
|
||||
transform12 := LocalEffectK[string](func(l1 Level1) Effect[Level1, Level2] {
|
||||
return Of[Level1](Level2{B: l1.A + "-b"})
|
||||
})
|
||||
|
||||
// Compose transformations
|
||||
level2Effect := transform23(level3Effect)
|
||||
level1Effect := transform12(level2Effect)
|
||||
|
||||
// Run with Level1 context
|
||||
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "deep result", result)
|
||||
})
|
||||
|
||||
t.Run("accesses outer context during transformation", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Environment string
|
||||
DBHost string
|
||||
DBPort int
|
||||
}
|
||||
|
||||
// Effect that needs DatabaseConfig
|
||||
dbEffect := Chain(func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
|
||||
return Of[DatabaseConfig](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
|
||||
})(readerreaderioresult.Ask[DatabaseConfig]())
|
||||
|
||||
// Transform using outer context
|
||||
transformWithContext := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
|
||||
// Access outer context to build inner context
|
||||
prefix := ""
|
||||
if app.Environment == "prod" {
|
||||
prefix = "prod-"
|
||||
}
|
||||
return Of[AppConfig](DatabaseConfig{
|
||||
Host: prefix + app.DBHost,
|
||||
Port: app.DBPort,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string](transformWithContext)
|
||||
appEffect := transform(dbEffect)
|
||||
|
||||
ioResult := Provide[AppConfig, string](AppConfig{
|
||||
Environment: "prod",
|
||||
DBHost: "localhost",
|
||||
DBPort: 5432,
|
||||
})(appEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, result, "prod-localhost")
|
||||
})
|
||||
|
||||
t.Run("validates context during transformation", func(t *testing.T) {
|
||||
type ValidatedConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
type RawConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
innerEffect := Of[ValidatedConfig]("success")
|
||||
|
||||
// Validation that can fail
|
||||
validateConfig := func(raw RawConfig) Effect[RawConfig, ValidatedConfig] {
|
||||
if raw.APIKey == "" {
|
||||
return Fail[RawConfig, ValidatedConfig](assert.AnError)
|
||||
}
|
||||
return Of[RawConfig](ValidatedConfig{
|
||||
APIKey: raw.APIKey,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[string](validateConfig)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
// Test with invalid config
|
||||
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test with valid config
|
||||
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result, err2 := readerResult2(context.Background())
|
||||
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "success", result)
|
||||
})
|
||||
|
||||
t.Run("composes with other Local functions", func(t *testing.T) {
|
||||
type Level1 struct {
|
||||
Value string
|
||||
}
|
||||
type Level2 struct {
|
||||
Data string
|
||||
}
|
||||
type Level3 struct {
|
||||
Info string
|
||||
}
|
||||
|
||||
// Effect at deepest level
|
||||
effect3 := Of[Level3]("result")
|
||||
|
||||
// Use LocalEffectK for first transformation (effectful)
|
||||
localEffectK23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
|
||||
return Of[Level2](Level3{Info: l2.Data})
|
||||
})
|
||||
|
||||
// Use Local for second transformation (pure)
|
||||
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
|
||||
return Level2{Data: l1.Value}
|
||||
})
|
||||
|
||||
// Compose them
|
||||
effect2 := localEffectK23(effect3)
|
||||
effect1 := local12(effect2)
|
||||
|
||||
// Run
|
||||
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("handles complex nested effects in transformation", func(t *testing.T) {
|
||||
type InnerCtx struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type OuterCtx struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Effect that uses InnerCtx
|
||||
innerEffect := Chain(func(ctx InnerCtx) Effect[InnerCtx, int] {
|
||||
return Of[InnerCtx](ctx.Value * 2)
|
||||
})(readerreaderioresult.Ask[InnerCtx]())
|
||||
|
||||
// Complex transformation with nested effects
|
||||
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
|
||||
return Of[OuterCtx](InnerCtx{
|
||||
Value: outer.Multiplier * 10,
|
||||
})
|
||||
}
|
||||
|
||||
transform := LocalEffectK[int](complexTransform)
|
||||
outerEffect := transform(innerEffect)
|
||||
|
||||
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 60, result) // 3 * 10 * 2
|
||||
})
|
||||
}
|
||||
222
v2/effect/doc.go
Normal file
222
v2/effect/doc.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package effect provides a functional effect system for managing side effects in Go.
|
||||
|
||||
# Overview
|
||||
|
||||
The effect package is a high-level abstraction for composing effectful computations
|
||||
that may fail, require dependencies (context), and perform I/O operations. It is built
|
||||
on top of ReaderReaderIOResult, providing a clean API for dependency injection and
|
||||
error handling.
|
||||
|
||||
# Naming Conventions
|
||||
|
||||
The naming conventions in this package are modeled after effect-ts (https://effect.website/),
|
||||
a popular TypeScript library for functional effect systems. This alignment helps developers
|
||||
familiar with effect-ts to quickly understand and use this Go implementation.
|
||||
|
||||
# Core Type
|
||||
|
||||
The central type is Effect[C, A], which represents:
|
||||
- C: The context/dependency type required by the effect
|
||||
- A: The success value type produced by the effect
|
||||
|
||||
An Effect can:
|
||||
- Succeed with a value of type A
|
||||
- Fail with an error
|
||||
- Require a context of type C
|
||||
- Perform I/O operations
|
||||
|
||||
# Basic Operations
|
||||
|
||||
Creating Effects:
|
||||
|
||||
// Create a successful effect
|
||||
effect.Succeed[MyContext, string]("hello")
|
||||
|
||||
// Create a failed effect
|
||||
effect.Fail[MyContext, string](errors.New("failed"))
|
||||
|
||||
// Lift a pure value into an effect
|
||||
effect.Of[MyContext, int](42)
|
||||
|
||||
Transforming Effects:
|
||||
|
||||
// Map over the success value
|
||||
effect.Map[MyContext](func(x int) string {
|
||||
return strconv.Itoa(x)
|
||||
})
|
||||
|
||||
// Chain effects together (flatMap)
|
||||
effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
|
||||
return effect.Succeed[MyContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
// Tap into an effect without changing its value
|
||||
effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
|
||||
return effect.Succeed[MyContext, any](fmt.Println(x))
|
||||
})
|
||||
|
||||
# Dependency Injection
|
||||
|
||||
Effects can access their required context:
|
||||
|
||||
// Transform the context before passing it to an effect
|
||||
effect.Local[OuterCtx, InnerCtx](func(outer OuterCtx) InnerCtx {
|
||||
return outer.Inner
|
||||
})
|
||||
|
||||
// Provide a context to run an effect
|
||||
effect.Provide[MyContext, string](myContext)
|
||||
|
||||
# Do Notation
|
||||
|
||||
The package provides "do notation" for composing effects in a sequential, imperative style:
|
||||
|
||||
type State struct {
|
||||
X int
|
||||
Y string
|
||||
}
|
||||
|
||||
result := effect.Do[MyContext](State{}).
|
||||
Bind(func(y string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Y = y
|
||||
return s
|
||||
}
|
||||
}, fetchString).
|
||||
Let(func(x int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.X = x
|
||||
return s
|
||||
}
|
||||
}, func(s State) int {
|
||||
return len(s.Y)
|
||||
})
|
||||
|
||||
# Bind Operations
|
||||
|
||||
The package provides various bind operations for integrating with other effect types:
|
||||
|
||||
- BindIOK: Bind an IO operation
|
||||
- BindIOEitherK: Bind an IOEither operation
|
||||
- BindIOResultK: Bind an IOResult operation
|
||||
- BindReaderK: Bind a Reader operation
|
||||
- BindReaderIOK: Bind a ReaderIO operation
|
||||
- BindEitherK: Bind an Either operation
|
||||
|
||||
Each bind operation has a corresponding "L" variant for working with lenses:
|
||||
- BindL, BindIOKL, BindReaderKL, etc.
|
||||
|
||||
# Applicative Operations
|
||||
|
||||
Apply effects in parallel:
|
||||
|
||||
// Apply a function effect to a value effect
|
||||
effect.Ap[string, MyContext](valueEffect)(functionEffect)
|
||||
|
||||
// Apply effects to build up a structure
|
||||
effect.ApS[MyContext](setter, effect1)
|
||||
|
||||
# Traversal
|
||||
|
||||
Traverse collections with effects:
|
||||
|
||||
// Map an array with an effectful function
|
||||
effect.TraverseArray[MyContext](func(x int) Effect[MyContext, string] {
|
||||
return effect.Succeed[MyContext, string](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
# Retry Logic
|
||||
|
||||
Retry effects with configurable policies:
|
||||
|
||||
effect.Retrying[MyContext, string](
|
||||
retryPolicy,
|
||||
func(status retry.RetryStatus) Effect[MyContext, string] {
|
||||
return fetchData()
|
||||
},
|
||||
func(result Result[string]) bool {
|
||||
return result.IsLeft() // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
# Monoids
|
||||
|
||||
Combine effects using monoid operations:
|
||||
|
||||
// Combine effects using applicative semantics
|
||||
effect.ApplicativeMonoid[MyContext](stringMonoid)
|
||||
|
||||
// Combine effects using alternative semantics (first success)
|
||||
effect.AlternativeMonoid[MyContext](stringMonoid)
|
||||
|
||||
# Running Effects
|
||||
|
||||
To execute an effect:
|
||||
|
||||
// Provide the context
|
||||
ioResult := effect.Provide[MyContext, string](myContext)(myEffect)
|
||||
|
||||
// Run synchronously
|
||||
readerResult := effect.RunSync(ioResult)
|
||||
|
||||
// Execute with a context.Context
|
||||
value, err := readerResult(ctx)
|
||||
|
||||
# Integration with Other Packages
|
||||
|
||||
The effect package integrates seamlessly with other fp-go packages:
|
||||
- either: For error handling
|
||||
- io: For I/O operations
|
||||
- reader: For dependency injection
|
||||
- result: For result types
|
||||
- retry: For retry logic
|
||||
- monoid: For combining effects
|
||||
|
||||
# Example
|
||||
|
||||
type Config struct {
|
||||
APIKey string
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func fetchUser(id int) Effect[Config, User] {
|
||||
return effect.Chain[Config](func(cfg Config) Effect[Config, User] {
|
||||
// Use cfg.APIKey and cfg.BaseURL
|
||||
return effect.Succeed[Config, User](User{ID: id})
|
||||
})(effect.Of[Config, Config](Config{}))
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg := Config{APIKey: "key", BaseURL: "https://api.example.com"}
|
||||
userEffect := fetchUser(42)
|
||||
|
||||
// Run the effect
|
||||
ioResult := effect.Provide(cfg)(userEffect)
|
||||
readerResult := effect.RunSync(ioResult)
|
||||
user, err := readerResult(context.Background())
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("User: %+v\n", user)
|
||||
}
|
||||
*/
|
||||
package effect
|
||||
|
||||
//go:generate go run ../main.go lens --dir . --filename gen_lens.go --include-test-files
|
||||
51
v2/effect/effect.go
Normal file
51
v2/effect/effect.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func Succeed[C, A any](a A) Effect[C, A] {
|
||||
return readerreaderioresult.Of[C](a)
|
||||
}
|
||||
|
||||
func Fail[C, A any](err error) Effect[C, A] {
|
||||
return readerreaderioresult.Left[C, A](err)
|
||||
}
|
||||
|
||||
func Of[C, A any](a A) Effect[C, A] {
|
||||
return readerreaderioresult.Of[C](a)
|
||||
}
|
||||
|
||||
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
|
||||
return readerreaderioresult.Map[C](f)
|
||||
}
|
||||
|
||||
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.Chain(f)
|
||||
}
|
||||
|
||||
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
|
||||
return readerreaderioresult.Ap[B](fa)
|
||||
}
|
||||
|
||||
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
|
||||
return readerreaderioresult.Defer(fa)
|
||||
}
|
||||
|
||||
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
|
||||
return readerreaderioresult.Tap(f)
|
||||
}
|
||||
|
||||
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
|
||||
return function.Ternary(pred, onTrue, onFalse)
|
||||
}
|
||||
|
||||
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
|
||||
return readerreaderioresult.ChainResultK[C](f)
|
||||
}
|
||||
|
||||
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
506
v2/effect/effect_test.go
Normal file
506
v2/effect/effect_test.go
Normal file
@@ -0,0 +1,506 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestContext struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func runEffect[A any](eff Effect[TestContext, A], ctx TestContext) (A, error) {
|
||||
ioResult := Provide[TestContext, A](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
return readerResult(context.Background())
|
||||
}
|
||||
|
||||
func TestSucceed(t *testing.T) {
|
||||
t.Run("creates successful effect with value", func(t *testing.T) {
|
||||
eff := Succeed[TestContext](42)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("creates successful effect with string", func(t *testing.T) {
|
||||
eff := Succeed[TestContext]("hello")
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("creates successful effect with struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
eff := Succeed[TestContext](user)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFail(t *testing.T) {
|
||||
t.Run("creates failed effect with error", func(t *testing.T) {
|
||||
expectedErr := errors.New("test error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("creates failed effect with custom error", func(t *testing.T) {
|
||||
expectedErr := fmt.Errorf("custom error: %s", "details")
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
t.Run("lifts value into effect", func(t *testing.T) {
|
||||
eff := Of[TestContext](100)
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to Succeed", func(t *testing.T) {
|
||||
value := "test value"
|
||||
eff1 := Of[TestContext](value)
|
||||
eff2 := Succeed[TestContext](value)
|
||||
|
||||
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
|
||||
result2, err2 := runEffect(eff2, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("maps over successful effect", func(t *testing.T) {
|
||||
eff := Of[TestContext](10)
|
||||
mapped := Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(mapped, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("maps to different type", func(t *testing.T) {
|
||||
eff := Of[TestContext](42)
|
||||
mapped := Map[TestContext](func(x int) string {
|
||||
return fmt.Sprintf("value: %d", x)
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(mapped, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 42", result)
|
||||
})
|
||||
|
||||
t.Run("preserves error in failed effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("original error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
mapped := Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(mapped, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple maps", func(t *testing.T) {
|
||||
eff := Of[TestContext](5)
|
||||
result := Map[TestContext](func(x int) int {
|
||||
return x + 1
|
||||
})(Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 11, value) // (5 * 2) + 1
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("chains successful effects", func(t *testing.T) {
|
||||
eff := Of[TestContext](10)
|
||||
chained := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("chains to different type", func(t *testing.T) {
|
||||
eff := Of[TestContext](42)
|
||||
chained := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](fmt.Sprintf("number: %d", x))
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "number: 42", result)
|
||||
})
|
||||
|
||||
t.Run("propagates first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("first error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
chained := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates second error", func(t *testing.T) {
|
||||
expectedErr := errors.New("second error")
|
||||
eff := Of[TestContext](10)
|
||||
chained := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(chained, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple operations", func(t *testing.T) {
|
||||
eff := Of[TestContext](5)
|
||||
result := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x + 10)
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, value) // (5 * 2) + 10
|
||||
})
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("applies function effect to value effect", func(t *testing.T) {
|
||||
fn := Of[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
value := Of[TestContext](21)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
val, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, val)
|
||||
})
|
||||
|
||||
t.Run("applies function to different type", func(t *testing.T) {
|
||||
fn := Of[TestContext](func(x int) string {
|
||||
return fmt.Sprintf("value: %d", x)
|
||||
})
|
||||
value := Of[TestContext](42)
|
||||
|
||||
result := Ap[string](value)(fn)
|
||||
val, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 42", val)
|
||||
})
|
||||
|
||||
t.Run("propagates error from function effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("function error")
|
||||
fn := Fail[TestContext, func(int) int](expectedErr)
|
||||
value := Of[TestContext](42)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
_, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates error from value effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("value error")
|
||||
fn := Of[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
value := Fail[TestContext, int](expectedErr)
|
||||
|
||||
result := Ap[int](value)(fn)
|
||||
_, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSuspend(t *testing.T) {
|
||||
t.Run("suspends effect computation", func(t *testing.T) {
|
||||
callCount := 0
|
||||
eff := Suspend(func() Effect[TestContext, int] {
|
||||
callCount++
|
||||
return Of[TestContext](42)
|
||||
})
|
||||
|
||||
// Effect not executed yet
|
||||
assert.Equal(t, 0, callCount)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
|
||||
t.Run("suspends failing effect", func(t *testing.T) {
|
||||
expectedErr := errors.New("suspended error")
|
||||
eff := Suspend(func() Effect[TestContext, int] {
|
||||
return Fail[TestContext, int](expectedErr)
|
||||
})
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("allows lazy evaluation", func(t *testing.T) {
|
||||
var value int
|
||||
eff := Suspend(func() Effect[TestContext, int] {
|
||||
return Of[TestContext](value)
|
||||
})
|
||||
|
||||
value = 10
|
||||
result1, err1 := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
value = 20
|
||||
result2, err2 := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, 10, result1)
|
||||
assert.Equal(t, 20, result2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTap(t *testing.T) {
|
||||
t.Run("executes side effect without changing value", func(t *testing.T) {
|
||||
sideEffectValue := 0
|
||||
eff := Of[TestContext](42)
|
||||
tapped := Tap(func(x int) Effect[TestContext, any] {
|
||||
sideEffectValue = x * 2
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff)
|
||||
|
||||
result, err := runEffect(tapped, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 84, sideEffectValue)
|
||||
})
|
||||
|
||||
t.Run("propagates original error", func(t *testing.T) {
|
||||
expectedErr := errors.New("original error")
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
tapped := Tap(func(x int) Effect[TestContext, any] {
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(tapped, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates tap error", func(t *testing.T) {
|
||||
expectedErr := errors.New("tap error")
|
||||
eff := Of[TestContext](42)
|
||||
tapped := Tap(func(x int) Effect[TestContext, any] {
|
||||
return Fail[TestContext, any](expectedErr)
|
||||
})(eff)
|
||||
|
||||
_, err := runEffect(tapped, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("chains multiple taps", func(t *testing.T) {
|
||||
values := []int{}
|
||||
eff := Of[TestContext](10)
|
||||
result := Tap(func(x int) Effect[TestContext, any] {
|
||||
values = append(values, x+2)
|
||||
return Of[TestContext, any](nil)
|
||||
})(Tap(func(x int) Effect[TestContext, any] {
|
||||
values = append(values, x+1)
|
||||
return Of[TestContext, any](nil)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, value)
|
||||
assert.Equal(t, []int{11, 12}, values)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTernary(t *testing.T) {
|
||||
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(kleisli(15), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "greater", result)
|
||||
})
|
||||
|
||||
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(kleisli(5), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "less or equal", result)
|
||||
})
|
||||
|
||||
t.Run("handles errors in onTrue branch", func(t *testing.T) {
|
||||
expectedErr := errors.New("true branch error")
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("less or equal")
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(kleisli(15), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("handles errors in onFalse branch", func(t *testing.T) {
|
||||
expectedErr := errors.New("false branch error")
|
||||
kleisli := Ternary(
|
||||
func(x int) bool { return x > 10 },
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("greater")
|
||||
},
|
||||
func(x int) Effect[TestContext, string] {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(kleisli(5), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEffectComposition(t *testing.T) {
|
||||
t.Run("composes Map and Chain", func(t *testing.T) {
|
||||
eff := Of[TestContext](5)
|
||||
result := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](fmt.Sprintf("result: %d", x))
|
||||
})(Map[TestContext](func(x int) int {
|
||||
return x * 2
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result: 10", value)
|
||||
})
|
||||
|
||||
t.Run("composes Chain and Tap", func(t *testing.T) {
|
||||
sideEffect := 0
|
||||
eff := Of[TestContext](10)
|
||||
result := Tap(func(x int) Effect[TestContext, any] {
|
||||
sideEffect = x
|
||||
return Of[TestContext, any](nil)
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(eff))
|
||||
|
||||
value, err := runEffect(result, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 20, value)
|
||||
assert.Equal(t, 20, sideEffect)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEffectWithResult(t *testing.T) {
|
||||
t.Run("converts result to effect", func(t *testing.T) {
|
||||
res := result.Of(42)
|
||||
// This demonstrates integration with result package
|
||||
assert.True(t, result.IsRight(res))
|
||||
})
|
||||
}
|
||||
118
v2/effect/gen_lens_test.go
Normal file
118
v2/effect/gen_lens_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package effect
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2026-01-27 22:19:41.6840253 +0100 CET m=+0.008579701
|
||||
|
||||
import (
|
||||
__lens "github.com/IBM/fp-go/v2/optics/lens"
|
||||
__option "github.com/IBM/fp-go/v2/option"
|
||||
__prism "github.com/IBM/fp-go/v2/optics/prism"
|
||||
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
|
||||
)
|
||||
|
||||
// ComplexServiceLenses provides lenses for accessing fields of ComplexService
|
||||
type ComplexServiceLenses struct {
|
||||
// mandatory fields
|
||||
service1 __lens.Lens[ComplexService, Service1]
|
||||
service2 __lens.Lens[ComplexService, Service2]
|
||||
// optional fields
|
||||
service1O __lens_option.LensO[ComplexService, Service1]
|
||||
service2O __lens_option.LensO[ComplexService, Service2]
|
||||
}
|
||||
|
||||
// ComplexServiceRefLenses provides lenses for accessing fields of ComplexService via a reference to ComplexService
|
||||
type ComplexServiceRefLenses struct {
|
||||
// mandatory fields
|
||||
service1 __lens.Lens[*ComplexService, Service1]
|
||||
service2 __lens.Lens[*ComplexService, Service2]
|
||||
// optional fields
|
||||
service1O __lens_option.LensO[*ComplexService, Service1]
|
||||
service2O __lens_option.LensO[*ComplexService, Service2]
|
||||
// prisms
|
||||
service1P __prism.Prism[*ComplexService, Service1]
|
||||
service2P __prism.Prism[*ComplexService, Service2]
|
||||
}
|
||||
|
||||
// ComplexServicePrisms provides prisms for accessing fields of ComplexService
|
||||
type ComplexServicePrisms struct {
|
||||
service1 __prism.Prism[ComplexService, Service1]
|
||||
service2 __prism.Prism[ComplexService, Service2]
|
||||
}
|
||||
|
||||
// MakeComplexServiceLenses creates a new ComplexServiceLenses with lenses for all fields
|
||||
func MakeComplexServiceLenses() ComplexServiceLenses {
|
||||
// mandatory lenses
|
||||
lensservice1 := __lens.MakeLensWithName(
|
||||
func(s ComplexService) Service1 { return s.service1 },
|
||||
func(s ComplexService, v Service1) ComplexService { s.service1 = v; return s },
|
||||
"ComplexService.service1",
|
||||
)
|
||||
lensservice2 := __lens.MakeLensWithName(
|
||||
func(s ComplexService) Service2 { return s.service2 },
|
||||
func(s ComplexService, v Service2) ComplexService { s.service2 = v; return s },
|
||||
"ComplexService.service2",
|
||||
)
|
||||
// optional lenses
|
||||
lensservice1O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
|
||||
lensservice2O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
|
||||
return ComplexServiceLenses{
|
||||
// mandatory lenses
|
||||
service1: lensservice1,
|
||||
service2: lensservice2,
|
||||
// optional lenses
|
||||
service1O: lensservice1O,
|
||||
service2O: lensservice2O,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeComplexServiceRefLenses creates a new ComplexServiceRefLenses with lenses for all fields
|
||||
func MakeComplexServiceRefLenses() ComplexServiceRefLenses {
|
||||
// mandatory lenses
|
||||
lensservice1 := __lens.MakeLensStrictWithName(
|
||||
func(s *ComplexService) Service1 { return s.service1 },
|
||||
func(s *ComplexService, v Service1) *ComplexService { s.service1 = v; return s },
|
||||
"(*ComplexService).service1",
|
||||
)
|
||||
lensservice2 := __lens.MakeLensStrictWithName(
|
||||
func(s *ComplexService) Service2 { return s.service2 },
|
||||
func(s *ComplexService, v Service2) *ComplexService { s.service2 = v; return s },
|
||||
"(*ComplexService).service2",
|
||||
)
|
||||
// optional lenses
|
||||
lensservice1O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
|
||||
lensservice2O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
|
||||
return ComplexServiceRefLenses{
|
||||
// mandatory lenses
|
||||
service1: lensservice1,
|
||||
service2: lensservice2,
|
||||
// optional lenses
|
||||
service1O: lensservice1O,
|
||||
service2O: lensservice2O,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeComplexServicePrisms creates a new ComplexServicePrisms with prisms for all fields
|
||||
func MakeComplexServicePrisms() ComplexServicePrisms {
|
||||
_fromNonZeroservice1 := __option.FromNonZero[Service1]()
|
||||
_prismservice1 := __prism.MakePrismWithName(
|
||||
func(s ComplexService) __option.Option[Service1] { return _fromNonZeroservice1(s.service1) },
|
||||
func(v Service1) ComplexService {
|
||||
return ComplexService{ service1: v }
|
||||
},
|
||||
"ComplexService.service1",
|
||||
)
|
||||
_fromNonZeroservice2 := __option.FromNonZero[Service2]()
|
||||
_prismservice2 := __prism.MakePrismWithName(
|
||||
func(s ComplexService) __option.Option[Service2] { return _fromNonZeroservice2(s.service2) },
|
||||
func(v Service2) ComplexService {
|
||||
return ComplexService{ service2: v }
|
||||
},
|
||||
"ComplexService.service2",
|
||||
)
|
||||
return ComplexServicePrisms {
|
||||
service1: _prismservice1,
|
||||
service2: _prismservice2,
|
||||
}
|
||||
}
|
||||
207
v2/effect/injection_test.go
Normal file
207
v2/effect/injection_test.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Package effect demonstrates dependency injection using the Effect pattern.
|
||||
//
|
||||
// This test file shows how to build a type-safe dependency injection system where:
|
||||
// - An InjectionContainer can resolve services by ID (InjectionToken)
|
||||
// - Services are generic effects that depend on the container
|
||||
// - Lookup methods convert from untyped container to typed dependencies
|
||||
// - Handler functions depend type-safely on specific service interfaces
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
// InjectionToken is a unique identifier for services in the container
|
||||
InjectionToken string
|
||||
|
||||
// InjectionContainer is an Effect that resolves services by their token.
|
||||
// It takes an InjectionToken and returns a Thunk that produces any type.
|
||||
// This allows the container to store and retrieve services of different types.
|
||||
InjectionContainer = Effect[InjectionToken, any]
|
||||
|
||||
// Service is a generic Effect that depends on the InjectionContainer.
|
||||
// It represents a computation that needs access to the dependency injection
|
||||
// container to resolve its dependencies before producing a string result.
|
||||
Service[T any] = Effect[InjectionContainer, T]
|
||||
|
||||
// Service1 is an example service interface that can be resolved from the container
|
||||
Service1 interface {
|
||||
GetService1() string
|
||||
}
|
||||
|
||||
// Service2 is another example service interface
|
||||
Service2 interface {
|
||||
GetService2() string
|
||||
}
|
||||
|
||||
// impl1 is a concrete implementation of Service1
|
||||
impl1 struct{}
|
||||
// impl2 is a concrete implementation of Service2
|
||||
impl2 struct{}
|
||||
)
|
||||
|
||||
// ComplexService demonstrates a more complex dependency injection scenario
|
||||
// where a service depends on multiple other services. This struct aggregates
|
||||
// Service1 and Service2, showing how to compose dependencies.
|
||||
// The fp-go:Lens directive generates lens functions for type-safe field access.
|
||||
//
|
||||
// fp-go:Lens
|
||||
type ComplexService struct {
|
||||
service1 Service1
|
||||
service2 Service2
|
||||
}
|
||||
|
||||
func (_ *impl1) GetService1() string {
|
||||
return "service1"
|
||||
}
|
||||
|
||||
func (_ *impl2) GetService2() string {
|
||||
return "service2"
|
||||
}
|
||||
|
||||
const (
|
||||
// service1 is the injection token for Service1
|
||||
service1 = InjectionToken("service1")
|
||||
// service2 is the injection token for Service2
|
||||
service2 = InjectionToken("service2")
|
||||
)
|
||||
|
||||
var (
|
||||
// complexServiceLenses provides type-safe accessors for ComplexService fields,
|
||||
// generated by the fp-go:Lens directive. These lenses are used in applicative
|
||||
// composition to build the ComplexService from individual dependencies.
|
||||
complexServiceLenses = MakeComplexServiceLenses()
|
||||
)
|
||||
|
||||
// makeSampleInjectionContainer creates an InjectionContainer that can resolve services by ID.
|
||||
// The container maps InjectionTokens to their corresponding service implementations.
|
||||
// It returns an error if a requested service is not available.
|
||||
func makeSampleInjectionContainer() InjectionContainer {
|
||||
|
||||
return func(token InjectionToken) Thunk[any] {
|
||||
switch token {
|
||||
case service1:
|
||||
return thunk.Of(any(&impl1{}))
|
||||
case service2:
|
||||
return thunk.Of(any(&impl2{}))
|
||||
default:
|
||||
return thunk.Left[any](errors.New("dependency not available"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleService1 is an Effect that depends type-safely on Service1.
|
||||
// It demonstrates how to write handlers that work with specific service interfaces
|
||||
// rather than the untyped container, providing compile-time type safety.
|
||||
func handleService1() Effect[Service1, string] {
|
||||
return func(ctx Service1) ReaderIOResult[string] {
|
||||
return thunk.Of(fmt.Sprintf("Service1: %s", ctx.GetService1()))
|
||||
}
|
||||
}
|
||||
|
||||
// handleComplexService is an Effect that depends on ComplexService, which itself
|
||||
// aggregates multiple service dependencies (Service1 and Service2).
|
||||
// This demonstrates how to work with composite dependencies in a type-safe manner.
|
||||
func handleComplexService() Effect[ComplexService, string] {
|
||||
return func(ctx ComplexService) ReaderIOResult[string] {
|
||||
return thunk.Of(fmt.Sprintf("ComplexService: %s x %s", ctx.service1.GetService1(), ctx.service2.GetService2()))
|
||||
}
|
||||
}
|
||||
|
||||
// lookupService1 is a lookup method that converts from an untyped InjectionContainer
|
||||
// to a typed Service1 dependency. It performs two steps:
|
||||
// 1. Read[any](service1) - retrieves the service from the container by token
|
||||
// 2. ChainResultK(result.InstanceOf[Service1]) - safely casts from any to Service1
|
||||
// This conversion provides type safety when moving from the untyped container to typed handlers.
|
||||
var lookupService1 = F.Flow2(
|
||||
Read[any](service1),
|
||||
thunk.ChainResultK(result.InstanceOf[Service1]),
|
||||
)
|
||||
|
||||
// lookupService2 is a lookup method for Service2, following the same pattern as lookupService1.
|
||||
// It retrieves Service2 from the container and safely casts it to the correct type.
|
||||
var lookupService2 = F.Flow2(
|
||||
Read[any](service2),
|
||||
thunk.ChainResultK(result.InstanceOf[Service2]),
|
||||
)
|
||||
|
||||
// lookupComplexService demonstrates applicative composition for complex dependency injection.
|
||||
// It builds a ComplexService by composing multiple service lookups:
|
||||
// 1. Do[InjectionContainer](ComplexService{}) - starts with an empty ComplexService in the Effect context
|
||||
// 2. ApSL(complexServiceLenses.service1, lookupService1) - looks up Service1 and sets it using the lens
|
||||
// 3. ApSL(complexServiceLenses.service2, lookupService2) - looks up Service2 and sets it using the lens
|
||||
//
|
||||
// This applicative style allows parallel composition of independent dependencies,
|
||||
// building the complete ComplexService from its constituent parts in a type-safe way.
|
||||
var lookupComplexService = F.Pipe2(
|
||||
Do[InjectionContainer](ComplexService{}),
|
||||
ApSL(complexServiceLenses.service1, lookupService1),
|
||||
ApSL(complexServiceLenses.service2, lookupService2),
|
||||
)
|
||||
|
||||
// handleResult is a curried function that combines results from two services.
|
||||
// It demonstrates how to compose the outputs of multiple effects into a final result.
|
||||
// The curried form allows it to be used with applicative composition (ApS).
|
||||
func handleResult(s1 string) func(string) string {
|
||||
return func(s2 string) string {
|
||||
return fmt.Sprintf("Final Result: %s : %s", s1, s2)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDependencyLookup demonstrates both simple and complex dependency injection patterns:
|
||||
//
|
||||
// Simple Pattern (handle1):
|
||||
// 1. Create an InjectionContainer with registered services
|
||||
// 2. Define a handler (handleService1) that depends on a single typed service interface
|
||||
// 3. Use a lookup method (lookupService1) to resolve the dependency from the container
|
||||
// 4. Compose the handler with the lookup using LocalThunkK to inject the dependency
|
||||
//
|
||||
// Complex Pattern (handleComplex):
|
||||
// 1. Define a handler (handleComplexService) that depends on a composite service (ComplexService)
|
||||
// 2. Use applicative composition (lookupComplexService) to build the composite from multiple lookups
|
||||
// 3. Each sub-dependency is resolved independently and combined using lenses
|
||||
// 4. LocalThunkK injects the complete composite dependency into the handler
|
||||
//
|
||||
// Service Composition:
|
||||
// - ApS combines the results of handle1 and handleComplex using handleResult
|
||||
// - This demonstrates how to compose multiple independent effects that share the same container
|
||||
// - The final result aggregates outputs from both simple and complex dependency patterns
|
||||
func TestDependencyLookup(t *testing.T) {
|
||||
|
||||
// Create the dependency injection container
|
||||
container := makeSampleInjectionContainer()
|
||||
|
||||
// Simple dependency injection: single service lookup
|
||||
// LocalThunkK transforms the handler to work with the container
|
||||
handle1 := F.Pipe1(
|
||||
handleService1(),
|
||||
LocalThunkK[string](lookupService1),
|
||||
)
|
||||
|
||||
// Complex dependency injection: composite service with multiple dependencies
|
||||
// lookupComplexService uses applicative composition to build ComplexService
|
||||
handleComplex := F.Pipe1(
|
||||
handleComplexService(),
|
||||
LocalThunkK[string](lookupComplexService),
|
||||
)
|
||||
|
||||
// Compose both services using applicative style
|
||||
// ApS applies handleResult to combine outputs from handle1 and handleComplex
|
||||
result := F.Pipe1(
|
||||
handle1,
|
||||
ApS(handleResult, handleComplex),
|
||||
)
|
||||
|
||||
// Execute: provide container, then context, then run the IO operation
|
||||
res := result(container)(t.Context())()
|
||||
|
||||
fmt.Println(res)
|
||||
|
||||
}
|
||||
14
v2/effect/monoid.go
Normal file
14
v2/effect/monoid.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
|
||||
return readerreaderioresult.ApplicativeMonoid[C](m)
|
||||
}
|
||||
|
||||
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
|
||||
return readerreaderioresult.AlternativeMonoid[C](m)
|
||||
}
|
||||
350
v2/effect/monoid_test.go
Normal file
350
v2/effect/monoid_test.go
Normal file
@@ -0,0 +1,350 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("combines successful effects with string monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext]("Hello")
|
||||
eff2 := Of[TestContext](" ")
|
||||
eff3 := Of[TestContext]("World")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello World", result)
|
||||
})
|
||||
|
||||
t.Run("combines successful effects with int monoid", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
|
||||
|
||||
eff1 := Of[TestContext](10)
|
||||
eff2 := Of[TestContext](20)
|
||||
eff3 := Of[TestContext](30)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 60, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty value for empty monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"empty",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "empty", result)
|
||||
})
|
||||
|
||||
t.Run("propagates first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("first error")
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](expectedErr)
|
||||
eff2 := Of[TestContext]("World")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
_, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("propagates second error", func(t *testing.T) {
|
||||
expectedErr := errors.New("second error")
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext]("Hello")
|
||||
eff2 := Fail[TestContext, string](expectedErr)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
_, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("combines multiple effects", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a * b },
|
||||
1,
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
|
||||
|
||||
effects := []Effect[TestContext, int]{
|
||||
Of[TestContext](2),
|
||||
Of[TestContext](3),
|
||||
Of[TestContext](4),
|
||||
Of[TestContext](5),
|
||||
}
|
||||
|
||||
combined := effectMonoid.Empty()
|
||||
for _, eff := range effects {
|
||||
combined = effectMonoid.Concat(combined, eff)
|
||||
}
|
||||
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 120, result) // 1 * 2 * 3 * 4 * 5
|
||||
})
|
||||
|
||||
t.Run("works with custom types", func(t *testing.T) {
|
||||
type Counter struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
counterMonoid := monoid.MakeMonoid(
|
||||
func(a, b Counter) Counter {
|
||||
return Counter{Count: a.Count + b.Count}
|
||||
},
|
||||
Counter{Count: 0},
|
||||
)
|
||||
|
||||
effectMonoid := ApplicativeMonoid[TestContext](counterMonoid)
|
||||
|
||||
eff1 := Of[TestContext](Counter{Count: 5})
|
||||
eff2 := Of[TestContext](Counter{Count: 10})
|
||||
eff3 := Of[TestContext](Counter{Count: 15})
|
||||
|
||||
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result.Count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAlternativeMonoid(t *testing.T) {
|
||||
t.Run("combines successful effects with monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext]("First")
|
||||
eff2 := Of[TestContext]("Second")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "FirstSecond", result) // Alternative still combines when both succeed
|
||||
})
|
||||
|
||||
t.Run("tries second effect if first fails", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](errors.New("first failed"))
|
||||
eff2 := Of[TestContext]("Second")
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Second", result)
|
||||
})
|
||||
|
||||
t.Run("returns error if all effects fail", func(t *testing.T) {
|
||||
expectedErr := errors.New("second error")
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, string](errors.New("first error"))
|
||||
eff2 := Fail[TestContext, string](expectedErr)
|
||||
|
||||
combined := effectMonoid.Concat(eff1, eff2)
|
||||
_, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("returns empty value for empty monoid", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + b },
|
||||
"default",
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "default", result)
|
||||
})
|
||||
|
||||
t.Run("chains multiple alternatives", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext](intMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, int](errors.New("error 1"))
|
||||
eff2 := Fail[TestContext, int](errors.New("error 2"))
|
||||
eff3 := Of[TestContext](42)
|
||||
eff4 := Of[TestContext](100)
|
||||
|
||||
combined := effectMonoid.Concat(
|
||||
effectMonoid.Concat(eff1, eff2),
|
||||
effectMonoid.Concat(eff3, eff4),
|
||||
)
|
||||
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 142, result) // Combines successful values: 42 + 100
|
||||
})
|
||||
|
||||
t.Run("works with custom types", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value string
|
||||
Code int
|
||||
}
|
||||
|
||||
resultMonoid := monoid.MakeMonoid(
|
||||
func(a, b Result) Result {
|
||||
return Result{Value: a.Value + b.Value, Code: a.Code + b.Code}
|
||||
},
|
||||
Result{Value: "", Code: 0},
|
||||
)
|
||||
|
||||
effectMonoid := AlternativeMonoid[TestContext](resultMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, Result](errors.New("failed"))
|
||||
eff2 := Of[TestContext](Result{Value: "success", Code: 200})
|
||||
eff3 := Of[TestContext](Result{Value: "backup", Code: 201})
|
||||
|
||||
combined := effectMonoid.Concat(effectMonoid.Concat(eff1, eff2), eff3)
|
||||
result, err := runEffect(combined, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "successbackup", result.Value) // Combines both successful values
|
||||
assert.Equal(t, 401, result.Code) // 200 + 201
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonoidComparison(t *testing.T) {
|
||||
t.Run("ApplicativeMonoid vs AlternativeMonoid with all success", func(t *testing.T) {
|
||||
stringMonoid := monoid.MakeMonoid(
|
||||
func(a, b string) string { return a + "," + b },
|
||||
"",
|
||||
)
|
||||
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext](stringMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext](stringMonoid)
|
||||
|
||||
eff1 := Of[TestContext]("A")
|
||||
eff2 := Of[TestContext]("B")
|
||||
|
||||
// Applicative combines values
|
||||
applicativeResult, err1 := runEffect(
|
||||
applicativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
// Alternative takes first
|
||||
alternativeResult, err2 := runEffect(
|
||||
alternativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "A,B", applicativeResult) // Combined with comma separator
|
||||
assert.Equal(t, "A,B", alternativeResult) // Also combined (Alternative uses Alt semigroup)
|
||||
})
|
||||
|
||||
t.Run("ApplicativeMonoid vs AlternativeMonoid with failures", func(t *testing.T) {
|
||||
intMonoid := monoid.MakeMonoid(
|
||||
func(a, b int) int { return a + b },
|
||||
0,
|
||||
)
|
||||
|
||||
applicativeMonoid := ApplicativeMonoid[TestContext](intMonoid)
|
||||
alternativeMonoid := AlternativeMonoid[TestContext](intMonoid)
|
||||
|
||||
eff1 := Fail[TestContext, int](errors.New("error 1"))
|
||||
eff2 := Of[TestContext](42)
|
||||
|
||||
// Applicative fails on first error
|
||||
_, err1 := runEffect(
|
||||
applicativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
// Alternative tries second on first failure
|
||||
result2, err2 := runEffect(
|
||||
alternativeMonoid.Concat(eff1, eff2),
|
||||
TestContext{Value: "test"},
|
||||
)
|
||||
|
||||
assert.Error(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, 42, result2)
|
||||
})
|
||||
}
|
||||
14
v2/effect/retry.go
Normal file
14
v2/effect/retry.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
)
|
||||
|
||||
func Retrying[C, A any](
|
||||
policy retry.RetryPolicy,
|
||||
action Kleisli[C, retry.RetryStatus, A],
|
||||
check Predicate[Result[A]],
|
||||
) Effect[C, A] {
|
||||
return readerreaderioresult.Retrying(policy, action, check)
|
||||
}
|
||||
377
v2/effect/retry_test.go
Normal file
377
v2/effect/retry_test.go
Normal file
@@ -0,0 +1,377 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/retry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRetrying(t *testing.T) {
|
||||
t.Run("succeeds on first attempt", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
return Of[TestContext]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
assert.Equal(t, 1, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("retries on failure and eventually succeeds", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 3 {
|
||||
return Fail[TestContext, string](errors.New("temporary error"))
|
||||
}
|
||||
return Of[TestContext]("success after retries")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success after retries", result)
|
||||
assert.Equal(t, 3, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("exhausts retry limit", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
maxRetries := uint(3)
|
||||
policy := retry.LimitRetries(maxRetries)
|
||||
|
||||
eff := Retrying(
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
return Fail[TestContext, string](errors.New("persistent error"))
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, int(maxRetries+1), attemptCount) // initial attempt + retries
|
||||
})
|
||||
|
||||
t.Run("does not retry on success", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, int](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
return Of[TestContext](42)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
return result.IsLeft(res) // retry on error
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 1, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("uses custom retry predicate", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying[TestContext, int](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
return Of[TestContext](attemptCount * 10)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
// Retry if value is less than 30
|
||||
if result.IsRight(res) {
|
||||
val, _ := result.Unwrap(res)
|
||||
return val < 30
|
||||
}
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
assert.Equal(t, 3, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("tracks retry status", func(t *testing.T) {
|
||||
var statuses []retry.RetryStatus
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
statuses = append(statuses, status)
|
||||
if len(statuses) < 3 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext]("done")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "done", result)
|
||||
assert.Len(t, statuses, 3)
|
||||
// First attempt has iteration 0
|
||||
assert.Equal(t, uint(0), statuses[0].IterNumber)
|
||||
assert.Equal(t, uint(1), statuses[1].IterNumber)
|
||||
assert.Equal(t, uint(2), statuses[2].IterNumber)
|
||||
})
|
||||
|
||||
t.Run("works with exponential backoff", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.Monoid.Concat(
|
||||
retry.LimitRetries(3),
|
||||
retry.ExponentialBackoff(10*time.Millisecond),
|
||||
)
|
||||
|
||||
startTime := time.Now()
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 3 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
elapsed := time.Since(startTime)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", result)
|
||||
assert.Equal(t, 3, attemptCount)
|
||||
// Should have some delay due to backoff
|
||||
assert.Greater(t, elapsed, 10*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("combines with other effect operations", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Map[TestContext](func(s string) string {
|
||||
return "mapped: " + s
|
||||
})(Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext]("success")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "mapped: success", result)
|
||||
assert.Equal(t, 2, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("retries with different error types", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
errors := []error{
|
||||
errors.New("error 1"),
|
||||
errors.New("error 2"),
|
||||
errors.New("error 3"),
|
||||
}
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
if attemptCount < len(errors) {
|
||||
err := errors[attemptCount]
|
||||
attemptCount++
|
||||
return Fail[TestContext, string](err)
|
||||
}
|
||||
attemptCount++
|
||||
return Of[TestContext]("finally succeeded")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "finally succeeded", result)
|
||||
assert.Equal(t, 4, attemptCount)
|
||||
})
|
||||
|
||||
t.Run("no retry when predicate returns false", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(5)
|
||||
|
||||
eff := Retrying(
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
return Fail[TestContext, string](errors.New("error"))
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return false // never retry
|
||||
},
|
||||
)
|
||||
|
||||
_, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, attemptCount) // only initial attempt
|
||||
})
|
||||
|
||||
t.Run("retries with context access", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
ctx := TestContext{Value: "retry-context"}
|
||||
|
||||
eff := Retrying[TestContext, string](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, string] {
|
||||
attemptCount++
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, string](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext]("success with context")
|
||||
},
|
||||
func(res Result[string]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success with context", result)
|
||||
assert.Equal(t, 2, attemptCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRetryingWithComplexScenarios(t *testing.T) {
|
||||
t.Run("retry with state accumulation", func(t *testing.T) {
|
||||
type State struct {
|
||||
Attempts []int
|
||||
Value string
|
||||
}
|
||||
|
||||
policy := retry.LimitRetries(4)
|
||||
|
||||
eff := Retrying[TestContext, State](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, State] {
|
||||
state := State{
|
||||
Attempts: make([]int, status.IterNumber+1),
|
||||
Value: "attempt",
|
||||
}
|
||||
for i := uint(0); i <= status.IterNumber; i++ {
|
||||
state.Attempts[i] = int(i)
|
||||
}
|
||||
|
||||
if status.IterNumber < 2 {
|
||||
return Fail[TestContext, State](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext](state)
|
||||
},
|
||||
func(res Result[State]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
)
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "attempt", result.Value)
|
||||
assert.Equal(t, []int{0, 1, 2}, result.Attempts)
|
||||
})
|
||||
|
||||
t.Run("retry with chain operations", func(t *testing.T) {
|
||||
attemptCount := 0
|
||||
policy := retry.LimitRetries(3)
|
||||
|
||||
eff := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("final: " + string(rune('0'+x)))
|
||||
})(Retrying[TestContext, int](
|
||||
policy,
|
||||
func(status retry.RetryStatus) Effect[TestContext, int] {
|
||||
attemptCount++
|
||||
if attemptCount < 2 {
|
||||
return Fail[TestContext, int](errors.New("retry"))
|
||||
}
|
||||
return Of[TestContext](attemptCount)
|
||||
},
|
||||
func(res Result[int]) bool {
|
||||
return result.IsLeft(res)
|
||||
},
|
||||
))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, result, "final:")
|
||||
})
|
||||
}
|
||||
19
v2/effect/run.go
Normal file
19
v2/effect/run.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
|
||||
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
|
||||
return func(ctx context.Context) (A, error) {
|
||||
return result.Unwrap(fa(ctx)())
|
||||
}
|
||||
}
|
||||
326
v2/effect/run_test.go
Normal file
326
v2/effect/run_test.go
Normal file
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProvide(t *testing.T) {
|
||||
t.Run("provides context to effect", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test-value"}
|
||||
eff := Of[TestContext]("result")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("provides context with specific values", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
cfg := Config{Host: "localhost", Port: 8080}
|
||||
eff := Of[Config]("connected")
|
||||
|
||||
ioResult := Provide[Config, string](cfg)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "connected", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors", func(t *testing.T) {
|
||||
expectedErr := errors.New("provide error")
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Fail[TestContext, string](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("works with different context types", func(t *testing.T) {
|
||||
type SimpleContext struct {
|
||||
ID int
|
||||
}
|
||||
|
||||
ctx := SimpleContext{ID: 42}
|
||||
eff := Of[SimpleContext](100)
|
||||
|
||||
ioResult := Provide[SimpleContext, int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 100, result)
|
||||
})
|
||||
|
||||
t.Run("provides context to chained effects", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "base"}
|
||||
|
||||
eff := Chain(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext]("result")
|
||||
})(Of[TestContext](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
|
||||
t.Run("provides context to mapped effects", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Map[TestContext](func(x int) string {
|
||||
return "mapped"
|
||||
})(Of[TestContext](42))
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "mapped", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunSync(t *testing.T) {
|
||||
t.Run("runs effect synchronously", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("runs effect with context.Context", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext]("hello")
|
||||
|
||||
ioResult := Provide[TestContext, string](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
|
||||
bgCtx := context.Background()
|
||||
result, err := readerResult(bgCtx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", result)
|
||||
})
|
||||
|
||||
t.Run("propagates errors synchronously", func(t *testing.T) {
|
||||
expectedErr := errors.New("sync error")
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Fail[TestContext, int](expectedErr)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
_, err := readerResult(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("runs complex effect chains", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x + 10)
|
||||
})(Of[TestContext](5)))
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result) // (5 + 10) * 2
|
||||
})
|
||||
|
||||
t.Run("handles multiple sequential runs", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
eff := Of[TestContext](42)
|
||||
|
||||
ioResult := Provide[TestContext, int](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
|
||||
// Run multiple times
|
||||
result1, err1 := readerResult(context.Background())
|
||||
result2, err2 := readerResult(context.Background())
|
||||
result3, err3 := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.NoError(t, err3)
|
||||
assert.Equal(t, 42, result1)
|
||||
assert.Equal(t, 42, result2)
|
||||
assert.Equal(t, 42, result3)
|
||||
})
|
||||
|
||||
t.Run("works with different result types", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
ctx := TestContext{Value: "test"}
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
eff := Of[TestContext](user)
|
||||
|
||||
ioResult := Provide[TestContext, User](ctx)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvideAndRunSyncIntegration(t *testing.T) {
|
||||
t.Run("complete workflow with success", func(t *testing.T) {
|
||||
type AppConfig struct {
|
||||
APIKey string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
cfg := AppConfig{APIKey: "secret", Timeout: 30}
|
||||
|
||||
// Create an effect that uses the config
|
||||
eff := Of[AppConfig]("API call successful")
|
||||
|
||||
// Provide config and run
|
||||
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "API call successful", result)
|
||||
})
|
||||
|
||||
t.Run("complete workflow with error", func(t *testing.T) {
|
||||
type AppConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
expectedErr := errors.New("API error")
|
||||
cfg := AppConfig{APIKey: "secret"}
|
||||
|
||||
eff := Fail[AppConfig, string](expectedErr)
|
||||
|
||||
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("workflow with transformations", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Map[TestContext](func(x int) string {
|
||||
return "final"
|
||||
})(Chain(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(Of[TestContext](21)))
|
||||
|
||||
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "final", result)
|
||||
})
|
||||
|
||||
t.Run("workflow with bind operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
|
||||
ctx := TestContext{Value: "test"}
|
||||
|
||||
eff := Bind(
|
||||
func(y int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Y = y
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s State) Effect[TestContext, int] {
|
||||
return Of[TestContext](s.X * 2)
|
||||
},
|
||||
)(BindTo[TestContext](func(x int) State {
|
||||
return State{X: x}
|
||||
})(Of[TestContext](10)))
|
||||
|
||||
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 10, result.X)
|
||||
assert.Equal(t, 20, result.Y)
|
||||
})
|
||||
|
||||
t.Run("workflow with context transformation", func(t *testing.T) {
|
||||
type OuterCtx struct {
|
||||
Value string
|
||||
}
|
||||
type InnerCtx struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
outerCtx := OuterCtx{Value: "outer"}
|
||||
innerEff := Of[InnerCtx]("inner result")
|
||||
|
||||
// Transform context
|
||||
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
|
||||
return InnerCtx{Data: outer.Value + "-transformed"}
|
||||
})(innerEff)
|
||||
|
||||
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "inner result", result)
|
||||
})
|
||||
|
||||
t.Run("workflow with array traversal", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "test"}
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
|
||||
eff := TraverseArray(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})(input)
|
||||
|
||||
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
}
|
||||
7
v2/effect/traverse.go
Normal file
7
v2/effect/traverse.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package effect
|
||||
|
||||
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
|
||||
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
|
||||
return readerreaderioresult.TraverseArray(f)
|
||||
}
|
||||
266
v2/effect/traverse_test.go
Normal file
266
v2/effect/traverse_test.go
Normal file
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTraverseArray(t *testing.T) {
|
||||
t.Run("traverses empty array", func(t *testing.T) {
|
||||
input := []int{}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("traverses array with single element", func(t *testing.T) {
|
||||
input := []int{42}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"42"}, result)
|
||||
})
|
||||
|
||||
t.Run("traverses array with multiple elements", func(t *testing.T) {
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, result)
|
||||
})
|
||||
|
||||
t.Run("transforms to different type", func(t *testing.T) {
|
||||
input := []string{"hello", "world", "test"}
|
||||
kleisli := TraverseArray(func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext](len(s))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{5, 5, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("stops on first error", func(t *testing.T) {
|
||||
expectedErr := errors.New("traverse error")
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
if x == 3 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("handles complex transformations", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
input := []int{1, 2, 3}
|
||||
kleisli := TraverseArray(func(id int) Effect[TestContext, User] {
|
||||
return Of[TestContext](User{
|
||||
ID: id,
|
||||
Name: fmt.Sprintf("User%d", id),
|
||||
})
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, 1, result[0].ID)
|
||||
assert.Equal(t, "User1", result[0].Name)
|
||||
assert.Equal(t, 2, result[1].ID)
|
||||
assert.Equal(t, "User2", result[1].Name)
|
||||
assert.Equal(t, 3, result[2].ID)
|
||||
assert.Equal(t, "User3", result[2].Name)
|
||||
})
|
||||
|
||||
t.Run("chains with other operations", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
eff := Chain(func(strings []string) Effect[TestContext, int] {
|
||||
total := 0
|
||||
for _, s := range strings {
|
||||
val, _ := strconv.Atoi(s)
|
||||
total += val
|
||||
}
|
||||
return Of[TestContext](total)
|
||||
})(TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x * 2))
|
||||
})(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 12, result) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
|
||||
})
|
||||
|
||||
t.Run("uses context in transformation", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Chain(func(ctx TestContext) Effect[TestContext, string] {
|
||||
return Of[TestContext](fmt.Sprintf("%s-%d", ctx.Value, x))
|
||||
})(Of[TestContext](TestContext{Value: "prefix"}))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"prefix-1", "prefix-2", "prefix-3"}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
input := []int{5, 3, 8, 1, 9, 2}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 10)
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{50, 30, 80, 10, 90, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("handles large arrays", func(t *testing.T) {
|
||||
size := 1000
|
||||
input := make([]int, size)
|
||||
for i := 0; i < size; i++ {
|
||||
input[i] = i
|
||||
}
|
||||
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
|
||||
return Of[TestContext](x * 2)
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, size)
|
||||
assert.Equal(t, 0, result[0])
|
||||
assert.Equal(t, 1998, result[999])
|
||||
})
|
||||
|
||||
t.Run("composes multiple traversals", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
// First traversal: int -> string
|
||||
kleisli1 := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
// Second traversal: string -> int (length)
|
||||
kleisli2 := TraverseArray(func(s string) Effect[TestContext, int] {
|
||||
return Of[TestContext](len(s))
|
||||
})
|
||||
|
||||
eff := Chain(kleisli2)(kleisli1(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 1, 1}, result) // All single-digit numbers have length 1
|
||||
})
|
||||
|
||||
t.Run("handles nil array", func(t *testing.T) {
|
||||
var input []int
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, result) // TraverseArray returns empty slice for nil input
|
||||
})
|
||||
|
||||
t.Run("works with Map for post-processing", func(t *testing.T) {
|
||||
input := []int{1, 2, 3}
|
||||
|
||||
eff := Map[TestContext](func(strings []string) string {
|
||||
result := ""
|
||||
for _, s := range strings {
|
||||
result += s + ","
|
||||
}
|
||||
return result
|
||||
})(TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})(input))
|
||||
|
||||
result, err := runEffect(eff, TestContext{Value: "test"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "1,2,3,", result)
|
||||
})
|
||||
|
||||
t.Run("error in middle of array", func(t *testing.T) {
|
||||
expectedErr := errors.New("middle error")
|
||||
input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
if x == 5 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("error at end of array", func(t *testing.T) {
|
||||
expectedErr := errors.New("end error")
|
||||
input := []int{1, 2, 3, 4, 5}
|
||||
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
|
||||
if x == 5 {
|
||||
return Fail[TestContext, string](expectedErr)
|
||||
}
|
||||
return Of[TestContext](strconv.Itoa(x))
|
||||
})
|
||||
|
||||
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
37
v2/effect/types.go
Normal file
37
v2/effect/types.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
IO[A any] = io.IO[A]
|
||||
IOEither[E, A any] = ioeither.IOEither[E, A]
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
|
||||
Thunk[A any] = ReaderIOResult[A]
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
Result[A any] = result.Result[A]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
|
||||
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
|
||||
)
|
||||
@@ -188,6 +188,81 @@ func MonadChain[E, A, B any](fa Either[E, A], f Kleisli[E, A, B]) Either[E, B] {
|
||||
return f(fa.r)
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the Left (error) value, allowing error recovery or transformation.
|
||||
// If the Either is Left, applies the provided function to the error value, which returns a new Either.
|
||||
// If the Either is Right, returns the Right value unchanged with the new error type.
|
||||
//
|
||||
// This is the dual of [MonadChain] - while MonadChain operates on Right values (success),
|
||||
// MonadChainLeft operates on Left values (errors). It's useful for error recovery, error transformation,
|
||||
// or chaining alternative computations when an error occurs.
|
||||
//
|
||||
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// The error type can be transformed from EA to EB, allowing flexible error type conversions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Error recovery: convert specific errors to success
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Left[int](errors.New("not found")),
|
||||
// func(err error) either.Either[string, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[string](0) // default value
|
||||
// }
|
||||
// return either.Left[int](err.Error()) // transform error
|
||||
// },
|
||||
// ) // Right(0)
|
||||
//
|
||||
// // Error transformation: change error type
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Left[int](404),
|
||||
// func(code int) either.Either[string, int] {
|
||||
// return either.Left[int](fmt.Sprintf("Error code: %d", code))
|
||||
// },
|
||||
// ) // Left("Error code: 404")
|
||||
//
|
||||
// // Right values pass through unchanged
|
||||
// result := either.MonadChainLeft(
|
||||
// either.Right[error](42),
|
||||
// func(err error) either.Either[string, int] {
|
||||
// return either.Left[int]("error")
|
||||
// },
|
||||
// ) // Right(42)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[EA, EB, A any](fa Either[EA, A], f Kleisli[EB, EA, A]) Either[EB, A] {
|
||||
return MonadFold(fa, f, Of[EB])
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// Returns a function that sequences a computation on the Left (error) value.
|
||||
//
|
||||
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for creating reusable error handlers or transformers that can be
|
||||
// composed with other Either operations using pipes or function composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a reusable error handler
|
||||
// handleNotFound := either.ChainLeft[error, string](func(err error) either.Either[string, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[string](0)
|
||||
// }
|
||||
// return either.Left[int](err.Error())
|
||||
// })
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// result := F.Pipe1(
|
||||
// either.Left[int](errors.New("not found")),
|
||||
// handleNotFound,
|
||||
// ) // Right(0)
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[EA, EB, A any](f Kleisli[EB, EA, A]) Kleisli[EB, Either[EA, A], A] {
|
||||
return Fold(f, Of[EB])
|
||||
}
|
||||
|
||||
// MonadChainFirst executes a side-effect computation but returns the original value.
|
||||
// Useful for performing actions (like logging) without changing the value.
|
||||
//
|
||||
@@ -471,6 +546,8 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
// If the Either is Left, it applies the provided function to the error value,
|
||||
// which returns a new Either that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
@@ -504,7 +581,7 @@ func ToType[A, E any](onError func(any) E) func(any) Either[E, A] {
|
||||
return func(value any) Either[E, A] {
|
||||
return F.Pipe2(
|
||||
value,
|
||||
O.ToType[A],
|
||||
O.InstanceOf[A],
|
||||
O.Fold(F.Nullary3(F.Constant(value), onError, Left[A, E]), Right[E, A]),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,79 +124,52 @@ func TestStringer(t *testing.T) {
|
||||
func TestZeroWithIntegers(t *testing.T) {
|
||||
e := Zero[error, int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0, value, "Right value should be zero for int")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](0), e, "Zero should create a Right value with zero for int")
|
||||
}
|
||||
|
||||
// TestZeroWithStrings tests Zero function with string types
|
||||
func TestZeroWithStrings(t *testing.T) {
|
||||
e := Zero[error, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](""), e, "Zero should create a Right value with empty string")
|
||||
}
|
||||
|
||||
// TestZeroWithBooleans tests Zero function with boolean types
|
||||
func TestZeroWithBooleans(t *testing.T) {
|
||||
e := Zero[error, bool]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, false, value, "Right value should be false for bool")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](false), e, "Zero should create a Right value with false for bool")
|
||||
}
|
||||
|
||||
// TestZeroWithFloats tests Zero function with float types
|
||||
func TestZeroWithFloats(t *testing.T) {
|
||||
e := Zero[error, float64]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](0.0), e, "Zero should create a Right value with 0.0 for float64")
|
||||
}
|
||||
|
||||
// TestZeroWithPointers tests Zero function with pointer types
|
||||
func TestZeroWithPointers(t *testing.T) {
|
||||
e := Zero[error, *int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for pointer type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilPtr *int
|
||||
assert.Equal(t, Of[error](nilPtr), e, "Zero should create a Right value with nil pointer")
|
||||
}
|
||||
|
||||
// TestZeroWithSlices tests Zero function with slice types
|
||||
func TestZeroWithSlices(t *testing.T) {
|
||||
e := Zero[error, []int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for slice type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilSlice []int
|
||||
assert.Equal(t, Of[error](nilSlice), e, "Zero should create a Right value with nil slice")
|
||||
}
|
||||
|
||||
// TestZeroWithMaps tests Zero function with map types
|
||||
func TestZeroWithMaps(t *testing.T) {
|
||||
e := Zero[error, map[string]int]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for map type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilMap map[string]int
|
||||
assert.Equal(t, Of[error](nilMap), e, "Zero should create a Right value with nil map")
|
||||
}
|
||||
|
||||
// TestZeroWithStructs tests Zero function with struct types
|
||||
@@ -208,23 +181,16 @@ func TestZeroWithStructs(t *testing.T) {
|
||||
|
||||
e := Zero[error, TestStruct]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := TestStruct{Field1: 0, Field2: ""}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for struct")
|
||||
}
|
||||
|
||||
// TestZeroWithInterfaces tests Zero function with interface types
|
||||
func TestZeroWithInterfaces(t *testing.T) {
|
||||
e := Zero[error, interface{}]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Nil(t, value, "Right value should be nil for interface type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
var nilInterface interface{}
|
||||
assert.Equal(t, Of[error](nilInterface), e, "Zero should create a Right value with nil interface")
|
||||
}
|
||||
|
||||
// TestZeroWithCustomErrorType tests Zero function with custom error types
|
||||
@@ -236,12 +202,7 @@ func TestZeroWithCustomErrorType(t *testing.T) {
|
||||
|
||||
e := Zero[CustomError, string]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
assert.False(t, IsLeft(e), "Zero should not create a Left value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.Equal(t, "", value, "Right value should be empty string")
|
||||
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
|
||||
assert.Equal(t, Of[CustomError](""), e, "Zero should create a Right value with empty string")
|
||||
}
|
||||
|
||||
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
|
||||
@@ -252,17 +213,13 @@ func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
|
||||
mapped := MonadMap(e, func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
})
|
||||
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
|
||||
value, _ := Unwrap(mapped)
|
||||
assert.Equal(t, "0", value, "Mapped value should be '0'")
|
||||
assert.Equal(t, Of[error]("0"), mapped, "Mapped Zero should be Right with '0'")
|
||||
|
||||
// Test with Chain
|
||||
chained := MonadChain(e, func(n int) Either[error, string] {
|
||||
return Right[error](fmt.Sprintf("value: %d", n))
|
||||
})
|
||||
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
|
||||
chainedValue, _ := Unwrap(chained)
|
||||
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
|
||||
assert.Equal(t, Of[error]("value: 0"), chained, "Chained Zero should be Right with 'value: 0'")
|
||||
|
||||
// Test with Fold
|
||||
folded := MonadFold(e,
|
||||
@@ -295,23 +252,15 @@ func TestZeroWithComplexTypes(t *testing.T) {
|
||||
|
||||
e := Zero[error, ComplexType]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
expected := ComplexType{Nested: nil, Ptr: nil}
|
||||
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for complex struct")
|
||||
}
|
||||
|
||||
// TestZeroWithOption tests Zero with Option type
|
||||
func TestZeroWithOption(t *testing.T) {
|
||||
e := Zero[error, O.Option[int]]()
|
||||
|
||||
assert.True(t, IsRight(e), "Zero should create a Right value")
|
||||
|
||||
value, err := Unwrap(e)
|
||||
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
|
||||
assert.Nil(t, err, "Error should be nil for Right value")
|
||||
assert.Equal(t, Of[error](O.None[int]()), e, "Zero should create a Right value with None option")
|
||||
}
|
||||
|
||||
// TestZeroIsNotLeft tests that Zero never creates a Left value
|
||||
@@ -343,3 +292,211 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
|
||||
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
|
||||
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
|
||||
}
|
||||
|
||||
// TestMonadChainLeft tests the MonadChainLeft function with various scenarios
|
||||
func TestMonadChainLeft(t *testing.T) {
|
||||
t.Run("Left value is transformed by function", func(t *testing.T) {
|
||||
// Transform error to success
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("not found")),
|
||||
func(err error) Either[string, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[string](0) // default value
|
||||
}
|
||||
return Left[int](err.Error())
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("Left value error type is transformed", func(t *testing.T) {
|
||||
// Transform error type from int to string
|
||||
result := MonadChainLeft(
|
||||
Left[int](404),
|
||||
func(code int) Either[string, int] {
|
||||
return Left[int](fmt.Sprintf("Error code: %d", code))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[int]("Error code: 404"), result)
|
||||
})
|
||||
|
||||
t.Run("Right value passes through unchanged", func(t *testing.T) {
|
||||
// Right value should not be affected
|
||||
result := MonadChainLeft(
|
||||
Right[error](42),
|
||||
func(err error) Either[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("Chain multiple error transformations", func(t *testing.T) {
|
||||
// First transformation
|
||||
step1 := MonadChainLeft(
|
||||
Left[int](errors.New("error1")),
|
||||
func(err error) Either[error, int] {
|
||||
return Left[int](errors.New("error2"))
|
||||
},
|
||||
)
|
||||
// Second transformation
|
||||
step2 := MonadChainLeft(
|
||||
step1,
|
||||
func(err error) Either[string, int] {
|
||||
return Left[int](err.Error())
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[int]("error2"), step2)
|
||||
})
|
||||
|
||||
t.Run("Error recovery with fallback", func(t *testing.T) {
|
||||
// Recover from specific errors
|
||||
result := MonadChainLeft(
|
||||
Left[int](errors.New("timeout")),
|
||||
func(err error) Either[error, int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right[error](999) // fallback value
|
||||
}
|
||||
return Left[int](err)
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Of[error](999), result)
|
||||
})
|
||||
|
||||
t.Run("Transform error to different Left", func(t *testing.T) {
|
||||
// Transform one error to another
|
||||
result := MonadChainLeft(
|
||||
Left[string]("original error"),
|
||||
func(s string) Either[int, string] {
|
||||
return Left[string](len(s))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, Left[string](14), result) // length of "original error"
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeft tests the curried ChainLeft function
|
||||
func TestChainLeft(t *testing.T) {
|
||||
t.Run("Curried function transforms Left value", func(t *testing.T) {
|
||||
// Create a reusable error handler
|
||||
handleNotFound := ChainLeft[error, string](func(err error) Either[string, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[string](0)
|
||||
}
|
||||
return Left[int](err.Error())
|
||||
})
|
||||
|
||||
result := handleNotFound(Left[int](errors.New("not found")))
|
||||
assert.Equal(t, Of[string](0), result)
|
||||
})
|
||||
|
||||
t.Run("Curried function with Right value", func(t *testing.T) {
|
||||
handler := ChainLeft[error, string](func(err error) Either[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
})
|
||||
|
||||
result := handler(Right[error](42))
|
||||
assert.Equal(t, Of[string](42), result)
|
||||
})
|
||||
|
||||
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
|
||||
// Create error transformer
|
||||
toStringError := ChainLeft[int, string](func(code int) Either[string, string] {
|
||||
return Left[string](fmt.Sprintf("Error: %d", code))
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[string](404),
|
||||
toStringError,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Error: 404"), result)
|
||||
})
|
||||
|
||||
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
|
||||
// First handler: convert error to string
|
||||
handler1 := ChainLeft[error, string](func(err error) Either[string, int] {
|
||||
return Left[int](err.Error())
|
||||
})
|
||||
|
||||
// Second handler: add prefix to string error
|
||||
handler2 := ChainLeft[string, string](func(s string) Either[string, int] {
|
||||
return Left[int]("Handled: " + s)
|
||||
})
|
||||
|
||||
result := F.Pipe2(
|
||||
Left[int](errors.New("original")),
|
||||
handler1,
|
||||
handler2,
|
||||
)
|
||||
assert.Equal(t, Left[int]("Handled: original"), result)
|
||||
})
|
||||
|
||||
t.Run("Error recovery in pipeline", func(t *testing.T) {
|
||||
// Handler that recovers from specific errors
|
||||
recoverFromTimeout := ChainLeft(func(err error) Either[error, int] {
|
||||
if err.Error() == "timeout" {
|
||||
return Right[error](0) // recovered value
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
|
||||
// Test with timeout error
|
||||
result1 := F.Pipe1(
|
||||
Left[int](errors.New("timeout")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.Equal(t, Of[error](0), result1)
|
||||
|
||||
// Test with other error
|
||||
result2 := F.Pipe1(
|
||||
Left[int](errors.New("other error")),
|
||||
recoverFromTimeout,
|
||||
)
|
||||
assert.True(t, IsLeft(result2))
|
||||
})
|
||||
|
||||
t.Run("Transform error type in pipeline", func(t *testing.T) {
|
||||
// Convert numeric error codes to descriptive strings
|
||||
codeToMessage := ChainLeft(func(code int) Either[string, string] {
|
||||
messages := map[int]string{
|
||||
404: "Not Found",
|
||||
500: "Internal Server Error",
|
||||
}
|
||||
if msg, ok := messages[code]; ok {
|
||||
return Left[string](msg)
|
||||
}
|
||||
return Left[string](fmt.Sprintf("Unknown error: %d", code))
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Left[string](404),
|
||||
codeToMessage,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Not Found"), result)
|
||||
})
|
||||
|
||||
t.Run("ChainLeft with Map combination", func(t *testing.T) {
|
||||
// Combine ChainLeft with Map to handle both channels
|
||||
errorHandler := ChainLeft(func(err error) Either[string, int] {
|
||||
return Left[int]("Error: " + err.Error())
|
||||
})
|
||||
|
||||
valueMapper := Map[string](S.Format[int]("Value: %d"))
|
||||
|
||||
// Test with Left
|
||||
result1 := F.Pipe2(
|
||||
Left[int](errors.New("fail")),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.Equal(t, Left[string]("Error: fail"), result1)
|
||||
|
||||
// Test with Right
|
||||
result2 := F.Pipe2(
|
||||
Right[error](42),
|
||||
errorHandler,
|
||||
valueMapper,
|
||||
)
|
||||
assert.Equal(t, Of[string]("Value: 42"), result2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,7 +82,30 @@ func Bind[S1, S2, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// Similar to Bind, but uses the Functor's Map operation instead of the Monad's Chain.
|
||||
// This is useful when you want to add a computed value to the context without needing
|
||||
// the full power of monadic composition.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// Sum int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{X: 10, Y: 20}),
|
||||
// identity.Let(
|
||||
// func(sum int) func(State) State {
|
||||
// return func(s State) State { s.Sum = sum; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// return s.X + s.Y
|
||||
// },
|
||||
// ),
|
||||
// ) // State{X: 10, Y: 20, Sum: 30}
|
||||
func Let[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -94,7 +117,27 @@ func Let[S1, S2, T any](
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
|
||||
// This is a specialized version of Let that doesn't require a computation function,
|
||||
// useful when you want to add a known value to the context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// Constant string
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// identity.Do(State{X: 10, Y: 20}),
|
||||
// identity.LetTo(
|
||||
// func(c string) func(State) State {
|
||||
// return func(s State) State { s.Constant = c; return s }
|
||||
// },
|
||||
// "fixed value",
|
||||
// ),
|
||||
// ) // State{X: 10, Y: 20, Constant: "fixed value"}
|
||||
func LetTo[S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
@@ -106,7 +149,31 @@ func LetTo[S1, S2, B any](
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
// BindTo initializes a new state [S1] from a value [T].
|
||||
// This is typically used as the first operation in a do-notation chain to convert
|
||||
// a plain value into a context that can be used with subsequent Bind operations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// 42,
|
||||
// identity.BindTo(func(x int) State {
|
||||
// return State{X: x}
|
||||
// }),
|
||||
// identity.Bind(
|
||||
// func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// },
|
||||
// func(s State) int {
|
||||
// return s.X * 2
|
||||
// },
|
||||
// ),
|
||||
// ) // State{X: 42, Y: 84}
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) func(T) S1 {
|
||||
|
||||
@@ -29,7 +29,7 @@ func ExampleIOResult_do() {
|
||||
bar := Of(1)
|
||||
|
||||
// quux consumes the state of three bindings and returns an [IO] instead of an [IOResult]
|
||||
quux := func(t T.Tuple3[string, int, string]) IO[any] {
|
||||
quux := func(t T.Tuple3[string, int, string]) IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
ioresult.Map(func(r *http.Request) *url.URL {
|
||||
return r.URL
|
||||
}),
|
||||
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[any] {
|
||||
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
q := u.Query()
|
||||
assert.Equal(t, "10", q.Get("limit"))
|
||||
|
||||
@@ -15,8 +15,12 @@
|
||||
|
||||
package builder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
|
||||
)
|
||||
|
||||
type (
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ func MakeClient(httpClient *http.Client) Client {
|
||||
}
|
||||
|
||||
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
|
||||
func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
|
||||
func ReadFullResponse(client Client) Operator[*http.Request, H.FullResponse] {
|
||||
return F.Flow3(
|
||||
client.Do,
|
||||
ioresult.ChainEitherK(H.ValidateResponse),
|
||||
@@ -101,7 +101,7 @@ func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as bytes
|
||||
func ReadAll(client Client) Kleisli[Requester, []byte] {
|
||||
func ReadAll(client Client) Operator[*http.Request, []byte] {
|
||||
return F.Flow2(
|
||||
ReadFullResponse(client),
|
||||
ioresult.Map(H.Body),
|
||||
@@ -109,7 +109,7 @@ func ReadAll(client Client) Kleisli[Requester, []byte] {
|
||||
}
|
||||
|
||||
// ReadText sends a request, reads the response and represents the response as a text string
|
||||
func ReadText(client Client) Kleisli[Requester, string] {
|
||||
func ReadText(client Client) Operator[*http.Request, string] {
|
||||
return F.Flow2(
|
||||
ReadAll(client),
|
||||
ioresult.Map(B.ToString),
|
||||
@@ -117,7 +117,7 @@ func ReadText(client Client) Kleisli[Requester, string] {
|
||||
}
|
||||
|
||||
// readJSON sends a request, reads the response and parses the response as a []byte
|
||||
func readJSON(client Client) Kleisli[Requester, []byte] {
|
||||
func readJSON(client Client) Operator[*http.Request, []byte] {
|
||||
return F.Flow3(
|
||||
ReadFullResponse(client),
|
||||
ioresult.ChainFirstEitherK(F.Flow2(
|
||||
@@ -129,7 +129,7 @@ func readJSON(client Client) Kleisli[Requester, []byte] {
|
||||
}
|
||||
|
||||
// ReadJSON sends a request, reads the response and parses the response as JSON
|
||||
func ReadJSON[A any](client Client) Kleisli[Requester, A] {
|
||||
func ReadJSON[A any](client Client) Operator[*http.Request, A] {
|
||||
return F.Flow2(
|
||||
readJSON(client),
|
||||
ioresult.ChainEitherK(J.Unmarshal[A]),
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
Requester = IOResult[*http.Request]
|
||||
IOResult[A any] = ioresult.IOResult[A]
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
Operator[A, B any] = ioresult.Operator[A, B]
|
||||
Requester = IOResult[*http.Request]
|
||||
|
||||
Client interface {
|
||||
Do(Requester) IOResult[*http.Response]
|
||||
|
||||
@@ -664,11 +664,11 @@ func WithResource[A, R, ANY any](
|
||||
|
||||
// FromImpure converts an impure side-effecting function into an IOResult.
|
||||
// The function is executed when the IOResult runs, and always succeeds with nil.
|
||||
func FromImpure(f func()) IOResult[any] {
|
||||
func FromImpure(f func()) IOResult[Void] {
|
||||
return function.Pipe2(
|
||||
f,
|
||||
io.FromImpure,
|
||||
FromIO[any],
|
||||
FromIO[Void],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
@@ -39,4 +40,6 @@ type (
|
||||
Operator[A, B any] = Kleisli[IOResult[A], B]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -19,6 +19,31 @@ import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// MonadSequenceSegment sequences a segment of an array of effects using a divide-and-conquer approach.
|
||||
// It recursively splits the array segment in half, sequences each half, and concatenates the results.
|
||||
//
|
||||
// This function is optimized for performance by using a divide-and-conquer strategy that reduces
|
||||
// the depth of nested function calls compared to a linear fold approach.
|
||||
//
|
||||
// Type parameters:
|
||||
// - HKTB: The higher-kinded type containing values (e.g., Option[B], Either[E, B])
|
||||
// - HKTRB: The higher-kinded type containing an array of values (e.g., Option[[]B], Either[E, []B])
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a single HKTB into HKTRB
|
||||
// - empty: The empty/identity value for HKTRB
|
||||
// - concat: Function to concatenate two HKTRB values
|
||||
// - fbs: The array of effects to sequence
|
||||
// - start: The starting index of the segment (inclusive)
|
||||
// - end: The ending index of the segment (exclusive)
|
||||
//
|
||||
// Returns:
|
||||
// - HKTRB: The sequenced result for the segment
|
||||
//
|
||||
// The function handles three cases:
|
||||
// - Empty segment (end - start == 0): returns empty
|
||||
// - Single element (end - start == 1): returns fof(fbs[start])
|
||||
// - Multiple elements: recursively divides and conquers
|
||||
func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
empty HKTRB,
|
||||
@@ -41,6 +66,23 @@ func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// SequenceSegment creates a function that sequences a segment of an array of effects.
|
||||
// Unlike MonadSequenceSegment, this returns a curried function that can be reused.
|
||||
//
|
||||
// This function builds a computation tree at construction time, which can be more efficient
|
||||
// when the same sequencing pattern needs to be applied multiple times to arrays of the same length.
|
||||
//
|
||||
// Type parameters:
|
||||
// - HKTB: The higher-kinded type containing values
|
||||
// - HKTRB: The higher-kinded type containing an array of values
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a single HKTB into HKTRB
|
||||
// - empty: The empty/identity value for HKTRB
|
||||
// - concat: Function to concatenate two HKTRB values
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array of HKTB and returns HKTRB
|
||||
func SequenceSegment[HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
empty HKTRB,
|
||||
@@ -85,14 +127,39 @@ func SequenceSegment[HKTB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverse maps each element of an array to an effect, then sequences the results.
|
||||
// This is the monadic version that takes the array as a direct parameter.
|
||||
//
|
||||
// Traverse combines mapping and sequencing in one operation. It's useful when you want to
|
||||
// transform each element of an array into an effect (like Option, Either, IO, etc.) and
|
||||
// then collect all those effects into a single effect containing an array.
|
||||
//
|
||||
// We need to pass the members of the applicative explicitly, because golang does neither
|
||||
// support higher kinded types nor template methods on structs or interfaces.
|
||||
//
|
||||
// Type parameters:
|
||||
// - GA: The input array type (e.g., []A)
|
||||
// - GB: The output array type (e.g., []B)
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
// - HKTB: HKT<B> - The effect containing B (e.g., Option[B])
|
||||
// - HKTAB: HKT<func(B)GB> - Intermediate applicative type
|
||||
// - HKTRB: HKT<GB> - The effect containing the result array (e.g., Option[[]B])
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - ta: The input array to traverse
|
||||
// - f: The function to apply to each element, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - HKTRB: An effect containing the array of transformed values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// If any element produces None, the entire result is None.
|
||||
// If all elements produce Some, the result is Some containing all values.
|
||||
func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -103,14 +170,20 @@ func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
return MonadTraverseReduce(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverseWithIndex is like MonadTraverse but the transformation function also receives the index.
|
||||
// This is useful when the transformation depends on the element's position in the array.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverse
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - ta: The input array to traverse
|
||||
// - f: The function to apply to each element with its index, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - HKTRB: An effect containing the array of transformed values
|
||||
func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -121,6 +194,19 @@ func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
|
||||
}
|
||||
|
||||
// Traverse creates a curried function that maps each element to an effect and sequences the results.
|
||||
// This is the curried version of MonadTraverse, useful for partial application and composition.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverse
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - f: The function to apply to each element, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns an effect containing the transformed array
|
||||
func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -133,6 +219,19 @@ func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseWithIndex creates a curried function like Traverse but with index-aware transformation.
|
||||
// This is the curried version of MonadTraverseWithIndex.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverse
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift a value into the effect (Of/Pure)
|
||||
// - fmap: Function to map over the effect (Map)
|
||||
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
|
||||
// - f: The function to apply to each element with its index, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns an effect containing the transformed array
|
||||
func TraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -231,6 +330,16 @@ func TraverseReduce[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseReduceWithIndex creates a curried function for index-aware custom reduction during traversal.
|
||||
// This is the curried version of MonadTraverseReduceWithIndex.
|
||||
//
|
||||
// Type parameters: Same as MonadTraverseReduce
|
||||
//
|
||||
// Parameters: Same as TraverseReduce, except:
|
||||
// - transform: Function that takes index and element, producing an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an array and returns an effect containing the accumulated value
|
||||
func TraverseReduceWithIndex[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
|
||||
@@ -1,10 +1,60 @@
|
||||
// Copyright (c) 2024 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package iter provides functional programming utilities for working with Go 1.23+ iterators.
|
||||
// It offers operations for reducing, mapping, concatenating, and transforming iterator sequences
|
||||
// in a functional style, compatible with the range-over-func pattern.
|
||||
package iter
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
func From[A any](as ...A) Seq[A] {
|
||||
return slices.Values(as)
|
||||
}
|
||||
|
||||
// MonadReduceWithIndex reduces an iterator sequence to a single value using a reducer function
|
||||
// that receives the current index, accumulated value, and current element.
|
||||
//
|
||||
// The function iterates through all elements in the sequence, applying the reducer function
|
||||
// at each step with the element's index. This is useful when the position of elements matters
|
||||
// in the reduction logic.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to reduce
|
||||
// - f: The reducer function that takes (index, accumulator, element) and returns the new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - The final accumulated value after processing all elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(10)
|
||||
// yield(20)
|
||||
// yield(30)
|
||||
// }
|
||||
// // Sum with index multiplier: 0*10 + 1*20 + 2*30 = 80
|
||||
// result := MonadReduceWithIndex(iter, func(i, acc, val int) int {
|
||||
// return acc + i*val
|
||||
// }, 0)
|
||||
func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(int, B, A) B, initial B) B {
|
||||
current := initial
|
||||
var i int
|
||||
@@ -15,6 +65,29 @@ func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(
|
||||
return current
|
||||
}
|
||||
|
||||
// MonadReduce reduces an iterator sequence to a single value using a reducer function.
|
||||
//
|
||||
// This is similar to MonadReduceWithIndex but without index tracking, making it more
|
||||
// efficient when the position of elements is not needed in the reduction logic.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to reduce
|
||||
// - f: The reducer function that takes (accumulator, element) and returns the new accumulator
|
||||
// - initial: The initial value for the accumulator
|
||||
//
|
||||
// Returns:
|
||||
// - The final accumulated value after processing all elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// yield(3)
|
||||
// }
|
||||
// sum := MonadReduce(iter, func(acc, val int) int {
|
||||
// return acc + val
|
||||
// }, 0) // Returns: 6
|
||||
func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B, initial B) B {
|
||||
current := initial
|
||||
for a := range fa {
|
||||
@@ -23,7 +96,30 @@ func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B,
|
||||
return current
|
||||
}
|
||||
|
||||
// Concat concatenates two sequences, yielding all elements from left followed by all elements from right.
|
||||
// Concat concatenates two iterator sequences, yielding all elements from left followed by all elements from right.
|
||||
//
|
||||
// The resulting iterator will first yield all elements from the left sequence, then all elements
|
||||
// from the right sequence. If the consumer stops early (yield returns false), iteration stops
|
||||
// immediately without processing remaining elements.
|
||||
//
|
||||
// Parameters:
|
||||
// - left: The first iterator sequence
|
||||
// - right: The second iterator sequence
|
||||
//
|
||||
// Returns:
|
||||
// - A new iterator that yields elements from both sequences in order
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// left := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// right := func(yield func(int) bool) {
|
||||
// yield(3)
|
||||
// yield(4)
|
||||
// }
|
||||
// combined := Concat(left, right) // Yields: 1, 2, 3, 4
|
||||
func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
|
||||
return func(yield func(T) bool) {
|
||||
for t := range left {
|
||||
@@ -39,28 +135,129 @@ func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
|
||||
}
|
||||
}
|
||||
|
||||
// Of creates an iterator sequence containing a single element.
|
||||
//
|
||||
// This is the unit/return operation for the iterator monad, lifting a single value
|
||||
// into the iterator context.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The element to wrap in an iterator
|
||||
//
|
||||
// Returns:
|
||||
// - An iterator that yields exactly one element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := Of[func(yield func(int) bool)](42)
|
||||
// // Yields: 42
|
||||
func Of[GA ~func(yield func(A) bool), A any](a A) GA {
|
||||
return func(yield func(A) bool) {
|
||||
yield(a)
|
||||
}
|
||||
}
|
||||
|
||||
// MonadAppend appends a single element to the end of an iterator sequence.
|
||||
//
|
||||
// This creates a new iterator that yields all elements from the original sequence
|
||||
// followed by the tail element.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The original iterator sequence
|
||||
// - tail: The element to append
|
||||
//
|
||||
// Returns:
|
||||
// - A new iterator with the tail element appended
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := MonadAppend(iter, 3) // Yields: 1, 2, 3
|
||||
func MonadAppend[GA ~func(yield func(A) bool), A any](f GA, tail A) GA {
|
||||
return Concat(f, Of[GA](tail))
|
||||
}
|
||||
|
||||
// Append returns a function that appends a single element to the end of an iterator sequence.
|
||||
//
|
||||
// This is the curried version of MonadAppend, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - tail: The element to append
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a new iterator with the tail element appended
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// appendThree := Append[func(yield func(int) bool)](3)
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := appendThree(iter) // Yields: 1, 2, 3
|
||||
func Append[GA ~func(yield func(A) bool), A any](tail A) func(GA) GA {
|
||||
return F.Bind2nd(Concat[GA], Of[GA](tail))
|
||||
}
|
||||
|
||||
// Prepend returns a function that prepends a single element to the beginning of an iterator sequence.
|
||||
//
|
||||
// This is the curried version for prepending, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - head: The element to prepend
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a new iterator with the head element prepended
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prependZero := Prepend[func(yield func(int) bool)](0)
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := prependZero(iter) // Yields: 0, 1, 2
|
||||
func Prepend[GA ~func(yield func(A) bool), A any](head A) func(GA) GA {
|
||||
return F.Bind1st(Concat[GA], Of[GA](head))
|
||||
}
|
||||
|
||||
// Empty creates an empty iterator sequence that yields no elements.
|
||||
//
|
||||
// This is the identity element for the Concat operation and represents an empty collection
|
||||
// in the iterator context.
|
||||
//
|
||||
// Returns:
|
||||
// - An iterator that yields no elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := Empty[func(yield func(int) bool), int]()
|
||||
// // Yields nothing
|
||||
func Empty[GA ~func(yield func(A) bool), A any]() GA {
|
||||
return func(_ func(A) bool) {}
|
||||
}
|
||||
|
||||
// ToArray collects all elements from an iterator sequence into a slice.
|
||||
//
|
||||
// This eagerly evaluates the entire iterator sequence and materializes all elements
|
||||
// into memory as a slice.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to collect
|
||||
//
|
||||
// Returns:
|
||||
// - A slice containing all elements from the iterator
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// yield(3)
|
||||
// }
|
||||
// arr := ToArray[func(yield func(int) bool), []int](iter) // Returns: []int{1, 2, 3}
|
||||
func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
|
||||
bs := make(GB, 0)
|
||||
for a := range fa {
|
||||
@@ -69,6 +266,28 @@ func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
|
||||
return bs
|
||||
}
|
||||
|
||||
// MonadMapToArray maps each element of an iterator sequence through a function and collects the results into a slice.
|
||||
//
|
||||
// This combines mapping and collection into a single operation, eagerly evaluating the entire
|
||||
// iterator sequence and materializing the transformed elements into memory.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to map and collect
|
||||
// - f: The mapping function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - A slice containing the mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// yield(3)
|
||||
// }
|
||||
// doubled := MonadMapToArray[func(yield func(int) bool), []int](iter, func(x int) int {
|
||||
// return x * 2
|
||||
// }) // Returns: []int{2, 4, 6}
|
||||
func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(A) B) GB {
|
||||
bs := make(GB, 0)
|
||||
for a := range fa {
|
||||
@@ -77,10 +296,54 @@ func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f f
|
||||
return bs
|
||||
}
|
||||
|
||||
// MapToArray returns a function that maps each element through a function and collects the results into a slice.
|
||||
//
|
||||
// This is the curried version of MonadMapToArray, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The mapping function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a slice of mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// double := MapToArray[func(yield func(int) bool), []int](func(x int) int {
|
||||
// return x * 2
|
||||
// })
|
||||
// iter := func(yield func(int) bool) {
|
||||
// yield(1)
|
||||
// yield(2)
|
||||
// }
|
||||
// result := double(iter) // Returns: []int{2, 4}
|
||||
func MapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(A) B) func(GA) GB {
|
||||
return F.Bind2nd(MonadMapToArray[GA, GB], f)
|
||||
}
|
||||
|
||||
// MonadMapToArrayWithIndex maps each element of an iterator sequence through a function that receives
|
||||
// the element's index, and collects the results into a slice.
|
||||
//
|
||||
// This is similar to MonadMapToArray but the mapping function also receives the zero-based index
|
||||
// of each element, useful when the position matters in the transformation logic.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The iterator sequence to map and collect
|
||||
// - f: The mapping function that takes (index, element) and returns the transformed element
|
||||
//
|
||||
// Returns:
|
||||
// - A slice containing the mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("a")
|
||||
// yield("b")
|
||||
// yield("c")
|
||||
// }
|
||||
// indexed := MonadMapToArrayWithIndex[func(yield func(string) bool), []string](iter,
|
||||
// func(i int, s string) string {
|
||||
// return fmt.Sprintf("%d:%s", i, s)
|
||||
// }) // Returns: []string{"0:a", "1:b", "2:c"}
|
||||
func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(int, A) B) GB {
|
||||
bs := make(GB, 0)
|
||||
var i int
|
||||
@@ -91,10 +354,49 @@ func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f
|
||||
return bs
|
||||
}
|
||||
|
||||
// MapToArrayWithIndex returns a function that maps each element through an indexed function
|
||||
// and collects the results into a slice.
|
||||
//
|
||||
// This is the curried version of MonadMapToArrayWithIndex, useful for partial application and composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The mapping function that takes (index, element) and returns the transformed element
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns a slice of mapped elements
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// addIndex := MapToArrayWithIndex[func(yield func(string) bool), []string](
|
||||
// func(i int, s string) string {
|
||||
// return fmt.Sprintf("%d:%s", i, s)
|
||||
// })
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("a")
|
||||
// yield("b")
|
||||
// }
|
||||
// result := addIndex(iter) // Returns: []string{"0:a", "1:b"}
|
||||
func MapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
|
||||
return F.Bind2nd(MonadMapToArrayWithIndex[GA, GB], f)
|
||||
}
|
||||
|
||||
// Monoid returns a Monoid instance for iterator sequences.
|
||||
//
|
||||
// The monoid uses Concat as the binary operation and Empty as the identity element,
|
||||
// allowing iterator sequences to be combined in an associative way with a neutral element.
|
||||
// This enables generic operations that work with any monoid, such as folding a collection
|
||||
// of iterators into a single iterator.
|
||||
//
|
||||
// Returns:
|
||||
// - A Monoid instance with Concat and Empty operations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// m := Monoid[func(yield func(int) bool), int]()
|
||||
// iter1 := func(yield func(int) bool) { yield(1); yield(2) }
|
||||
// iter2 := func(yield func(int) bool) { yield(3); yield(4) }
|
||||
// combined := m.Concat(iter1, iter2) // Yields: 1, 2, 3, 4
|
||||
// empty := m.Empty() // Yields nothing
|
||||
func Monoid[GA ~func(yield func(A) bool), A any]() M.Monoid[GA] {
|
||||
return M.MakeMonoid(Concat[GA], Empty[GA]())
|
||||
}
|
||||
|
||||
@@ -21,18 +21,50 @@ import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
)
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverse traverses an iterator sequence, applying an effectful function to each element
|
||||
// and collecting the results in an applicative context.
|
||||
//
|
||||
// This is a fundamental operation in functional programming that allows you to "turn inside out"
|
||||
// a structure containing effects. It maps each element through a function that produces an effect,
|
||||
// then sequences all those effects together while preserving the iterator structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The output iterator type ~func(yield func(B) bool)
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
// - HKT_B: The higher-kinded type representing an effect containing B
|
||||
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
|
||||
// - HKT_GB: The higher-kinded type representing an effect containing GB (the result iterator)
|
||||
//
|
||||
// Parameters:
|
||||
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
|
||||
// - fof_gb: Lifts a GB value into the effect context (pure/of operation)
|
||||
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
|
||||
// - fap_gb: Applies an effectful function to an effectful value (ap operation)
|
||||
// - ta: The input iterator sequence to traverse
|
||||
// - f: The effectful function to apply to each element
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing an iterator of transformed elements
|
||||
//
|
||||
// Note: We need to pass the applicative operations explicitly because Go doesn't support
|
||||
// higher-kinded types or template methods on structs/interfaces.
|
||||
//
|
||||
// Example (conceptual with Option):
|
||||
//
|
||||
// // Traverse an iterator of strings, parsing each as an integer
|
||||
// // If any parse fails, the whole result is None
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("1")
|
||||
// yield("2")
|
||||
// yield("3")
|
||||
// }
|
||||
// result := MonadTraverse(..., iter, parseInt) // Some(iterator of [1,2,3]) or None
|
||||
func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
|
||||
fmap_b func(HKT_B, func(B) GB) HKT_GB,
|
||||
|
||||
fof_gb func(GB) HKT_GB,
|
||||
fof_gb OfType[GB, HKT_GB],
|
||||
fmap_gb func(HKT_GB, func(GB) func(GB) GB) HKT_GB_GB,
|
||||
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
|
||||
|
||||
@@ -54,14 +86,43 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
|
||||
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// Traverse is the curried version of MonadTraverse, returning a function that traverses an iterator.
|
||||
//
|
||||
// This version uses type aliases for better readability and is more suitable for partial application
|
||||
// and function composition. It returns a Kleisli arrow (a function from GA to HKT_GB).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The output iterator type ~func(yield func(B) bool)
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
// - HKT_B: The higher-kinded type representing an effect containing B
|
||||
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
|
||||
// - HKT_GB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
|
||||
// - fof_gb: Lifts a GB value into the effect context
|
||||
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
|
||||
// - fap_gb: Applies an effectful function to an effectful value
|
||||
// - f: The effectful function to apply to each element (Kleisli arrow)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// parseInts := Traverse[...](fmap, fof, fmap_gb, fap, parseInt)
|
||||
// iter := func(yield func(string) bool) { yield("1"); yield("2") }
|
||||
// result := parseInts(iter) // Effect containing iterator of integers
|
||||
func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
|
||||
fmap_b func(func(B) GB) func(HKT_B) HKT_GB,
|
||||
fmap_b MapType[B, GB, HKT_B, HKT_GB],
|
||||
|
||||
fof_gb func(GB) HKT_GB,
|
||||
fmap_gb func(func(GB) func(GB) GB) func(HKT_GB) HKT_GB_GB,
|
||||
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
|
||||
fof_gb OfType[GB, HKT_GB],
|
||||
fmap_gb MapType[GB, Endomorphism[GB], HKT_GB, HKT_GB_GB],
|
||||
fap_gb ApType[HKT_GB, HKT_GB, HKT_GB_GB],
|
||||
|
||||
f func(A) HKT_B) func(GA) HKT_GB {
|
||||
f Kleisli[A, HKT_B]) Kleisli[GA, HKT_GB] {
|
||||
|
||||
fof := fmap_b(Of[GB])
|
||||
empty := fof_gb(Empty[GB]())
|
||||
@@ -69,18 +130,50 @@ func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B,
|
||||
concat_gb := fmap_gb(cb)
|
||||
|
||||
concat := func(first, second HKT_GB) HKT_GB {
|
||||
return fap_gb(concat_gb(first), second)
|
||||
return fap_gb(second)(concat_gb(first))
|
||||
}
|
||||
|
||||
return func(ma GA) HKT_GB {
|
||||
// return INTA.SequenceSegment(fof, empty, concat)(MapToArray[GA, []HKT_B](f)(ma))
|
||||
hktb := MonadMapToArray[GA, []HKT_B](ma, f)
|
||||
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
|
||||
}
|
||||
return F.Flow2(
|
||||
MapToArray[GA, []HKT_B](f),
|
||||
INTA.SequenceSegment(fof, empty, concat),
|
||||
)
|
||||
}
|
||||
|
||||
// MonadSequence sequences an iterator of effects into an effect containing an iterator.
|
||||
//
|
||||
// This is a special case of traverse where the transformation function is the identity.
|
||||
// It "flips" the nesting of the iterator and effect types, collecting all effects into
|
||||
// a single effect containing an iterator of values.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(HKTA) bool)
|
||||
// - HKTA: The higher-kinded type representing an effect containing A
|
||||
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTA value into the HKTRA context
|
||||
// - m: A monoid for combining HKTRA values
|
||||
// - ta: The input iterator of effects to sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing an iterator of values
|
||||
//
|
||||
// Example (conceptual with Option):
|
||||
//
|
||||
// iter := func(yield func(Option[int]) bool) {
|
||||
// yield(Some(1))
|
||||
// yield(Some(2))
|
||||
// yield(Some(3))
|
||||
// }
|
||||
// result := MonadSequence(..., iter) // Some(iterator of [1,2,3])
|
||||
//
|
||||
// iter2 := func(yield func(Option[int]) bool) {
|
||||
// yield(Some(1))
|
||||
// yield(None)
|
||||
// }
|
||||
// result2 := MonadSequence(..., iter2) // None
|
||||
func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
fof OfType[HKTA, HKTRA],
|
||||
m M.Monoid[HKTRA],
|
||||
|
||||
ta GA) HKTRA {
|
||||
@@ -90,14 +183,37 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
|
||||
|
||||
HKTRB = HKT<GB>
|
||||
HKTB = HKT<B>
|
||||
HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
|
||||
// function to each element along with its index.
|
||||
//
|
||||
// This is similar to MonadTraverse but the transformation function receives both the element's
|
||||
// zero-based index and the element itself, useful when the position matters in the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - A: The input element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTB value into the HKTRB context
|
||||
// - m: A monoid for combining HKTRB values
|
||||
// - ta: The input iterator sequence to traverse
|
||||
// - f: The effectful function that takes (index, element) and returns an effect
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing an iterator of transformed elements
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// iter := func(yield func(string) bool) {
|
||||
// yield("a")
|
||||
// yield("b")
|
||||
// }
|
||||
// // Add index prefix to each element
|
||||
// result := MonadTraverseWithIndex(..., iter, func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// }) // Effect containing iterator of ["0:a", "1:b"]
|
||||
func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
m M.Monoid[HKTRB],
|
||||
@@ -110,8 +226,29 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(HKTA) bool)
|
||||
// - HKTA: The higher-kinded type representing an effect containing A
|
||||
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTA value into the HKTRA context
|
||||
// - m: A monoid for combining HKTRA values
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator of effects and returns an effect containing an iterator
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// sequenceOptions := Sequence[...](fof, monoid)
|
||||
// iter := func(yield func(Option[int]) bool) { yield(Some(1)); yield(Some(2)) }
|
||||
// result := sequenceOptions(iter) // Some(iterator of [1,2])
|
||||
func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
fof OfType[HKTA, HKTRA],
|
||||
m M.Monoid[HKTRA]) func(GA) HKTRA {
|
||||
|
||||
return func(ma GA) HKTRA {
|
||||
@@ -119,6 +256,32 @@ func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseWithIndex is the curried version of MonadTraverseWithIndex, returning a function that
|
||||
// traverses an iterator with index tracking.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - A: The input element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts an HKTB value into the HKTRB context
|
||||
// - m: A monoid for combining HKTRB values
|
||||
// - f: The effectful function that takes (index, element) and returns an effect
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// addIndexPrefix := TraverseWithIndex[...](fof, monoid, func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// })
|
||||
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
|
||||
// result := addIndexPrefix(iter) // Effect containing iterator of ["0:a", "1:b"]
|
||||
func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
m M.Monoid[HKTRB],
|
||||
@@ -130,6 +293,39 @@ func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
}
|
||||
}
|
||||
|
||||
// MonadTraverseReduce combines traversal with reduction, applying an effectful transformation
|
||||
// and accumulating results using a reducer function.
|
||||
//
|
||||
// This is a more efficient operation when you want to both transform elements through effects
|
||||
// and reduce them to a single accumulated value, avoiding intermediate collections.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - ta: The input iterator sequence to traverse and reduce
|
||||
// - transform: The effectful function to apply to each element
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing the final accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
|
||||
// // Parse strings to ints and sum them
|
||||
// result := MonadTraverseReduce(..., iter, parseInt, add, 0)
|
||||
// // Returns: Some(6) or None if any parse fails
|
||||
func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -152,6 +348,44 @@ func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HK
|
||||
}, fof(initial))
|
||||
}
|
||||
|
||||
// MonadTraverseReduceWithIndex combines indexed traversal with reduction, applying an effectful
|
||||
// transformation that receives element indices and accumulating results using a reducer function.
|
||||
//
|
||||
// This is similar to MonadTraverseReduce but the transformation function also receives the
|
||||
// zero-based index of each element, useful when position matters in the transformation logic.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - ta: The input iterator sequence to traverse and reduce
|
||||
// - transform: The effectful function that takes (index, element) and returns an effect
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - An effect containing the final accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// iter := func(yield func(string) bool) { yield("a"); yield("b"); yield("c") }
|
||||
// // Create indexed strings and concatenate
|
||||
// result := MonadTraverseReduceWithIndex(..., iter,
|
||||
// func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// },
|
||||
// func(acc, s string) string { return acc + "," + s },
|
||||
// "")
|
||||
// // Returns: Effect containing "0:a,1:b,2:c"
|
||||
func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -174,6 +408,36 @@ func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB,
|
||||
}, fof(initial))
|
||||
}
|
||||
|
||||
// TraverseReduce is the curried version of MonadTraverseReduce, returning a function that
|
||||
// traverses and reduces an iterator.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - transform: The effectful function to apply to each element
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing the accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// sumParsedInts := TraverseReduce[...](fof, fmap, fap, parseInt, add, 0)
|
||||
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
|
||||
// result := sumParsedInts(iter) // Some(6) or None if any parse fails
|
||||
func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
@@ -188,6 +452,41 @@ func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB a
|
||||
}
|
||||
}
|
||||
|
||||
// TraverseReduceWithIndex is the curried version of MonadTraverseReduceWithIndex, returning a
|
||||
// function that traverses and reduces an iterator with index tracking.
|
||||
//
|
||||
// This version is more suitable for partial application and function composition.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: The input iterator type ~func(yield func(A) bool)
|
||||
// - GB: The accumulator type
|
||||
// - A: The input element type
|
||||
// - B: The transformed element type
|
||||
// - HKTB: The higher-kinded type representing an effect containing B
|
||||
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
|
||||
// - HKTRB: The higher-kinded type representing an effect containing GB
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a GB value into the effect context
|
||||
// - fmap: Maps a function over the effect to produce an effectful function
|
||||
// - fap: Applies an effectful function to an effectful value
|
||||
// - transform: The effectful function that takes (index, element) and returns an effect
|
||||
// - reduce: The reducer function that combines the accumulator with a transformed element
|
||||
// - initial: The initial accumulator value
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator and returns an effect containing the accumulated value
|
||||
//
|
||||
// Example (conceptual):
|
||||
//
|
||||
// concatIndexed := TraverseReduceWithIndex[...](fof, fmap, fap,
|
||||
// func(i int, s string) Effect[string] {
|
||||
// return Pure(fmt.Sprintf("%d:%s", i, s))
|
||||
// },
|
||||
// func(acc, s string) string { return acc + "," + s },
|
||||
// "")
|
||||
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
|
||||
// result := concatIndexed(iter) // Effect containing "0:a,1:b"
|
||||
func TraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
|
||||
@@ -2,10 +2,23 @@ package iter
|
||||
|
||||
import (
|
||||
I "iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
)
|
||||
|
||||
type (
|
||||
// Seq represents Go's standard library iterator type for single values.
|
||||
// It's an alias for iter.Seq[A] and provides interoperability with Go 1.23+ range-over-func.
|
||||
Seq[A any] = I.Seq[A]
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
OfType[A, HKT_A any] = pointed.OfType[A, HKT_A]
|
||||
MapType[A, B, HKT_A, HKT_B any] = functor.MapType[A, B, HKT_A, HKT_B]
|
||||
ApType[HKT_A, HKT_B, HKT_AB any] = apply.ApType[HKT_A, HKT_B, HKT_AB]
|
||||
|
||||
Kleisli[A, HKT_B any] = func(A) HKT_B
|
||||
)
|
||||
|
||||
@@ -247,7 +247,7 @@ func TestBracket(t *testing.T) {
|
||||
return Of(x * 2)
|
||||
}
|
||||
|
||||
release := func(x int, result int) IO[any] {
|
||||
release := func(x int, result int) IO[Void] {
|
||||
return FromImpure(func() {
|
||||
released = true
|
||||
})
|
||||
@@ -271,7 +271,7 @@ func TestWithResource(t *testing.T) {
|
||||
return 42
|
||||
}
|
||||
|
||||
onRelease := func(x int) IO[any] {
|
||||
onRelease := func(x int) IO[Void] {
|
||||
return FromImpure(func() {
|
||||
released = true
|
||||
})
|
||||
|
||||
12
v2/io/io.go
12
v2/io/io.go
@@ -18,6 +18,7 @@ package io
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
@@ -31,11 +32,6 @@ const (
|
||||
useParallel = true
|
||||
)
|
||||
|
||||
var (
|
||||
// undefined represents an undefined value
|
||||
undefined = struct{}{}
|
||||
)
|
||||
|
||||
// Of wraps a pure value in an IO context, creating a computation that returns that value.
|
||||
// This is the monadic return operation for IO.
|
||||
//
|
||||
@@ -58,10 +54,10 @@ func FromIO[A any](a IO[A]) IO[A] {
|
||||
}
|
||||
|
||||
// FromImpure converts a side effect without a return value into a side effect that returns any
|
||||
func FromImpure[ANY ~func()](f ANY) IO[any] {
|
||||
return func() any {
|
||||
func FromImpure[ANY ~func()](f ANY) IO[Void] {
|
||||
return func() Void {
|
||||
f()
|
||||
return undefined
|
||||
return function.VOID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,18 +61,105 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseIter applies an IO-returning function to each element of an iterator sequence
|
||||
// and collects the results into an IO of an iterator sequence. Executes in parallel by default.
|
||||
//
|
||||
// This function is useful for processing lazy sequences where each element requires an IO operation.
|
||||
// The resulting iterator is also lazy and will only execute IO operations when iterated.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an element of type A and returns an IO computation producing B
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an iterator sequence of A and returns an IO of an iterator sequence of B
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Fetch user data for each ID in a sequence
|
||||
// fetchUser := func(id int) io.IO[User] {
|
||||
// return func() User {
|
||||
// // Simulate fetching user from database
|
||||
// return User{ID: id, Name: fmt.Sprintf("User%d", id)}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Create an iterator of user IDs
|
||||
// userIDs := func(yield func(int) bool) {
|
||||
// for _, id := range []int{1, 2, 3, 4, 5} {
|
||||
// if !yield(id) { return }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Traverse the iterator, fetching each user
|
||||
// fetchUsers := io.TraverseIter(fetchUser)
|
||||
// usersIO := fetchUsers(userIDs)
|
||||
//
|
||||
// // Execute the IO to get the iterator of users
|
||||
// users := usersIO()
|
||||
// for user := range users {
|
||||
// fmt.Printf("User: %v\n", user)
|
||||
// }
|
||||
func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
|
||||
return INTI.Traverse[Seq[A]](
|
||||
Map[B],
|
||||
|
||||
Of[Seq[B]],
|
||||
Map[Seq[B]],
|
||||
MonadAp[Seq[B]],
|
||||
Ap[Seq[B]],
|
||||
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// SequenceIter converts an iterator sequence of IO computations into an IO of an iterator sequence of results.
|
||||
// All computations are executed in parallel by default when the resulting IO is invoked.
|
||||
//
|
||||
// This is a special case of TraverseIter where the transformation function is the identity.
|
||||
// It "flips" the nesting of the iterator and IO types, executing all IO operations and collecting
|
||||
// their results into a lazy iterator.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The element type
|
||||
//
|
||||
// Parameters:
|
||||
// - as: An iterator sequence where each element is an IO computation
|
||||
//
|
||||
// Returns:
|
||||
// - An IO computation that, when executed, produces an iterator sequence of results
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create an iterator of IO operations
|
||||
// operations := func(yield func(io.IO[int]) bool) {
|
||||
// yield(func() int { return 1 })
|
||||
// yield(func() int { return 2 })
|
||||
// yield(func() int { return 3 })
|
||||
// }
|
||||
//
|
||||
// // Sequence the operations
|
||||
// resultsIO := io.SequenceIter(operations)
|
||||
//
|
||||
// // Execute all IO operations and get the iterator of results
|
||||
// results := resultsIO()
|
||||
// for result := range results {
|
||||
// fmt.Printf("Result: %d\n", result)
|
||||
// }
|
||||
//
|
||||
// Note: The IO operations are executed when resultsIO() is called, not when iterating
|
||||
// over the results. The resulting iterator is lazy but the computations have already
|
||||
// been performed.
|
||||
func SequenceIter[A any](as Seq[IO[A]]) IO[Seq[A]] {
|
||||
return INTI.MonadSequence(
|
||||
Map(INTI.Of[Seq[A]]),
|
||||
ApplicativeMonoid(INTI.Monoid[Seq[A]]()),
|
||||
as,
|
||||
)
|
||||
}
|
||||
|
||||
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index.
|
||||
// Executes in parallel by default.
|
||||
//
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package io
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -36,3 +39,265 @@ func TestTraverseCustomSlice(t *testing.T) {
|
||||
|
||||
assert.Equal(t, res(), []string{"A", "B"})
|
||||
}
|
||||
|
||||
func TestTraverseIter(t *testing.T) {
|
||||
t.Run("transforms all elements successfully", func(t *testing.T) {
|
||||
// Create an iterator of strings
|
||||
input := slices.Values(A.From("hello", "world", "test"))
|
||||
|
||||
// Transform each string to uppercase
|
||||
transform := func(s string) IO[string] {
|
||||
return Of(strings.ToUpper(s))
|
||||
}
|
||||
|
||||
// Traverse the iterator
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
// Execute the IO and collect results
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"HELLO", "WORLD", "TEST"}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
// Create an empty iterator
|
||||
input := func(yield func(string) bool) {}
|
||||
|
||||
transform := func(s string) IO[string] {
|
||||
return Of(strings.ToUpper(s))
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
input := func(yield func(int) bool) {
|
||||
yield(42)
|
||||
}
|
||||
|
||||
transform := func(n int) IO[int] {
|
||||
return Of(n * 2)
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []int
|
||||
for n := range result {
|
||||
collected = append(collected, n)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{84}, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves order of elements", func(t *testing.T) {
|
||||
input := func(yield func(int) bool) {
|
||||
for i := 1; i <= 5; i++ {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transform := func(n int) IO[string] {
|
||||
return Of(fmt.Sprintf("item-%d", n))
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
expected := []string{"item-1", "item-2", "item-3", "item-4", "item-5"}
|
||||
assert.Equal(t, expected, collected)
|
||||
})
|
||||
|
||||
t.Run("handles complex transformations", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
input := func(yield func(int) bool) {
|
||||
for _, id := range []int{1, 2, 3} {
|
||||
if !yield(id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transform := func(id int) IO[User] {
|
||||
return Of(User{ID: id, Name: fmt.Sprintf("User%d", id)})
|
||||
}
|
||||
|
||||
traverseFn := TraverseIter(transform)
|
||||
resultIO := traverseFn(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []User
|
||||
for user := range result {
|
||||
collected = append(collected, user)
|
||||
}
|
||||
|
||||
expected := []User{
|
||||
{ID: 1, Name: "User1"},
|
||||
{ID: 2, Name: "User2"},
|
||||
{ID: 3, Name: "User3"},
|
||||
}
|
||||
assert.Equal(t, expected, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSequenceIter(t *testing.T) {
|
||||
t.Run("sequences multiple IO operations", func(t *testing.T) {
|
||||
// Create an iterator of IO operations
|
||||
input := slices.Values(A.From(Of(1), Of(2), Of(3)))
|
||||
|
||||
// Sequence the operations
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
// Execute and collect results
|
||||
result := resultIO()
|
||||
var collected []int
|
||||
for n := range result {
|
||||
collected = append(collected, n)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with empty iterator", func(t *testing.T) {
|
||||
input := slices.Values(A.Empty[IO[string]]())
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
// TODO!!
|
||||
// t.Run("executes all IO operations", func(t *testing.T) {
|
||||
// // Track execution order
|
||||
// var executed []int
|
||||
|
||||
// input := func(yield func(IO[int]) bool) {
|
||||
// yield(func() int {
|
||||
// executed = append(executed, 1)
|
||||
// return 10
|
||||
// })
|
||||
// yield(func() int {
|
||||
// executed = append(executed, 2)
|
||||
// return 20
|
||||
// })
|
||||
// yield(func() int {
|
||||
// executed = append(executed, 3)
|
||||
// return 30
|
||||
// })
|
||||
// }
|
||||
|
||||
// resultIO := SequenceIter(input)
|
||||
|
||||
// // Before execution, nothing should be executed
|
||||
// assert.Empty(t, executed)
|
||||
|
||||
// // Execute the IO
|
||||
// result := resultIO()
|
||||
|
||||
// // Collect results
|
||||
// var collected []int
|
||||
// for n := range result {
|
||||
// collected = append(collected, n)
|
||||
// }
|
||||
|
||||
// // All operations should have been executed
|
||||
// assert.Equal(t, []int{1, 2, 3}, executed)
|
||||
// assert.Equal(t, []int{10, 20, 30}, collected)
|
||||
// })
|
||||
|
||||
t.Run("works with single IO operation", func(t *testing.T) {
|
||||
input := func(yield func(IO[string]) bool) {
|
||||
yield(Of("hello"))
|
||||
}
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []string
|
||||
for s := range result {
|
||||
collected = append(collected, s)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"hello"}, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves order of results", func(t *testing.T) {
|
||||
input := func(yield func(IO[int]) bool) {
|
||||
for i := 5; i >= 1; i-- {
|
||||
n := i // capture loop variable
|
||||
yield(func() int { return n * 10 })
|
||||
}
|
||||
}
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []int
|
||||
for n := range result {
|
||||
collected = append(collected, n)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{50, 40, 30, 20, 10}, collected)
|
||||
})
|
||||
|
||||
t.Run("works with complex types", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Label string
|
||||
}
|
||||
|
||||
input := func(yield func(IO[Result]) bool) {
|
||||
yield(Of(Result{Value: 1, Label: "first"}))
|
||||
yield(Of(Result{Value: 2, Label: "second"}))
|
||||
yield(Of(Result{Value: 3, Label: "third"}))
|
||||
}
|
||||
|
||||
resultIO := SequenceIter(input)
|
||||
|
||||
result := resultIO()
|
||||
var collected []Result
|
||||
for r := range result {
|
||||
collected = append(collected, r)
|
||||
}
|
||||
|
||||
expected := []Result{
|
||||
{Value: 1, Label: "first"},
|
||||
{Value: 2, Label: "second"},
|
||||
{Value: 3, Label: "third"},
|
||||
}
|
||||
assert.Equal(t, expected, collected)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func ExampleIOEither_do() {
|
||||
bar := Of[error](1)
|
||||
|
||||
// quux consumes the state of three bindings and returns an [IO] instead of an [IOEither]
|
||||
quux := func(t T.Tuple3[string, int, string]) IO[any] {
|
||||
quux := func(t T.Tuple3[string, int, string]) IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
ioeither.Map[error](func(r *http.Request) *url.URL {
|
||||
return r.URL
|
||||
}),
|
||||
ioeither.ChainFirstIOK[error](func(u *url.URL) io.IO[any] {
|
||||
ioeither.ChainFirstIOK[error](func(u *url.URL) io.IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
q := u.Query()
|
||||
assert.Equal(t, "10", q.Get("limit"))
|
||||
|
||||
@@ -15,8 +15,12 @@
|
||||
|
||||
package builder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/ioeither"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
)
|
||||
|
||||
type (
|
||||
IOEither[A any] = ioeither.IOEither[error, A]
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -428,11 +428,11 @@ func Swap[E, A any](val IOEither[E, A]) IOEither[A, E] {
|
||||
}
|
||||
|
||||
// FromImpure converts a side effect without a return value into an [IOEither] that returns any
|
||||
func FromImpure[E any](f func()) IOEither[E, any] {
|
||||
func FromImpure[E any](f func()) IOEither[E, Void] {
|
||||
return function.Pipe2(
|
||||
f,
|
||||
io.FromImpure,
|
||||
FromIO[E, any],
|
||||
FromIO[E, Void],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -489,6 +489,8 @@ func After[E, A any](timestamp time.Time) Operator[E, A, A] {
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
|
||||
//
|
||||
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery or error transformation scenarios where you want to handle
|
||||
// errors by performing another computation that may also fail.
|
||||
//
|
||||
@@ -523,6 +525,8 @@ func MonadChainLeft[EA, EB, A any](fa IOEither[EA, A], f Kleisli[EB, EA, A]) IOE
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of an [IOEither].
|
||||
//
|
||||
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is particularly useful in functional composition pipelines where you want to handle
|
||||
// errors by performing another computation that may also fail.
|
||||
//
|
||||
@@ -644,6 +648,8 @@ func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
|
||||
// If the IOEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new IOEither that replaces the original.
|
||||
//
|
||||
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
|
||||
@@ -490,3 +490,148 @@ func TestOrElseW(t *testing.T) {
|
||||
preserved := preserveRecover(preservedRight)()
|
||||
assert.Equal(t, E.Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
|
||||
// This test verifies that both functions produce the same results for all scenarios:
|
||||
// - Left values with error recovery
|
||||
// - Left values with error transformation
|
||||
// - Right values passing through unchanged
|
||||
func TestChainLeftIdenticalToOrElse(t *testing.T) {
|
||||
// Test 1: Left value with error recovery - both should recover to Right
|
||||
t.Run("Left value recovery - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) IOEither[string, int] {
|
||||
if e == "recoverable" {
|
||||
return Right[string](42)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("recoverable")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(recoveryFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(recoveryFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](42), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 2: Left value with error transformation - both should transform error
|
||||
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
transformFn := func(e string) IOEither[string, int] {
|
||||
return Left[int]("transformed: " + e)
|
||||
}
|
||||
|
||||
input := Left[int]("original error")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(transformFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(transformFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 3: Right value - both should pass through unchanged
|
||||
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handlerFn := func(e string) IOEither[string, int] {
|
||||
return Left[int]("should not be called")
|
||||
}
|
||||
|
||||
input := Right[string](100)
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(handlerFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(handlerFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](100), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 4: Error type widening - both should handle type transformation
|
||||
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
widenFn := func(e string) IOEither[int, int] {
|
||||
return Left[int](404)
|
||||
}
|
||||
|
||||
input := Left[int]("not found")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := ChainLeft(widenFn)(input)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := OrElse(widenFn)(input)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Left[int](404), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 5: Composition in pipeline - both should work identically in F.Pipe
|
||||
t.Run("Pipeline composition - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
recoveryFn := func(e string) IOEither[string, int] {
|
||||
if e == "network error" {
|
||||
return Right[string](0)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("network error")
|
||||
|
||||
// Using ChainLeft in pipeline
|
||||
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))()
|
||||
|
||||
// Using OrElse in pipeline
|
||||
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](0), resultChainLeft)
|
||||
})
|
||||
|
||||
// Test 6: Multiple chained operations - both should behave identically
|
||||
t.Run("Multiple operations - ChainLeft equals OrElse", func(t *testing.T) {
|
||||
handler1 := func(e string) IOEither[string, int] {
|
||||
if e == "error1" {
|
||||
return Right[string](1)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
handler2 := func(e string) IOEither[string, int] {
|
||||
if e == "error2" {
|
||||
return Right[string](2)
|
||||
}
|
||||
return Left[int](e)
|
||||
}
|
||||
|
||||
input := Left[int]("error2")
|
||||
|
||||
// Using ChainLeft
|
||||
resultChainLeft := F.Pipe2(
|
||||
input,
|
||||
ChainLeft(handler1),
|
||||
ChainLeft(handler2),
|
||||
)()
|
||||
|
||||
// Using OrElse
|
||||
resultOrElse := F.Pipe2(
|
||||
input,
|
||||
OrElse(handler1),
|
||||
OrElse(handler2),
|
||||
)()
|
||||
|
||||
// Both should produce identical results
|
||||
assert.Equal(t, resultOrElse, resultChainLeft)
|
||||
assert.Equal(t, E.Right[string](2), resultChainLeft)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package ioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
@@ -18,4 +19,6 @@ type (
|
||||
// Trampoline represents a tail-recursive computation that can be evaluated safely
|
||||
// without stack overflow. It's used for implementing stack-safe recursive algorithms.
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
package ioref
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
// MakeIORef creates a new IORef containing the given initial value.
|
||||
@@ -49,6 +51,32 @@ func MakeIORef[A any](a A) IO[IORef[A]] {
|
||||
}
|
||||
}
|
||||
|
||||
// Write atomically writes a new value to an IORef and returns the written value.
|
||||
//
|
||||
// This function returns a Kleisli arrow that takes an IORef and produces an IO
|
||||
// computation that writes the given value to the reference. The write operation
|
||||
// is atomic and thread-safe, using a write lock to ensure exclusive access.
|
||||
//
|
||||
// Parameters:
|
||||
// - a: The new value to write to the IORef
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow from IORef[A] to IO[A] that writes the value and returns it
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Write a new value
|
||||
// newValue := ioref.Write(100)(ref)() // Returns 100, ref now contains 100
|
||||
//
|
||||
// // Chain writes
|
||||
// pipe.Pipe2(
|
||||
// ref,
|
||||
// ioref.Write(50),
|
||||
// io.Chain(ioref.Write(75)),
|
||||
// )() // ref now contains 75
|
||||
//
|
||||
//go:inline
|
||||
func Write[A any](a A) io.Kleisli[IORef[A], A] {
|
||||
return func(ref IORef[A]) IO[A] {
|
||||
@@ -124,20 +152,112 @@ func Read[A any](ref IORef[A]) IO[A] {
|
||||
// ioref.Modify(func(x int) int { return x + 10 }),
|
||||
// io.Chain(ioref.Modify(func(x int) int { return x * 2 })),
|
||||
// )()
|
||||
//
|
||||
//go:inline
|
||||
func Modify[A any](f Endomorphism[A]) io.Kleisli[IORef[A], A] {
|
||||
return ModifyIOK(function.Flow2(f, io.Of))
|
||||
}
|
||||
|
||||
// ModifyIOK atomically modifies the value in an IORef using an IO-based transformation.
|
||||
//
|
||||
// This is a more powerful version of Modify that allows the transformation function
|
||||
// to perform IO effects. The function takes a Kleisli arrow (a function from A to IO[A])
|
||||
// and returns a Kleisli arrow that modifies the IORef atomically.
|
||||
//
|
||||
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
|
||||
// access during the read-modify-write cycle. The IO effect in the transformation
|
||||
// function is executed while holding the lock.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow (io.Kleisli[A, A]) that transforms the current value with IO effects
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow from IORef[A] to IO[A] that returns the new value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Modify with an IO effect (e.g., logging)
|
||||
// modifyWithLog := ioref.ModifyIOK(func(x int) io.IO[int] {
|
||||
// return func() int {
|
||||
// fmt.Printf("Old value: %d\n", x)
|
||||
// return x * 2
|
||||
// }
|
||||
// })
|
||||
// newValue := modifyWithLog(ref)() // Logs and returns 84
|
||||
//
|
||||
// // Chain multiple IO-based modifications
|
||||
// pipe.Pipe2(
|
||||
// ref,
|
||||
// ioref.ModifyIOK(func(x int) io.IO[int] {
|
||||
// return io.Of(x + 10)
|
||||
// }),
|
||||
// io.Chain(ioref.ModifyIOK(func(x int) io.IO[int] {
|
||||
// return io.Of(x * 2)
|
||||
// })),
|
||||
// )()
|
||||
func ModifyIOK[A any](f io.Kleisli[A, A]) io.Kleisli[IORef[A], A] {
|
||||
return func(ref IORef[A]) IO[A] {
|
||||
return func() A {
|
||||
ref.mu.Lock()
|
||||
defer ref.mu.Unlock()
|
||||
|
||||
ref.a = f(ref.a)
|
||||
ref.a = f(ref.a)()
|
||||
return ref.a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyReaderIOK atomically modifies the value in an IORef using a ReaderIO-based transformation.
|
||||
//
|
||||
// This is a variant of ModifyIOK that works with ReaderIO computations, allowing the
|
||||
// transformation function to access an environment of type R while performing IO effects.
|
||||
// This is useful when the modification logic needs access to configuration, context,
|
||||
// or other shared resources.
|
||||
//
|
||||
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
|
||||
// access during the read-modify-write cycle. The ReaderIO effect in the transformation
|
||||
// function is executed while holding the lock.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, A]) that takes the current value
|
||||
// and an environment R, and returns an IO computation producing the new value
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, A] that returns the new value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// multiplier int
|
||||
// }
|
||||
//
|
||||
// ref := ioref.MakeIORef(10)()
|
||||
//
|
||||
// // Modify using environment
|
||||
// modifyWithConfig := ioref.ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
// return func(cfg Config) io.IO[int] {
|
||||
// return func() int {
|
||||
// return x * cfg.multiplier
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// config := Config{multiplier: 5}
|
||||
// newValue := modifyWithConfig(ref)(config)() // Returns 50, ref now contains 50
|
||||
func ModifyReaderIOK[R, A any](f readerio.Kleisli[R, A, A]) readerio.Kleisli[R, IORef[A], A] {
|
||||
return func(ref IORef[A]) ReaderIO[R, A] {
|
||||
return func(r R) readerio.IO[A] {
|
||||
return func() A {
|
||||
ref.mu.Lock()
|
||||
defer ref.mu.Unlock()
|
||||
|
||||
ref.a = f(ref.a)(r)()
|
||||
return ref.a
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyWithResult atomically modifies the value in an IORef and returns both
|
||||
// the new value and an additional result computed from the old value.
|
||||
//
|
||||
@@ -167,14 +287,122 @@ func Modify[A any](f Endomorphism[A]) io.Kleisli[IORef[A], A] {
|
||||
//
|
||||
//go:inline
|
||||
func ModifyWithResult[A, B any](f func(A) Pair[A, B]) io.Kleisli[IORef[A], B] {
|
||||
return ModifyIOKWithResult(function.Flow2(f, io.Of))
|
||||
}
|
||||
|
||||
// ModifyIOKWithResult atomically modifies the value in an IORef and returns a result,
|
||||
// using an IO-based transformation function.
|
||||
//
|
||||
// This is a more powerful version of ModifyWithResult that allows the transformation
|
||||
// function to perform IO effects. The function takes a Kleisli arrow that transforms
|
||||
// the old value into an IO computation producing a Pair of (new value, result).
|
||||
//
|
||||
// This is useful when you need to:
|
||||
// - Both transform the stored value and compute some result based on the old value
|
||||
// - Perform IO effects during the transformation (e.g., logging, validation)
|
||||
// - Ensure atomicity of the entire read-transform-write-compute cycle
|
||||
//
|
||||
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
|
||||
// access. The IO effect in the transformation function is executed while holding the lock.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow (io.Kleisli[A, Pair[A, B]]) that takes the old value and
|
||||
// returns an IO computation producing a Pair of (new value, result)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow from IORef[A] to IO[B] that produces the result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Increment with IO effect and return old value
|
||||
// incrementWithLog := ioref.ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
|
||||
// return func() pair.Pair[int, int] {
|
||||
// fmt.Printf("Incrementing from %d\n", x)
|
||||
// return pair.MakePair(x+1, x)
|
||||
// }
|
||||
// })
|
||||
// oldValue := incrementWithLog(ref)() // Logs and returns 42, ref now contains 43
|
||||
//
|
||||
// // Swap with validation
|
||||
// swapWithValidation := ioref.ModifyIOKWithResult(func(old int) io.IO[pair.Pair[int, string]] {
|
||||
// return func() pair.Pair[int, string] {
|
||||
// if old < 0 {
|
||||
// return pair.MakePair(0, "reset negative")
|
||||
// }
|
||||
// return pair.MakePair(100, fmt.Sprintf("swapped %d", old))
|
||||
// }
|
||||
// })
|
||||
// message := swapWithValidation(ref)()
|
||||
func ModifyIOKWithResult[A, B any](f io.Kleisli[A, Pair[A, B]]) io.Kleisli[IORef[A], B] {
|
||||
return func(ref IORef[A]) IO[B] {
|
||||
return func() B {
|
||||
ref.mu.Lock()
|
||||
defer ref.mu.Unlock()
|
||||
|
||||
result := f(ref.a)
|
||||
result := f(ref.a)()
|
||||
ref.a = pair.Head(result)
|
||||
return pair.Tail(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyReaderIOKWithResult atomically modifies the value in an IORef and returns a result,
|
||||
// using a ReaderIO-based transformation function.
|
||||
//
|
||||
// This combines the capabilities of ModifyIOKWithResult and ModifyReaderIOK, allowing the
|
||||
// transformation function to:
|
||||
// - Access an environment of type R (like configuration or context)
|
||||
// - Perform IO effects during the transformation
|
||||
// - Both update the stored value and compute a result based on the old value
|
||||
// - Ensure atomicity of the entire read-transform-write-compute cycle
|
||||
//
|
||||
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
|
||||
// access. The ReaderIO effect in the transformation function is executed while holding the lock.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, Pair[A, B]]) that takes the old value
|
||||
// and an environment R, and returns an IO computation producing a Pair of (new value, result)
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, B] that produces the result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// logEnabled bool
|
||||
// }
|
||||
//
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Increment with conditional logging, return old value
|
||||
// incrementWithLog := ioref.ModifyReaderIOKWithResult(
|
||||
// func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
// return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
// return func() pair.Pair[int, int] {
|
||||
// if cfg.logEnabled {
|
||||
// fmt.Printf("Incrementing from %d\n", x)
|
||||
// }
|
||||
// return pair.MakePair(x+1, x)
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// config := Config{logEnabled: true}
|
||||
// oldValue := incrementWithLog(ref)(config)() // Logs and returns 42, ref now contains 43
|
||||
func ModifyReaderIOKWithResult[R, A, B any](f readerio.Kleisli[R, A, Pair[A, B]]) readerio.Kleisli[R, IORef[A], B] {
|
||||
return func(ref IORef[A]) ReaderIO[R, B] {
|
||||
return func(r R) readerio.IO[B] {
|
||||
return func() B {
|
||||
ref.mu.Lock()
|
||||
defer ref.mu.Unlock()
|
||||
|
||||
result := f(ref.a)(r)()
|
||||
ref.a = pair.Head(result)
|
||||
return pair.Tail(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
919
v2/ioref/ioref_test.go
Normal file
919
v2/ioref/ioref_test.go
Normal file
@@ -0,0 +1,919 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ioref
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMakeIORef(t *testing.T) {
|
||||
t.Run("creates IORef with integer value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, 42, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with string value", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, "hello", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with slice value", func(t *testing.T) {
|
||||
slice := []int{1, 2, 3}
|
||||
ref := MakeIORef(slice)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, slice, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with struct value", func(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
ref := MakeIORef(person)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, person, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with zero value", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Equal(t, 0, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("creates IORef with nil pointer", func(t *testing.T) {
|
||||
var ptr *int
|
||||
ref := MakeIORef(ptr)()
|
||||
assert.NotNil(t, ref)
|
||||
assert.Nil(t, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("multiple IORefs are independent", func(t *testing.T) {
|
||||
ref1 := MakeIORef(10)()
|
||||
ref2 := MakeIORef(20)()
|
||||
|
||||
assert.Equal(t, 10, Read(ref1)())
|
||||
assert.Equal(t, 20, Read(ref2)())
|
||||
|
||||
Write(30)(ref1)()
|
||||
assert.Equal(t, 30, Read(ref1)())
|
||||
assert.Equal(t, 20, Read(ref2)()) // ref2 unchanged
|
||||
})
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
t.Run("reads initial value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
value := Read(ref)()
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("reads updated value", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
Write(20)(ref)()
|
||||
value := Read(ref)()
|
||||
assert.Equal(t, 20, value)
|
||||
})
|
||||
|
||||
t.Run("multiple reads return same value", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
value1 := Read(ref)()
|
||||
value2 := Read(ref)()
|
||||
value3 := Read(ref)()
|
||||
assert.Equal(t, 100, value1)
|
||||
assert.Equal(t, 100, value2)
|
||||
assert.Equal(t, 100, value3)
|
||||
})
|
||||
|
||||
t.Run("concurrent reads are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
results := make([]int, iterations)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
results[idx] = Read(ref)()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// All reads should return the same value
|
||||
for _, v := range results {
|
||||
assert.Equal(t, 42, v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reads during concurrent writes", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 50
|
||||
|
||||
// Start concurrent writes
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(val int) {
|
||||
defer wg.Done()
|
||||
Write(val)(ref)()
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Start concurrent reads
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
value := Read(ref)()
|
||||
// Value should be valid (between 0 and iterations-1)
|
||||
assert.GreaterOrEqual(t, value, 0)
|
||||
assert.Less(t, value, iterations)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
t.Run("writes new value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
result := Write(100)(ref)()
|
||||
assert.Equal(t, 100, result)
|
||||
assert.Equal(t, 100, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("overwrites existing value", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
Write(20)(ref)()
|
||||
Write(30)(ref)()
|
||||
assert.Equal(t, 30, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("returns written value", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
result := Write(42)(ref)()
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("writes string value", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
result := Write("world")(ref)()
|
||||
assert.Equal(t, "world", result)
|
||||
assert.Equal(t, "world", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained writes", func(t *testing.T) {
|
||||
ref := MakeIORef(1)()
|
||||
Write(2)(ref)()
|
||||
Write(3)(ref)()
|
||||
result := Write(4)(ref)()
|
||||
assert.Equal(t, 4, result)
|
||||
assert.Equal(t, 4, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent writes are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(val int) {
|
||||
defer wg.Done()
|
||||
Write(val)(ref)()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Final value should be one of the written values
|
||||
finalValue := Read(ref)()
|
||||
assert.GreaterOrEqual(t, finalValue, 0)
|
||||
assert.Less(t, finalValue, iterations)
|
||||
})
|
||||
|
||||
t.Run("write with zero value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
Write(0)(ref)()
|
||||
assert.Equal(t, 0, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModify(t *testing.T) {
|
||||
t.Run("modifies value with simple function", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
result := Modify(func(x int) int { return x * 2 })(ref)()
|
||||
assert.Equal(t, 20, result)
|
||||
assert.Equal(t, 20, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modifies with addition", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
Modify(func(x int) int { return x + 10 })(ref)()
|
||||
assert.Equal(t, 15, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modifies string value", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
result := Modify(func(s string) string { return s + " world" })(ref)()
|
||||
assert.Equal(t, "hello world", result)
|
||||
assert.Equal(t, "hello world", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications", func(t *testing.T) {
|
||||
ref := MakeIORef(2)()
|
||||
Modify(func(x int) int { return x * 3 })(ref)() // 6
|
||||
Modify(func(x int) int { return x + 4 })(ref)() // 10
|
||||
result := Modify(func(x int) int { return x / 2 })(ref)()
|
||||
assert.Equal(t, 5, result)
|
||||
assert.Equal(t, 5, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
Modify(func(x int) int { return x + 1 })(ref)()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modify with identity function", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
result := Modify(func(x int) int { return x })(ref)()
|
||||
assert.Equal(t, 42, result)
|
||||
assert.Equal(t, 42, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modify returns new value", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
result := Modify(func(x int) int { return x - 50 })(ref)()
|
||||
assert.Equal(t, 50, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyWithResult(t *testing.T) {
|
||||
t.Run("modifies and returns old value", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x+1, x)
|
||||
})(ref)()
|
||||
assert.Equal(t, 42, oldValue)
|
||||
assert.Equal(t, 43, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("swaps value and returns old", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(200, x)
|
||||
})(ref)()
|
||||
assert.Equal(t, 100, oldValue)
|
||||
assert.Equal(t, 200, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("returns different type", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
message := ModifyWithResult(func(x int) pair.Pair[int, string] {
|
||||
return pair.MakePair(x*2, fmt.Sprintf("doubled from %d", x))
|
||||
})(ref)()
|
||||
assert.Equal(t, "doubled from 42", message)
|
||||
assert.Equal(t, 84, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("computes result based on old value", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
wasPositive := ModifyWithResult(func(x int) pair.Pair[int, bool] {
|
||||
return pair.MakePair(x+5, x > 0)
|
||||
})(ref)()
|
||||
assert.True(t, wasPositive)
|
||||
assert.Equal(t, 15, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications with results", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
result1 := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x*2, x)
|
||||
})(ref)()
|
||||
result2 := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x+10, x)
|
||||
})(ref)()
|
||||
assert.Equal(t, 5, result1)
|
||||
assert.Equal(t, 10, result2)
|
||||
assert.Equal(t, 20, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications with results are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
results := make([]int, iterations)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x+1, x)
|
||||
})(ref)()
|
||||
results[idx] = oldValue
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
|
||||
// All old values should be unique
|
||||
seen := make(map[int]bool)
|
||||
for _, v := range results {
|
||||
assert.False(t, seen[v])
|
||||
seen[v] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extract and replace pattern", func(t *testing.T) {
|
||||
ref := MakeIORef([]int{1, 2, 3})()
|
||||
first := ModifyWithResult(func(xs []int) pair.Pair[[]int, int] {
|
||||
if len(xs) == 0 {
|
||||
return pair.MakePair(xs, 0)
|
||||
}
|
||||
return pair.MakePair(xs[1:], xs[0])
|
||||
})(ref)()
|
||||
assert.Equal(t, 1, first)
|
||||
assert.Equal(t, []int{2, 3}, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyReaderIOK(t *testing.T) {
|
||||
type Config struct {
|
||||
multiplier int
|
||||
}
|
||||
|
||||
t.Run("modifies with environment", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
config := Config{multiplier: 5}
|
||||
|
||||
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x * cfg.multiplier)
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 50, result)
|
||||
assert.Equal(t, 50, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("uses environment for computation", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
config := Config{multiplier: 2}
|
||||
|
||||
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return func() int {
|
||||
return x / cfg.multiplier
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 50, result)
|
||||
assert.Equal(t, 50, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications with different configs", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
config1 := Config{multiplier: 2}
|
||||
config2 := Config{multiplier: 3}
|
||||
|
||||
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x * cfg.multiplier)
|
||||
}
|
||||
})(ref)(config1)()
|
||||
|
||||
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x + cfg.multiplier)
|
||||
}
|
||||
})(ref)(config2)()
|
||||
|
||||
assert.Equal(t, 23, result) // (10 * 2) + 3
|
||||
assert.Equal(t, 23, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
config := Config{multiplier: 1}
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
|
||||
return func(cfg Config) io.IO[int] {
|
||||
return io.Of(x + cfg.multiplier)
|
||||
}
|
||||
})(ref)(config)()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("environment provides configuration", func(t *testing.T) {
|
||||
type Settings struct {
|
||||
prefix string
|
||||
}
|
||||
ref := MakeIORef("world")()
|
||||
settings := Settings{prefix: "hello "}
|
||||
|
||||
result := ModifyReaderIOK(func(s string) readerio.ReaderIO[Settings, string] {
|
||||
return func(cfg Settings) io.IO[string] {
|
||||
return io.Of(cfg.prefix + s)
|
||||
}
|
||||
})(ref)(settings)()
|
||||
|
||||
assert.Equal(t, "hello world", result)
|
||||
assert.Equal(t, "hello world", Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyReaderIOKWithResult(t *testing.T) {
|
||||
type Config struct {
|
||||
logEnabled bool
|
||||
multiplier int
|
||||
}
|
||||
|
||||
t.Run("modifies with environment and returns result", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
config := Config{logEnabled: false, multiplier: 2}
|
||||
|
||||
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x*cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 42, oldValue)
|
||||
assert.Equal(t, 84, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("returns different type based on environment", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
config := Config{logEnabled: true, multiplier: 3}
|
||||
|
||||
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, string]] {
|
||||
return func() pair.Pair[int, string] {
|
||||
newVal := x * cfg.multiplier
|
||||
msg := fmt.Sprintf("multiplied %d by %d", x, cfg.multiplier)
|
||||
return pair.MakePair(newVal, msg)
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, "multiplied 10 by 3", message)
|
||||
assert.Equal(t, 30, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("conditional logic based on environment", func(t *testing.T) {
|
||||
ref := MakeIORef(-10)()
|
||||
config := Config{logEnabled: true, multiplier: 2}
|
||||
|
||||
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, string]] {
|
||||
return func() pair.Pair[int, string] {
|
||||
if x < 0 {
|
||||
return pair.MakePair(0, "reset negative value")
|
||||
}
|
||||
return pair.MakePair(x*cfg.multiplier, "multiplied positive value")
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, "reset negative value", message)
|
||||
assert.Equal(t, 0, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications with results", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
config := Config{logEnabled: false, multiplier: 2}
|
||||
|
||||
result1 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x*cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
result2 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x+cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, 5, result1)
|
||||
assert.Equal(t, 10, result2)
|
||||
assert.Equal(t, 12, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
config := Config{logEnabled: false, multiplier: 1}
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
results := make([]int, iterations)
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
|
||||
return func(cfg Config) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x+cfg.multiplier, x))
|
||||
}
|
||||
})(ref)(config)()
|
||||
results[idx] = oldValue
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
|
||||
// All old values should be unique
|
||||
seen := make(map[int]bool)
|
||||
for _, v := range results {
|
||||
assert.False(t, seen[v])
|
||||
seen[v] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("environment provides validation rules", func(t *testing.T) {
|
||||
type ValidationConfig struct {
|
||||
maxValue int
|
||||
}
|
||||
ref := MakeIORef(100)()
|
||||
config := ValidationConfig{maxValue: 50}
|
||||
|
||||
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[ValidationConfig, pair.Pair[int, string]] {
|
||||
return func(cfg ValidationConfig) io.IO[pair.Pair[int, string]] {
|
||||
return func() pair.Pair[int, string] {
|
||||
if x > cfg.maxValue {
|
||||
return pair.MakePair(cfg.maxValue, fmt.Sprintf("capped at %d", cfg.maxValue))
|
||||
}
|
||||
return pair.MakePair(x, "value within limits")
|
||||
}
|
||||
}
|
||||
})(ref)(config)()
|
||||
|
||||
assert.Equal(t, "capped at 50", message)
|
||||
assert.Equal(t, 50, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyIOK(t *testing.T) {
|
||||
t.Run("basic modification with IO effect", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
|
||||
// Double the value using ModifyIOK
|
||||
newValue := ModifyIOK(func(x int) io.IO[int] {
|
||||
return io.Of(x * 2)
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 84, newValue)
|
||||
assert.Equal(t, 84, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification with side effects", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
var sideEffect int
|
||||
|
||||
// Modify with a side effect
|
||||
newValue := ModifyIOK(func(x int) io.IO[int] {
|
||||
return func() int {
|
||||
sideEffect = x // Capture old value
|
||||
return x + 5
|
||||
}
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 15, newValue)
|
||||
assert.Equal(t, 10, sideEffect)
|
||||
assert.Equal(t, 15, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
|
||||
// First modification: add 10
|
||||
ModifyIOK(func(x int) io.IO[int] {
|
||||
return io.Of(x + 10)
|
||||
})(ref)()
|
||||
|
||||
// Second modification: multiply by 2
|
||||
result := ModifyIOK(func(x int) io.IO[int] {
|
||||
return io.Of(x * 2)
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 30, result)
|
||||
assert.Equal(t, 30, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
|
||||
// Increment concurrently
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ModifyIOK(func(x int) io.IO[int] {
|
||||
return io.Of(x + 1)
|
||||
})(ref)()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification with string type", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
|
||||
newValue := ModifyIOK(func(s string) io.IO[string] {
|
||||
return io.Of(s + " world")
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, "hello world", newValue)
|
||||
assert.Equal(t, "hello world", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification returns new value", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
|
||||
result := ModifyIOK(func(x int) io.IO[int] {
|
||||
return io.Of(x / 2)
|
||||
})(ref)()
|
||||
|
||||
// ModifyIOK returns the new value
|
||||
assert.Equal(t, 50, result)
|
||||
assert.Equal(t, 50, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification with complex IO computation", func(t *testing.T) {
|
||||
ref := MakeIORef(3)()
|
||||
|
||||
// Use a more complex IO computation
|
||||
newValue := ModifyIOK(func(x int) io.IO[int] {
|
||||
return F.Pipe1(
|
||||
io.Of(x),
|
||||
io.Map(func(n int) int { return n * n }),
|
||||
)
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 9, newValue)
|
||||
assert.Equal(t, 9, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyIOKWithResult(t *testing.T) {
|
||||
t.Run("basic modification with result", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
|
||||
// Increment and return old value
|
||||
oldValue := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x+1, x))
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 42, oldValue)
|
||||
assert.Equal(t, 43, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("swap and return old value", func(t *testing.T) {
|
||||
ref := MakeIORef(100)()
|
||||
|
||||
oldValue := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(200, x))
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 100, oldValue)
|
||||
assert.Equal(t, 200, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification with different result type", func(t *testing.T) {
|
||||
ref := MakeIORef(42)()
|
||||
|
||||
// Double the value and return a message
|
||||
message := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
|
||||
return io.Of(pair.MakePair(x*2, fmt.Sprintf("doubled from %d", x)))
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, "doubled from 42", message)
|
||||
assert.Equal(t, 84, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification with side effects in IO", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
var sideEffect string
|
||||
|
||||
result := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, bool]] {
|
||||
return func() pair.Pair[int, bool] {
|
||||
sideEffect = fmt.Sprintf("processing %d", x)
|
||||
return pair.MakePair(x+5, x > 5)
|
||||
}
|
||||
})(ref)()
|
||||
|
||||
assert.True(t, result)
|
||||
assert.Equal(t, "processing 10", sideEffect)
|
||||
assert.Equal(t, 15, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("chained modifications with results", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
|
||||
// First modification
|
||||
result1 := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x*2, x))
|
||||
})(ref)()
|
||||
|
||||
// Second modification
|
||||
result2 := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x+10, x))
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 5, result1) // Original value
|
||||
assert.Equal(t, 10, result2) // After first modification
|
||||
assert.Equal(t, 20, Read(ref)()) // After both modifications
|
||||
})
|
||||
|
||||
t.Run("concurrent modifications with results are thread-safe", func(t *testing.T) {
|
||||
ref := MakeIORef(0)()
|
||||
var wg sync.WaitGroup
|
||||
iterations := 100
|
||||
results := make([]int, iterations)
|
||||
|
||||
// Increment concurrently and collect old values
|
||||
for i := 0; i < iterations; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
oldValue := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, int]] {
|
||||
return io.Of(pair.MakePair(x+1, x))
|
||||
})(ref)()
|
||||
results[idx] = oldValue
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Final value should be iterations
|
||||
assert.Equal(t, iterations, Read(ref)())
|
||||
|
||||
// All old values should be unique and in range [0, iterations)
|
||||
seen := make(map[int]bool)
|
||||
for _, v := range results {
|
||||
assert.False(t, seen[v], "duplicate old value: %d", v)
|
||||
assert.GreaterOrEqual(t, v, 0)
|
||||
assert.Less(t, v, iterations)
|
||||
seen[v] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("modification with string types", func(t *testing.T) {
|
||||
ref := MakeIORef("hello")()
|
||||
|
||||
length := ModifyIOKWithResult(func(s string) io.IO[pair.Pair[string, int]] {
|
||||
return io.Of(pair.MakePair(s+" world", len(s)))
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 5, length)
|
||||
assert.Equal(t, "hello world", Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification with validation logic", func(t *testing.T) {
|
||||
ref := MakeIORef(-10)()
|
||||
|
||||
message := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
|
||||
return func() pair.Pair[int, string] {
|
||||
if x < 0 {
|
||||
return pair.MakePair(0, "reset negative value")
|
||||
}
|
||||
return pair.MakePair(x*2, "doubled positive value")
|
||||
}
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, "reset negative value", message)
|
||||
assert.Equal(t, 0, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("modification with complex IO computation", func(t *testing.T) {
|
||||
ref := MakeIORef(5)()
|
||||
|
||||
result := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
|
||||
return F.Pipe1(
|
||||
io.Of(x),
|
||||
io.Map(func(n int) pair.Pair[int, string] {
|
||||
squared := n * n
|
||||
return pair.MakePair(squared, fmt.Sprintf("%d squared is %d", n, squared))
|
||||
}),
|
||||
)
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, "5 squared is 25", result)
|
||||
assert.Equal(t, 25, Read(ref)())
|
||||
})
|
||||
|
||||
t.Run("extract and replace pattern", func(t *testing.T) {
|
||||
ref := MakeIORef([]int{1, 2, 3})()
|
||||
|
||||
// Extract first element and remove it from the slice
|
||||
first := ModifyIOKWithResult(func(xs []int) io.IO[pair.Pair[[]int, int]] {
|
||||
return func() pair.Pair[[]int, int] {
|
||||
if len(xs) == 0 {
|
||||
return pair.MakePair(xs, 0)
|
||||
}
|
||||
return pair.MakePair(xs[1:], xs[0])
|
||||
}
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 1, first)
|
||||
assert.Equal(t, []int{2, 3}, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyIOKIntegration(t *testing.T) {
|
||||
t.Run("ModifyIOK integrates with Modify", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
|
||||
// Use Modify (which internally uses ModifyIOK)
|
||||
result1 := Modify(N.Mul(2))(ref)()
|
||||
|
||||
assert.Equal(t, 20, result1)
|
||||
|
||||
// Use ModifyIOK directly
|
||||
result2 := ModifyIOK(func(x int) io.IO[int] {
|
||||
return io.Of(x + 5)
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 25, result2)
|
||||
assert.Equal(t, 25, Read(ref)())
|
||||
})
|
||||
}
|
||||
|
||||
func TestModifyIOKWithResultIntegration(t *testing.T) {
|
||||
t.Run("ModifyIOKWithResult integrates with ModifyWithResult", func(t *testing.T) {
|
||||
ref := MakeIORef(10)()
|
||||
|
||||
// Use ModifyWithResult (which internally uses ModifyIOKWithResult)
|
||||
result1 := ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
return pair.MakePair(x*2, x)
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, 10, result1)
|
||||
assert.Equal(t, 20, Read(ref)())
|
||||
|
||||
// Use ModifyIOKWithResult directly
|
||||
result2 := ModifyIOKWithResult(func(x int) io.IO[pair.Pair[int, string]] {
|
||||
return io.Of(pair.MakePair(x+5, fmt.Sprintf("was %d", x)))
|
||||
})(ref)()
|
||||
|
||||
assert.Equal(t, "was 20", result2)
|
||||
assert.Equal(t, 25, Read(ref)())
|
||||
})
|
||||
}
|
||||
@@ -45,30 +45,134 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
)
|
||||
|
||||
type (
|
||||
// ioRef is the internal implementation of a mutable reference.
|
||||
// It uses a read-write mutex to ensure thread-safe access.
|
||||
// It uses a read-write mutex to ensure thread-safe access to the stored value.
|
||||
//
|
||||
// The mutex allows multiple concurrent readers (using RLock) but ensures
|
||||
// exclusive access for writers (using Lock), preventing race conditions
|
||||
// when reading or modifying the stored value.
|
||||
//
|
||||
// This type is not exported; users interact with it through the IORef type alias.
|
||||
ioRef[A any] struct {
|
||||
mu sync.RWMutex
|
||||
a A
|
||||
mu sync.RWMutex // Protects concurrent access to the stored value
|
||||
a A // The stored value
|
||||
}
|
||||
|
||||
// IO represents a synchronous computation that may have side effects.
|
||||
// It's a function that takes no arguments and returns a value of type A.
|
||||
//
|
||||
// IO computations are lazy - they don't execute until explicitly invoked
|
||||
// by calling the function. This allows for composing and chaining effects
|
||||
// before execution.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Define an IO computation
|
||||
// computation := func() int {
|
||||
// fmt.Println("Computing...")
|
||||
// return 42
|
||||
// }
|
||||
//
|
||||
// // Nothing happens yet - the computation is lazy
|
||||
// result := computation() // Now it executes and prints "Computing..."
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
// ReaderIO represents a computation that requires an environment of type R
|
||||
// and produces an IO effect that yields a value of type A.
|
||||
//
|
||||
// This combines the Reader pattern (dependency injection) with IO effects,
|
||||
// allowing computations to access shared configuration or context while
|
||||
// performing side effects.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// multiplier int
|
||||
// }
|
||||
//
|
||||
// // A ReaderIO that uses config to compute a value
|
||||
// computation := func(cfg Config) io.IO[int] {
|
||||
// return func() int {
|
||||
// return 42 * cfg.multiplier
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute with specific config
|
||||
// result := computation(Config{multiplier: 2})() // Returns 84
|
||||
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
|
||||
|
||||
// IORef represents a mutable reference to a value of type A.
|
||||
// Operations on IORef are thread-safe and performed within the IO monad.
|
||||
//
|
||||
// IORef provides a way to work with mutable state in a functional style,
|
||||
// where mutations are explicit and contained within IO computations.
|
||||
// This makes side effects visible in the type system and allows for
|
||||
// better reasoning about code that uses mutable state.
|
||||
//
|
||||
// All operations on IORef (Read, Write, Modify, etc.) are atomic and
|
||||
// thread-safe, making it safe to share IORefs across goroutines.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a new IORef
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
//
|
||||
// // Read the current value
|
||||
// value := ioref.Read(ref)() // 42
|
||||
//
|
||||
// // Write a new value
|
||||
// ioref.Write(100)(ref)()
|
||||
//
|
||||
// // Modify the value atomically
|
||||
// ioref.Modify(func(x int) int { return x * 2 })(ref)()
|
||||
IORef[A any] = *ioRef[A]
|
||||
|
||||
// Endomorphism represents a function from A to A.
|
||||
// It's commonly used with Modify to transform the value in an IORef.
|
||||
//
|
||||
// An endomorphism is a morphism (structure-preserving map) from a
|
||||
// mathematical object to itself. In programming terms, it's simply
|
||||
// a function that takes a value and returns a value of the same type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // An endomorphism that doubles an integer
|
||||
// double := func(x int) int { return x * 2 }
|
||||
//
|
||||
// // An endomorphism that uppercases a string
|
||||
// upper := func(s string) string { return strings.ToUpper(s) }
|
||||
//
|
||||
// // Use with IORef
|
||||
// ref := ioref.MakeIORef(21)()
|
||||
// ioref.Modify(double)(ref)() // ref now contains 42
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Pair represents a tuple of two values of types A and B.
|
||||
// It's used with ModifyWithResult and ModifyIOKWithResult to return both
|
||||
// a new value for the IORef (head) and a computed result (tail).
|
||||
//
|
||||
// The head of the pair contains the new value to store in the IORef,
|
||||
// while the tail contains the result to return from the operation.
|
||||
// This allows atomic operations that both update the reference and
|
||||
// compute a result based on the old value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a pair where head is the new value and tail is the old value
|
||||
// p := pair.MakePair(newValue, oldValue)
|
||||
//
|
||||
// // Extract values
|
||||
// newVal := pair.Head(p) // Gets the head (new value)
|
||||
// oldVal := pair.Tail(p) // Gets the tail (old value)
|
||||
//
|
||||
// // Use with ModifyWithResult to swap and return old value
|
||||
// ref := ioref.MakeIORef(42)()
|
||||
// oldValue := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
|
||||
// return pair.MakePair(100, x) // Store 100, return old value
|
||||
// })(ref)() // oldValue is 42, ref now contains 100
|
||||
Pair[A, B any] = pair.Pair[A, B]
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ func ExampleIOEither_do() {
|
||||
bar := Of(1)
|
||||
|
||||
// quux consumes the state of three bindings and returns an [IO] instead of an [IOEither]
|
||||
quux := func(t T.Tuple3[string, int, string]) IO[any] {
|
||||
quux := func(t T.Tuple3[string, int, string]) IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
log.Printf("t1: %s, t2: %d, t3: %s", t.F1, t.F2, t.F3)
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestBuilderWithQuery(t *testing.T) {
|
||||
ioresult.Map(func(r *http.Request) *url.URL {
|
||||
return r.URL
|
||||
}),
|
||||
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[any] {
|
||||
ioresult.ChainFirstIOK(func(u *url.URL) io.IO[Void] {
|
||||
return io.FromImpure(func() {
|
||||
q := u.Query()
|
||||
assert.Equal(t, "10", q.Get("limit"))
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
|
||||
package builder
|
||||
|
||||
import "github.com/IBM/fp-go/v2/ioresult"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
type (
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
Kleisli[A, B any] = ioresult.Kleisli[A, B]
|
||||
Operator[A, B any] = ioresult.Operator[A, B]
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -52,27 +52,27 @@ func MakeClient(httpClient *http.Client) Client {
|
||||
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
|
||||
//
|
||||
//go:inline
|
||||
func ReadFullResponse(client Client) Kleisli[Requester, H.FullResponse] {
|
||||
func ReadFullResponse(client Client) Operator[*http.Request, H.FullResponse] {
|
||||
return IOEH.ReadFullResponse(client)
|
||||
}
|
||||
|
||||
// ReadAll sends a request and reads the response as bytes
|
||||
//
|
||||
//go:inline
|
||||
func ReadAll(client Client) Kleisli[Requester, []byte] {
|
||||
func ReadAll(client Client) Operator[*http.Request, []byte] {
|
||||
return IOEH.ReadAll(client)
|
||||
}
|
||||
|
||||
// ReadText sends a request, reads the response and represents the response as a text string
|
||||
//
|
||||
//go:inline
|
||||
func ReadText(client Client) Kleisli[Requester, string] {
|
||||
func ReadText(client Client) Operator[*http.Request, string] {
|
||||
return IOEH.ReadText(client)
|
||||
}
|
||||
|
||||
// ReadJSON sends a request, reads the response and parses the response as JSON
|
||||
//
|
||||
//go:inline
|
||||
func ReadJSON[A any](client Client) Kleisli[Requester, A] {
|
||||
func ReadJSON[A any](client Client) Operator[*http.Request, A] {
|
||||
return IOEH.ReadJSON[A](client)
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ func Swap[A any](val IOResult[A]) ioeither.IOEither[A, error] {
|
||||
// FromImpure converts a side effect without a return value into a side effect that returns any
|
||||
//
|
||||
//go:inline
|
||||
func FromImpure[E any](f func()) IOResult[any] {
|
||||
func FromImpure[E any](f func()) IOResult[Void] {
|
||||
return ioeither.FromImpure[error](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
@@ -56,4 +57,6 @@ type (
|
||||
// Predicate represents a function that tests a value of type A and returns a boolean.
|
||||
// It's commonly used for filtering and conditional operations.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
54
v2/iterator/iter/last.go
Normal file
54
v2/iterator/iter/last.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Last returns the last element from an [Iterator] wrapped in an [Option].
|
||||
//
|
||||
// This function retrieves the last element from the iterator by consuming the entire
|
||||
// sequence. If the iterator contains at least one element, it returns Some(element).
|
||||
// If the iterator is empty, it returns None.
|
||||
//
|
||||
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - it: The input iterator to get the last element from
|
||||
//
|
||||
// Returns:
|
||||
// - Option[U]: Some(last element) if the iterator is non-empty, None otherwise
|
||||
//
|
||||
// Example with non-empty sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// last := iter.Last(seq)
|
||||
// // Returns: Some(5)
|
||||
//
|
||||
// Example with empty sequence:
|
||||
//
|
||||
// seq := iter.Empty[int]()
|
||||
// last := iter.Last(seq)
|
||||
// // Returns: None
|
||||
//
|
||||
// Example with filtered sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// filtered := iter.Filter(func(x int) bool { return x < 4 })(seq)
|
||||
// last := iter.Last(filtered)
|
||||
// // Returns: Some(3)
|
||||
func Last[U any](it Seq[U]) Option[U] {
|
||||
var last U
|
||||
found := false
|
||||
|
||||
for last = range it {
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
return option.None[U]()
|
||||
}
|
||||
return option.Some(last)
|
||||
}
|
||||
305
v2/iterator/iter/last_test.go
Normal file
305
v2/iterator/iter/last_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestLast test getting the last element from a non-empty sequence
|
||||
func TestLastSimple(t *testing.T) {
|
||||
|
||||
t.Run("returns last element from integer sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(3), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of("c"), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
|
||||
t.Run("returns last element from large sequence", func(t *testing.T) {
|
||||
seq := From(100, 200, 300, 400, 500)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(500), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastEmpty tests getting the last element from an empty sequence
|
||||
func TestLastEmpty(t *testing.T) {
|
||||
|
||||
t.Run("returns None for empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[string](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty struct sequence", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Value int
|
||||
}
|
||||
seq := Empty[TestStruct]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[TestStruct](), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty sequence of functions", func(t *testing.T) {
|
||||
type TestFunc func(int)
|
||||
seq := Empty[TestFunc]()
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[TestFunc](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithComplex tests Last with complex types
|
||||
func TestLastWithComplex(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("returns last person", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
last := Last(seq)
|
||||
expected := O.Of(Person{"Charlie", 35})
|
||||
assert.Equal(t, expected, last)
|
||||
})
|
||||
|
||||
t.Run("returns last pointer", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(p2), last)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLastWithFunctions(t *testing.T) {
|
||||
|
||||
t.Run("return function", func(t *testing.T) {
|
||||
|
||||
want := "last"
|
||||
f1 := function.Constant("first")
|
||||
f2 := function.Constant("last")
|
||||
seq := From(f1, f2)
|
||||
|
||||
getLast := function.Flow2(
|
||||
Last,
|
||||
O.Map(funcReader),
|
||||
)
|
||||
assert.Equal(t, O.Of(want), getLast(seq))
|
||||
})
|
||||
}
|
||||
|
||||
func funcReader(f func() string) string {
|
||||
return f()
|
||||
}
|
||||
|
||||
// TestLastWithChan tests Last with channels
|
||||
func TestLastWithChan(t *testing.T) {
|
||||
t.Run("return function", func(t *testing.T) {
|
||||
want := 30
|
||||
seq := From(intChan(10),
|
||||
intChan(20),
|
||||
intChan(want))
|
||||
|
||||
getLast := function.Flow2(
|
||||
Last,
|
||||
O.Map(chanReader[int]),
|
||||
)
|
||||
assert.Equal(t, O.Of(want), getLast(seq))
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func chanReader[T any](c <-chan T) T {
|
||||
return <-c
|
||||
}
|
||||
|
||||
func intChan(val int) <-chan int {
|
||||
ch := make(chan int, 1)
|
||||
ch <- val
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// TestLastWithChainedOperations tests Last with multiple chained operations
|
||||
func TestLastWithChainedOperations(t *testing.T) {
|
||||
t.Run("chains filter, map, and last", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, N.MoreThan(5))
|
||||
mapped := MonadMap(filtered, N.Mul(10))
|
||||
result := Last(mapped)
|
||||
assert.Equal(t, O.Of(100), result)
|
||||
})
|
||||
|
||||
t.Run("chains map and filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
filtered := MonadFilter(mapped, N.MoreThan(5))
|
||||
result := Last(filtered)
|
||||
assert.Equal(t, O.Of(10), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithReplicate tests Last with replicated values
|
||||
func TestLastWithReplicate(t *testing.T) {
|
||||
t.Run("returns last from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
|
||||
t.Run("returns None from zero replications", func(t *testing.T) {
|
||||
seq := Replicate(0, 42)
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithMakeBy tests Last with MakeBy
|
||||
func TestLastWithMakeBy(t *testing.T) {
|
||||
t.Run("returns last generated element", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.Of(16), last)
|
||||
})
|
||||
|
||||
t.Run("returns None for zero elements", func(t *testing.T) {
|
||||
seq := MakeBy(0, F.Identity[int])
|
||||
last := Last(seq)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithPrepend tests Last with Prepend
|
||||
func TestLastWithPrepend(t *testing.T) {
|
||||
t.Run("returns last element, not prepended", func(t *testing.T) {
|
||||
seq := From(2, 3, 4)
|
||||
prepended := Prepend(1)(seq)
|
||||
last := Last(prepended)
|
||||
assert.Equal(t, O.Of(4), last)
|
||||
})
|
||||
|
||||
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
prepended := Prepend(42)(seq)
|
||||
last := Last(prepended)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithAppend tests Last with Append
|
||||
func TestLastWithAppend(t *testing.T) {
|
||||
t.Run("returns appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
last := Last(appended)
|
||||
assert.Equal(t, O.Of(4), last)
|
||||
})
|
||||
|
||||
t.Run("returns appended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
appended := Append(42)(seq)
|
||||
last := Last(appended)
|
||||
assert.Equal(t, O.Of(42), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithChain tests Last with Chain (flatMap)
|
||||
func TestLastWithChain(t *testing.T) {
|
||||
t.Run("returns last from chained sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
last := Last(chained)
|
||||
assert.Equal(t, O.Of(30), last)
|
||||
})
|
||||
|
||||
t.Run("returns None when chain produces empty", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return Empty[int]()
|
||||
})
|
||||
last := Last(chained)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastWithFlatten tests Last with Flatten
|
||||
func TestLastWithFlatten(t *testing.T) {
|
||||
t.Run("returns last from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5))
|
||||
flattened := Flatten(nested)
|
||||
last := Last(flattened)
|
||||
assert.Equal(t, O.Of(5), last)
|
||||
})
|
||||
|
||||
t.Run("returns None from empty nested sequence", func(t *testing.T) {
|
||||
nested := Empty[Seq[int]]()
|
||||
flattened := Flatten(nested)
|
||||
last := Last(flattened)
|
||||
assert.Equal(t, O.None[int](), last)
|
||||
})
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleLast() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
last := Last(seq)
|
||||
|
||||
if value, ok := O.Unwrap(last); ok {
|
||||
fmt.Printf("Last element: %d\n", value)
|
||||
}
|
||||
// Output: Last element: 5
|
||||
}
|
||||
|
||||
func ExampleLast_empty() {
|
||||
seq := Empty[int]()
|
||||
last := Last(seq)
|
||||
|
||||
if _, ok := O.Unwrap(last); !ok {
|
||||
fmt.Println("Sequence is empty")
|
||||
}
|
||||
// Output: Sequence is empty
|
||||
}
|
||||
|
||||
func ExampleLast_functions() {
|
||||
f1 := function.Constant("first")
|
||||
f2 := function.Constant("middle")
|
||||
f3 := function.Constant("last")
|
||||
seq := From(f1, f2, f3)
|
||||
|
||||
last := Last(seq)
|
||||
|
||||
if fn, ok := O.Unwrap(last); ok {
|
||||
result := fn()
|
||||
fmt.Printf("Last function result: %s\n", result)
|
||||
}
|
||||
// Output: Last function result: last
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func FromLazy[A any](a Lazy[A]) Lazy[A] {
|
||||
}
|
||||
|
||||
// FromImpure converts a side effect without a return value into a side effect that returns any
|
||||
func FromImpure(f func()) Lazy[any] {
|
||||
func FromImpure(f func()) Lazy[Void] {
|
||||
return io.FromImpure(f)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package lazy
|
||||
|
||||
import "github.com/IBM/fp-go/v2/predicate"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
// Lazy represents a synchronous computation without side effects.
|
||||
@@ -63,4 +66,6 @@ type (
|
||||
// Predicate represents a function that tests a value of type A and returns a boolean.
|
||||
// It's commonly used for filtering and conditional operations.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -710,6 +710,146 @@ func TestTranscodeEither(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestTranscodeEitherValidation(t *testing.T) {
|
||||
t.Run("validates Left value with context", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
result := eitherCodec.Decode(either.Left[any, any](123)) // Invalid: should be string
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors)
|
||||
// Verify error contains type information
|
||||
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "string")
|
||||
})
|
||||
|
||||
t.Run("validates Right value with context", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
result := eitherCodec.Decode(either.Right[any, any]("not a number"))
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
assert.NotEmpty(t, errors)
|
||||
// Verify error contains type information
|
||||
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "int")
|
||||
})
|
||||
|
||||
t.Run("preserves Either structure on validation failure", func(t *testing.T) {
|
||||
eitherCodec := TranscodeEither(String(), Int())
|
||||
|
||||
// Left with wrong type
|
||||
leftResult := eitherCodec.Decode(either.Left[any, any]([]int{1, 2, 3}))
|
||||
assert.True(t, either.IsLeft(leftResult))
|
||||
|
||||
// Right with wrong type
|
||||
rightResult := eitherCodec.Decode(either.Right[any, any](true))
|
||||
assert.True(t, either.IsLeft(rightResult))
|
||||
})
|
||||
|
||||
t.Run("validates with custom codec that can fail", func(t *testing.T) {
|
||||
// Create a codec that only accepts positive integers
|
||||
positiveInt := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](fmt.Errorf("not a positive integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i <= 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
eitherCodec := TranscodeEither(String(), positiveInt)
|
||||
|
||||
// Valid positive integer
|
||||
validResult := eitherCodec.Decode(either.Right[any](42))
|
||||
assert.True(t, either.IsRight(validResult))
|
||||
|
||||
// Invalid: zero
|
||||
zeroResult := eitherCodec.Decode(either.Right[any](0))
|
||||
assert.True(t, either.IsLeft(zeroResult))
|
||||
|
||||
// Invalid: negative
|
||||
negativeResult := eitherCodec.Decode(either.Right[any](-5))
|
||||
assert.True(t, either.IsLeft(negativeResult))
|
||||
})
|
||||
|
||||
t.Run("validates both branches independently", func(t *testing.T) {
|
||||
// Create codecs with specific validation rules
|
||||
nonEmptyString := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok || len(s) == 0 {
|
||||
return either.Left[string](fmt.Errorf("not a non-empty string"))
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) == 0 {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
evenInt := MakeType(
|
||||
"EvenInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i%2 != 0 {
|
||||
return either.Left[int](fmt.Errorf("not an even integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(i int) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
if i%2 != 0 {
|
||||
return validation.FailureWithMessage[int](i, "must be even")(c)
|
||||
}
|
||||
return validation.Success(i)
|
||||
}
|
||||
},
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
eitherCodec := TranscodeEither(nonEmptyString, evenInt)
|
||||
|
||||
// Valid Left: non-empty string
|
||||
validLeft := eitherCodec.Decode(either.Left[int]("hello"))
|
||||
assert.True(t, either.IsRight(validLeft))
|
||||
|
||||
// Invalid Left: empty string
|
||||
invalidLeft := eitherCodec.Decode(either.Left[int](""))
|
||||
assert.True(t, either.IsLeft(invalidLeft))
|
||||
|
||||
// Valid Right: even integer
|
||||
validRight := eitherCodec.Decode(either.Right[string](42))
|
||||
assert.True(t, either.IsRight(validRight))
|
||||
|
||||
// Invalid Right: odd integer
|
||||
invalidRight := eitherCodec.Decode(either.Right[string](43))
|
||||
assert.True(t, either.IsLeft(invalidRight))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTranscodeEitherWithTransformation(t *testing.T) {
|
||||
// Create a codec that transforms strings to their lengths
|
||||
stringToLength := MakeType(
|
||||
|
||||
340
v2/optics/codec/codecs.go
Normal file
340
v2/optics/codec/codecs.go
Normal file
@@ -0,0 +1,340 @@
|
||||
// Copyright (c) 2024 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package codec provides pre-built codec implementations for common types.
|
||||
// This package includes codecs for URL parsing, date/time formatting, and other
|
||||
// standard data transformations that require bidirectional encoding/decoding.
|
||||
//
|
||||
// The codecs in this package follow functional programming principles and integrate
|
||||
// with the validation framework to provide type-safe, composable transformations.
|
||||
package codec
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// validateFromParser creates a validation function from a parser that may fail.
|
||||
// It wraps a parser function that returns (A, error) into a Validate[I, A] function
|
||||
// that integrates with the validation framework.
|
||||
//
|
||||
// The returned validation function:
|
||||
// - Calls the parser with the input value
|
||||
// - On success: returns a successful validation containing the parsed value
|
||||
// - On failure: returns a validation failure with the error message and cause
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type to parse into
|
||||
// - I: The input type to parse from
|
||||
//
|
||||
// Parameters:
|
||||
// - parser: A function that attempts to parse input I into type A, returning an error on failure
|
||||
//
|
||||
// Returns:
|
||||
// - A Validate[I, A] function that can be used in codec construction
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a validator for parsing integers from strings
|
||||
// intValidator := validateFromParser(strconv.Atoi)
|
||||
// // Use in a codec
|
||||
// intCodec := MakeType("Int", Is[int](), intValidator, strconv.Itoa)
|
||||
func validateFromParser[A, I any](parser func(I) (A, error)) Validate[I, A] {
|
||||
return func(i I) Decode[Context, A] {
|
||||
// Attempt to parse the input value
|
||||
a, err := parser(i)
|
||||
if err != nil {
|
||||
// On error, create a validation failure with the error details
|
||||
return validation.FailureWithError[A](i, err.Error())(err)
|
||||
}
|
||||
// On success, wrap the parsed value in a successful validation
|
||||
return reader.Of[Context](validation.Success(a))
|
||||
}
|
||||
}
|
||||
|
||||
// URL creates a bidirectional codec for URL parsing and formatting.
|
||||
// This codec can parse strings into *url.URL and encode *url.URL back to strings.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string using url.Parse, validating URL syntax
|
||||
// - Encodes: Converts a *url.URL to its string representation using String()
|
||||
// - Validates: Ensures the input string is a valid URL format
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[*url.URL, string, string] codec that handles URL transformations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// urlCodec := URL()
|
||||
//
|
||||
// // Decode a string to URL
|
||||
// validation := urlCodec.Decode("https://example.com/path?query=value")
|
||||
// // validation is Right(*url.URL{...})
|
||||
//
|
||||
// // Encode a URL to string
|
||||
// u, _ := url.Parse("https://example.com")
|
||||
// str := urlCodec.Encode(u)
|
||||
// // str is "https://example.com"
|
||||
//
|
||||
// // Invalid URL fails validation
|
||||
// validation := urlCodec.Decode("not a valid url")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func URL() Type[*url.URL, string, string] {
|
||||
return MakeType(
|
||||
"URL",
|
||||
Is[*url.URL](),
|
||||
validateFromParser(url.Parse),
|
||||
(*url.URL).String,
|
||||
)
|
||||
}
|
||||
|
||||
// Date creates a bidirectional codec for date/time parsing and formatting with a specific layout.
|
||||
// This codec uses Go's time.Parse and time.Format with the provided layout string.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string into time.Time using the specified layout
|
||||
// - Encodes: Formats a time.Time back to a string using the same layout
|
||||
// - Validates: Ensures the input string matches the expected date/time format
|
||||
//
|
||||
// Parameters:
|
||||
// - layout: The time layout string (e.g., "2006-01-02", time.RFC3339)
|
||||
// See time package documentation for layout format details
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[time.Time, string, string] codec that handles date/time transformations
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for ISO 8601 dates
|
||||
// dateCodec := Date("2006-01-02")
|
||||
//
|
||||
// // Decode a string to time.Time
|
||||
// validation := dateCodec.Decode("2024-03-15")
|
||||
// // validation is Right(time.Time{...})
|
||||
//
|
||||
// // Encode a time.Time to string
|
||||
// t := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
// str := dateCodec.Encode(t)
|
||||
// // str is "2024-03-15"
|
||||
//
|
||||
// // Create a codec for RFC3339 timestamps
|
||||
// timestampCodec := Date(time.RFC3339)
|
||||
// validation := timestampCodec.Decode("2024-03-15T10:30:00Z")
|
||||
//
|
||||
// // Invalid format fails validation
|
||||
// validation := dateCodec.Decode("15-03-2024")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func Date(layout string) Type[time.Time, string, string] {
|
||||
return MakeType(
|
||||
"Date",
|
||||
Is[time.Time](),
|
||||
validateFromParser(func(s string) (time.Time, error) { return time.Parse(layout, s) }),
|
||||
F.Bind2nd(time.Time.Format, layout),
|
||||
)
|
||||
}
|
||||
|
||||
// Regex creates a bidirectional codec for regex pattern matching with capture groups.
|
||||
// This codec can match strings against a regular expression pattern and extract capture groups,
|
||||
// then reconstruct the original string from the match data.
|
||||
//
|
||||
// The codec uses prism.Match which contains:
|
||||
// - Before: Text before the match
|
||||
// - Groups: Capture groups (index 0 is the full match, 1+ are numbered capture groups)
|
||||
// - After: Text after the match
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Attempts to match the regex against the input string
|
||||
// - Encodes: Reconstructs the original string from a Match structure
|
||||
// - Validates: Ensures the string matches the regex pattern
|
||||
//
|
||||
// Parameters:
|
||||
// - re: A compiled regular expression pattern
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[prism.Match, string, string] codec that handles regex matching
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for matching numbers in text
|
||||
// numberRegex := regexp.MustCompile(`\d+`)
|
||||
// numberCodec := Regex(numberRegex)
|
||||
//
|
||||
// // Decode a string with a number
|
||||
// validation := numberCodec.Decode("Price: 42 dollars")
|
||||
// // validation is Right(Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"})
|
||||
//
|
||||
// // Encode a Match back to string
|
||||
// match := prism.Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"}
|
||||
// str := numberCodec.Encode(match)
|
||||
// // str is "Price: 42 dollars"
|
||||
//
|
||||
// // Non-matching string fails validation
|
||||
// validation := numberCodec.Decode("no numbers here")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func Regex(re *regexp.Regexp) Type[prism.Match, string, string] {
|
||||
return FromRefinement(prism.RegexMatcher(re))
|
||||
}
|
||||
|
||||
// RegexNamed creates a bidirectional codec for regex pattern matching with named capture groups.
|
||||
// This codec can match strings against a regular expression with named groups and extract them
|
||||
// by name, then reconstruct the original string from the match data.
|
||||
//
|
||||
// The codec uses prism.NamedMatch which contains:
|
||||
// - Before: Text before the match
|
||||
// - Groups: Map of named capture groups (name -> matched text)
|
||||
// - Full: The complete matched text
|
||||
// - After: Text after the match
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Attempts to match the regex against the input string
|
||||
// - Encodes: Reconstructs the original string from a NamedMatch structure
|
||||
// - Validates: Ensures the string matches the regex pattern with named groups
|
||||
//
|
||||
// Parameters:
|
||||
// - re: A compiled regular expression with named capture groups (e.g., `(?P<name>pattern)`)
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[prism.NamedMatch, string, string] codec that handles named regex matching
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for matching email addresses with named groups
|
||||
// emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
|
||||
// emailCodec := RegexNamed(emailRegex)
|
||||
//
|
||||
// // Decode an email string
|
||||
// validation := emailCodec.Decode("john@example.com")
|
||||
// // validation is Right(NamedMatch{
|
||||
// // Before: "",
|
||||
// // Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
// // Full: "john@example.com",
|
||||
// // After: ""
|
||||
// // })
|
||||
//
|
||||
// // Encode a NamedMatch back to string
|
||||
// match := prism.NamedMatch{
|
||||
// Before: "",
|
||||
// Groups: map[string]string{"user": "john", "domain": "example.com"},
|
||||
// Full: "john@example.com",
|
||||
// After: "",
|
||||
// }
|
||||
// str := emailCodec.Encode(match)
|
||||
// // str is "john@example.com"
|
||||
//
|
||||
// // Non-matching string fails validation
|
||||
// validation := emailCodec.Decode("not-an-email")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func RegexNamed(re *regexp.Regexp) Type[prism.NamedMatch, string, string] {
|
||||
return FromRefinement(prism.RegexNamedMatcher(re))
|
||||
}
|
||||
|
||||
// IntFromString creates a bidirectional codec for parsing integers from strings.
|
||||
// This codec converts string representations of integers to int values and vice versa.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string to an int using strconv.Atoi
|
||||
// - Encodes: Converts an int to its string representation using strconv.Itoa
|
||||
// - Validates: Ensures the string contains a valid integer (base 10)
|
||||
//
|
||||
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
|
||||
// It does not accept hexadecimal, octal, or other number formats.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[int, string, string] codec that handles int/string conversions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// intCodec := IntFromString()
|
||||
//
|
||||
// // Decode a valid integer string
|
||||
// validation := intCodec.Decode("42")
|
||||
// // validation is Right(42)
|
||||
//
|
||||
// // Decode negative integer
|
||||
// validation := intCodec.Decode("-123")
|
||||
// // validation is Right(-123)
|
||||
//
|
||||
// // Encode an integer to string
|
||||
// str := intCodec.Encode(42)
|
||||
// // str is "42"
|
||||
//
|
||||
// // Invalid integer string fails validation
|
||||
// validation := intCodec.Decode("not a number")
|
||||
// // validation is Left(ValidationError{...})
|
||||
//
|
||||
// // Floating point fails validation
|
||||
// validation := intCodec.Decode("3.14")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func IntFromString() Type[int, string, string] {
|
||||
return MakeType(
|
||||
"IntFromString",
|
||||
Is[int](),
|
||||
validateFromParser(strconv.Atoi),
|
||||
strconv.Itoa,
|
||||
)
|
||||
}
|
||||
|
||||
// Int64FromString creates a bidirectional codec for parsing 64-bit integers from strings.
|
||||
// This codec converts string representations of integers to int64 values and vice versa.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Parses a string to an int64 using strconv.ParseInt with base 10
|
||||
// - Encodes: Converts an int64 to its string representation
|
||||
// - Validates: Ensures the string contains a valid 64-bit integer (base 10)
|
||||
//
|
||||
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
|
||||
// It supports the full range of int64 values (-9223372036854775808 to 9223372036854775807).
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[int64, string, string] codec that handles int64/string conversions
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// int64Codec := Int64FromString()
|
||||
//
|
||||
// // Decode a valid integer string
|
||||
// validation := int64Codec.Decode("9223372036854775807")
|
||||
// // validation is Right(9223372036854775807)
|
||||
//
|
||||
// // Decode negative integer
|
||||
// validation := int64Codec.Decode("-9223372036854775808")
|
||||
// // validation is Right(-9223372036854775808)
|
||||
//
|
||||
// // Encode an int64 to string
|
||||
// str := int64Codec.Encode(42)
|
||||
// // str is "42"
|
||||
//
|
||||
// // Invalid integer string fails validation
|
||||
// validation := int64Codec.Decode("not a number")
|
||||
// // validation is Left(ValidationError{...})
|
||||
//
|
||||
// // Out of range value fails validation
|
||||
// validation := int64Codec.Decode("9223372036854775808")
|
||||
// // validation is Left(ValidationError{...})
|
||||
func Int64FromString() Type[int64, string, string] {
|
||||
return MakeType(
|
||||
"Int64FromString",
|
||||
Is[int64](),
|
||||
validateFromParser(func(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) }),
|
||||
prism.ParseInt64().ReverseGet,
|
||||
)
|
||||
}
|
||||
908
v2/optics/codec/codecs_test.go
Normal file
908
v2/optics/codec/codecs_test.go
Normal file
@@ -0,0 +1,908 @@
|
||||
// Copyright (c) 2024 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package codec
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestURL(t *testing.T) {
|
||||
urlCodec := URL()
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, *url.URL](nil))
|
||||
|
||||
t.Run("decodes valid HTTP URL", func(t *testing.T) {
|
||||
result := urlCodec.Decode("https://example.com/path?query=value")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode valid URL")
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "https", parsedURL.Scheme)
|
||||
assert.Equal(t, "example.com", parsedURL.Host)
|
||||
assert.Equal(t, "/path", parsedURL.Path)
|
||||
assert.Equal(t, "query=value", parsedURL.RawQuery)
|
||||
})
|
||||
|
||||
t.Run("decodes valid HTTP URL without path", func(t *testing.T) {
|
||||
result := urlCodec.Decode("https://example.com")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "https", parsedURL.Scheme)
|
||||
assert.Equal(t, "example.com", parsedURL.Host)
|
||||
})
|
||||
|
||||
t.Run("decodes URL with port", func(t *testing.T) {
|
||||
result := urlCodec.Decode("http://localhost:8080/api")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "http", parsedURL.Scheme)
|
||||
assert.Equal(t, "localhost:8080", parsedURL.Host)
|
||||
assert.Equal(t, "/api", parsedURL.Path)
|
||||
})
|
||||
|
||||
t.Run("decodes URL with fragment", func(t *testing.T) {
|
||||
result := urlCodec.Decode("https://example.com/page#section")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "section", parsedURL.Fragment)
|
||||
})
|
||||
|
||||
t.Run("decodes relative URL", func(t *testing.T) {
|
||||
result := urlCodec.Decode("/path/to/resource")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedURL := getOrElseNull(result)
|
||||
|
||||
require.NotNil(t, parsedURL)
|
||||
assert.Equal(t, "/path/to/resource", parsedURL.Path)
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid URL", func(t *testing.T) {
|
||||
result := urlCodec.Decode("not a valid url ://")
|
||||
|
||||
assert.True(t, either.IsLeft(result), "should fail to decode invalid URL")
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(*url.URL) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode URL with invalid characters", func(t *testing.T) {
|
||||
result := urlCodec.Decode("http://example.com/path with spaces")
|
||||
|
||||
// Note: url.Parse actually handles spaces, so let's test a truly invalid URL
|
||||
result = urlCodec.Decode("ht!tp://invalid")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes URL to string", func(t *testing.T) {
|
||||
parsedURL, err := url.Parse("https://example.com/path?query=value")
|
||||
require.NoError(t, err)
|
||||
|
||||
encoded := urlCodec.Encode(parsedURL)
|
||||
|
||||
assert.Equal(t, "https://example.com/path?query=value", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes URL with fragment", func(t *testing.T) {
|
||||
parsedURL, err := url.Parse("https://example.com/page#section")
|
||||
require.NoError(t, err)
|
||||
|
||||
encoded := urlCodec.Encode(parsedURL)
|
||||
|
||||
assert.Equal(t, "https://example.com/page#section", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "https://example.com/path?key=value&foo=bar#fragment"
|
||||
|
||||
// Decode
|
||||
decodeResult := urlCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
parsedURL := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := urlCodec.Encode(parsedURL)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
assert.Equal(t, "URL", urlCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDate(t *testing.T) {
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, time.Time](time.Time{}))
|
||||
|
||||
t.Run("ISO 8601 date format", func(t *testing.T) {
|
||||
dateCodec := Date("2006-01-02")
|
||||
|
||||
t.Run("decodes valid date", func(t *testing.T) {
|
||||
result := dateCodec.Decode("2024-03-15")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedDate := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedDate.Year())
|
||||
assert.Equal(t, time.March, parsedDate.Month())
|
||||
assert.Equal(t, 15, parsedDate.Day())
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid date format", func(t *testing.T) {
|
||||
result := dateCodec.Decode("15-03-2024")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(time.Time) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid date", func(t *testing.T) {
|
||||
result := dateCodec.Decode("2024-13-45")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-date string", func(t *testing.T) {
|
||||
result := dateCodec.Decode("not a date")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes date to string", func(t *testing.T) {
|
||||
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
encoded := dateCodec.Encode(date)
|
||||
|
||||
assert.Equal(t, "2024-03-15", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "2024-12-25"
|
||||
|
||||
// Decode
|
||||
decodeResult := dateCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
parsedDate := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := dateCodec.Encode(parsedDate)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("RFC3339 timestamp format", func(t *testing.T) {
|
||||
timestampCodec := Date(time.RFC3339)
|
||||
|
||||
t.Run("decodes valid RFC3339 timestamp", func(t *testing.T) {
|
||||
result := timestampCodec.Decode("2024-03-15T10:30:00Z")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedTime := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedTime.Year())
|
||||
assert.Equal(t, time.March, parsedTime.Month())
|
||||
assert.Equal(t, 15, parsedTime.Day())
|
||||
assert.Equal(t, 10, parsedTime.Hour())
|
||||
assert.Equal(t, 30, parsedTime.Minute())
|
||||
assert.Equal(t, 0, parsedTime.Second())
|
||||
})
|
||||
|
||||
t.Run("decodes RFC3339 with timezone offset", func(t *testing.T) {
|
||||
result := timestampCodec.Decode("2024-03-15T10:30:00+01:00")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedTime := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedTime.Year())
|
||||
assert.Equal(t, time.March, parsedTime.Month())
|
||||
assert.Equal(t, 15, parsedTime.Day())
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid RFC3339", func(t *testing.T) {
|
||||
result := timestampCodec.Decode("2024-03-15 10:30:00")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes timestamp to RFC3339 string", func(t *testing.T) {
|
||||
timestamp := time.Date(2024, 3, 15, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
encoded := timestampCodec.Encode(timestamp)
|
||||
|
||||
assert.Equal(t, "2024-03-15T10:30:00Z", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "2024-12-25T15:45:30Z"
|
||||
|
||||
// Decode
|
||||
decodeResult := timestampCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
parsedTime := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := timestampCodec.Encode(parsedTime)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("custom date format", func(t *testing.T) {
|
||||
customCodec := Date("02/01/2006")
|
||||
|
||||
t.Run("decodes custom format", func(t *testing.T) {
|
||||
result := customCodec.Decode("15/03/2024")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
parsedDate := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, 2024, parsedDate.Year())
|
||||
assert.Equal(t, time.March, parsedDate.Month())
|
||||
assert.Equal(t, 15, parsedDate.Day())
|
||||
})
|
||||
|
||||
t.Run("encodes to custom format", func(t *testing.T) {
|
||||
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
encoded := customCodec.Encode(date)
|
||||
|
||||
assert.Equal(t, "15/03/2024", encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
dateCodec := Date("2006-01-02")
|
||||
assert.Equal(t, "Date", dateCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateFromParser(t *testing.T) {
|
||||
t.Run("successful parsing", func(t *testing.T) {
|
||||
// Create a simple parser that always succeeds
|
||||
parser := func(s string) (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
|
||||
validator := validateFromParser(parser)
|
||||
decode := validator("test")
|
||||
|
||||
// Execute with empty context
|
||||
result := decode(validation.Context{})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("failed parsing", func(t *testing.T) {
|
||||
// Create a parser that always fails
|
||||
parser := func(s string) (int, error) {
|
||||
return 0, assert.AnError
|
||||
}
|
||||
|
||||
validator := validateFromParser(parser)
|
||||
decode := validator("test")
|
||||
|
||||
// Execute with empty context
|
||||
result := decode(validation.Context{})
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
|
||||
// Check that the error contains the input value
|
||||
if len(errors) > 0 {
|
||||
assert.Equal(t, "test", errors[0].Value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parser with context", func(t *testing.T) {
|
||||
parser := func(s string) (string, error) {
|
||||
if s == "" {
|
||||
return "", assert.AnError
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
validator := validateFromParser(parser)
|
||||
|
||||
// Test with context
|
||||
ctx := validation.Context{
|
||||
{Key: "field", Type: "string"},
|
||||
}
|
||||
|
||||
decode := validator("")
|
||||
result := decode(ctx)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(string) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
|
||||
// Verify context is preserved
|
||||
if len(errors) > 0 {
|
||||
assert.Equal(t, ctx, errors[0].Context)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegex(t *testing.T) {
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.Match{}))
|
||||
|
||||
t.Run("simple number pattern", func(t *testing.T) {
|
||||
numberRegex := regexp.MustCompile(`\d+`)
|
||||
regexCodec := Regex(numberRegex)
|
||||
|
||||
t.Run("decodes string with number", func(t *testing.T) {
|
||||
result := regexCodec.Decode("Price: 42 dollars")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "Price: ", match.Before)
|
||||
assert.Equal(t, []string{"42"}, match.Groups)
|
||||
assert.Equal(t, " dollars", match.After)
|
||||
})
|
||||
|
||||
t.Run("decodes number at start", func(t *testing.T) {
|
||||
result := regexCodec.Decode("123 items")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "", match.Before)
|
||||
assert.Equal(t, []string{"123"}, match.Groups)
|
||||
assert.Equal(t, " items", match.After)
|
||||
})
|
||||
|
||||
t.Run("decodes number at end", func(t *testing.T) {
|
||||
result := regexCodec.Decode("Total: 999")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "Total: ", match.Before)
|
||||
assert.Equal(t, []string{"999"}, match.Groups)
|
||||
assert.Equal(t, "", match.After)
|
||||
})
|
||||
|
||||
t.Run("fails to decode string without number", func(t *testing.T) {
|
||||
result := regexCodec.Decode("no numbers here")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(prism.Match) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("encodes Match to string", func(t *testing.T) {
|
||||
match := prism.Match{
|
||||
Before: "Price: ",
|
||||
Groups: []string{"42"},
|
||||
After: " dollars",
|
||||
}
|
||||
|
||||
encoded := regexCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, "Price: 42 dollars", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "Count: 789 items"
|
||||
|
||||
// Decode
|
||||
decodeResult := regexCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
match := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := regexCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("pattern with capture groups", func(t *testing.T) {
|
||||
// Pattern to match word followed by number
|
||||
wordNumberRegex := regexp.MustCompile(`(\w+)(\d+)`)
|
||||
regexCodec := Regex(wordNumberRegex)
|
||||
|
||||
t.Run("decodes with capture groups", func(t *testing.T) {
|
||||
result := regexCodec.Decode("item42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "", match.Before)
|
||||
// Groups contains the full match and capture groups
|
||||
require.NotEmpty(t, match.Groups)
|
||||
assert.Equal(t, "item42", match.Groups[0])
|
||||
// Verify we have capture groups
|
||||
if len(match.Groups) > 1 {
|
||||
assert.Contains(t, match.Groups[1], "item")
|
||||
assert.Contains(t, match.Groups[len(match.Groups)-1], "2")
|
||||
}
|
||||
assert.Equal(t, "", match.After)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("codec name contains pattern info", func(t *testing.T) {
|
||||
numberRegex := regexp.MustCompile(`\d+`)
|
||||
regexCodec := Regex(numberRegex)
|
||||
// The name is generated by FromRefinement and includes the pattern
|
||||
assert.Contains(t, regexCodec.Name(), "FromRefinement")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegexNamed(t *testing.T) {
|
||||
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.NamedMatch{}))
|
||||
|
||||
t.Run("email pattern with named groups", func(t *testing.T) {
|
||||
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
|
||||
emailCodec := RegexNamed(emailRegex)
|
||||
|
||||
t.Run("decodes valid email", func(t *testing.T) {
|
||||
result := emailCodec.Decode("john@example.com")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "", match.Before)
|
||||
assert.Equal(t, "john@example.com", match.Full)
|
||||
assert.Equal(t, "", match.After)
|
||||
require.NotNil(t, match.Groups)
|
||||
assert.Equal(t, "john", match.Groups["user"])
|
||||
assert.Equal(t, "example.com", match.Groups["domain"])
|
||||
})
|
||||
|
||||
t.Run("decodes email with surrounding text", func(t *testing.T) {
|
||||
result := emailCodec.Decode("Contact: alice@test.org for info")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "Contact: ", match.Before)
|
||||
assert.Equal(t, "alice@test.org", match.Full)
|
||||
assert.Equal(t, " for info", match.After)
|
||||
assert.Equal(t, "alice", match.Groups["user"])
|
||||
assert.Equal(t, "test.org", match.Groups["domain"])
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid email", func(t *testing.T) {
|
||||
result := emailCodec.Decode("not-an-email")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(prism.NamedMatch) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("encodes NamedMatch to string", func(t *testing.T) {
|
||||
match := prism.NamedMatch{
|
||||
Before: "Email: ",
|
||||
Groups: map[string]string{"user": "bob", "domain": "example.com"},
|
||||
Full: "bob@example.com",
|
||||
After: "",
|
||||
}
|
||||
|
||||
encoded := emailCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, "Email: bob@example.com", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "Contact: support@company.io"
|
||||
|
||||
// Decode
|
||||
decodeResult := emailCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
match := getOrElseNull(decodeResult)
|
||||
|
||||
// Encode
|
||||
encoded := emailCodec.Encode(match)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("phone pattern with named groups", func(t *testing.T) {
|
||||
phoneRegex := regexp.MustCompile(`(?P<area>\d{3})-(?P<prefix>\d{3})-(?P<line>\d{4})`)
|
||||
phoneCodec := RegexNamed(phoneRegex)
|
||||
|
||||
t.Run("decodes valid phone number", func(t *testing.T) {
|
||||
result := phoneCodec.Decode("555-123-4567")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
match := getOrElseNull(result)
|
||||
|
||||
assert.Equal(t, "555-123-4567", match.Full)
|
||||
assert.Equal(t, "555", match.Groups["area"])
|
||||
assert.Equal(t, "123", match.Groups["prefix"])
|
||||
assert.Equal(t, "4567", match.Groups["line"])
|
||||
})
|
||||
|
||||
t.Run("fails to decode invalid phone format", func(t *testing.T) {
|
||||
result := phoneCodec.Decode("123-45-6789")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("codec name contains refinement info", func(t *testing.T) {
|
||||
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
|
||||
emailCodec := RegexNamed(emailRegex)
|
||||
// The name is generated by FromRefinement
|
||||
assert.Contains(t, emailCodec.Name(), "FromRefinement")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntFromString(t *testing.T) {
|
||||
intCodec := IntFromString()
|
||||
|
||||
t.Run("decodes positive integer", func(t *testing.T) {
|
||||
result := intCodec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("decodes negative integer", func(t *testing.T) {
|
||||
result := intCodec.Decode("-123")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, -123, value)
|
||||
})
|
||||
|
||||
t.Run("decodes zero", func(t *testing.T) {
|
||||
result := intCodec.Decode("0")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 0, value)
|
||||
})
|
||||
|
||||
t.Run("decodes integer with plus sign", func(t *testing.T) {
|
||||
result := intCodec.Decode("+456")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
assert.Equal(t, 456, value)
|
||||
})
|
||||
|
||||
t.Run("fails to decode floating point", func(t *testing.T) {
|
||||
result := intCodec.Decode("3.14")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-numeric string", func(t *testing.T) {
|
||||
result := intCodec.Decode("not a number")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode empty string", func(t *testing.T) {
|
||||
result := intCodec.Decode("")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode hexadecimal", func(t *testing.T) {
|
||||
result := intCodec.Decode("0xFF")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode with whitespace", func(t *testing.T) {
|
||||
result := intCodec.Decode(" 42 ")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes positive integer", func(t *testing.T) {
|
||||
encoded := intCodec.Encode(42)
|
||||
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes negative integer", func(t *testing.T) {
|
||||
encoded := intCodec.Encode(-123)
|
||||
|
||||
assert.Equal(t, "-123", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes zero", func(t *testing.T) {
|
||||
encoded := intCodec.Encode(0)
|
||||
|
||||
assert.Equal(t, "0", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "9876"
|
||||
|
||||
// Decode
|
||||
decodeResult := intCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
value := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := intCodec.Encode(value)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
assert.Equal(t, "IntFromString", intCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
func TestInt64FromString(t *testing.T) {
|
||||
int64Codec := Int64FromString()
|
||||
|
||||
t.Run("decodes positive int64", func(t *testing.T) {
|
||||
result := int64Codec.Decode("9223372036854775807")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(9223372036854775807), value)
|
||||
})
|
||||
|
||||
t.Run("decodes negative int64", func(t *testing.T) {
|
||||
result := int64Codec.Decode("-9223372036854775808")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(-9223372036854775808), value)
|
||||
})
|
||||
|
||||
t.Run("decodes zero", func(t *testing.T) {
|
||||
result := int64Codec.Decode("0")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return -1 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(0), value)
|
||||
})
|
||||
|
||||
t.Run("decodes small int64", func(t *testing.T) {
|
||||
result := int64Codec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
assert.Equal(t, int64(42), value)
|
||||
})
|
||||
|
||||
t.Run("fails to decode out of range positive", func(t *testing.T) {
|
||||
result := int64Codec.Decode("9223372036854775808")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(int64) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
|
||||
t.Run("fails to decode out of range negative", func(t *testing.T) {
|
||||
result := int64Codec.Decode("-9223372036854775809")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode floating point", func(t *testing.T) {
|
||||
result := int64Codec.Decode("3.14")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-numeric string", func(t *testing.T) {
|
||||
result := int64Codec.Decode("not a number")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("fails to decode empty string", func(t *testing.T) {
|
||||
result := int64Codec.Decode("")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("encodes positive int64", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(9223372036854775807)
|
||||
|
||||
assert.Equal(t, "9223372036854775807", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes negative int64", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(-9223372036854775808)
|
||||
|
||||
assert.Equal(t, "-9223372036854775808", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes zero", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(0)
|
||||
|
||||
assert.Equal(t, "0", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes small int64", func(t *testing.T) {
|
||||
encoded := int64Codec.Encode(42)
|
||||
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip encoding and decoding", func(t *testing.T) {
|
||||
original := "1234567890123456"
|
||||
|
||||
// Decode
|
||||
decodeResult := int64Codec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
value := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) int64 { return 0 },
|
||||
F.Identity[int64],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := int64Codec.Encode(value)
|
||||
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("codec has correct name", func(t *testing.T) {
|
||||
assert.Equal(t, "Int64FromString", int64Codec.Name())
|
||||
})
|
||||
}
|
||||
321
v2/optics/codec/decode/OrElse_ChainLeft_explanation.md
Normal file
321
v2/optics/codec/decode/OrElse_ChainLeft_explanation.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# ChainLeft and OrElse in the Decode Package
|
||||
|
||||
## Overview
|
||||
|
||||
In [`optics/codec/decode/monad.go`](monad.go:53-62), the [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) functions work with decoders that may fail during decoding operations.
|
||||
|
||||
```go
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Insight: OrElse is ChainLeft
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**.
|
||||
|
||||
## Understanding the Types
|
||||
|
||||
### Decode[I, A]
|
||||
A decoder that takes input of type `I` and produces a `Validation[A]`:
|
||||
```go
|
||||
type Decode[I, A any] = func(I) Validation[A]
|
||||
```
|
||||
|
||||
### Kleisli[I, Errors, A]
|
||||
A function that takes `Errors` and produces a `Decode[I, A]`:
|
||||
```go
|
||||
type Kleisli[I, Errors, A] = func(Errors) Decode[I, A]
|
||||
```
|
||||
|
||||
This allows error handlers to:
|
||||
1. Access the validation errors that occurred
|
||||
2. Access the original input (via the returned Decode function)
|
||||
3. Either recover with a success value or produce new errors
|
||||
|
||||
### Operator[I, A, A]
|
||||
A function that transforms one decoder into another:
|
||||
```go
|
||||
type Operator[I, A, A] = func(Decode[I, A]) Decode[I, A]
|
||||
```
|
||||
|
||||
## Core Behavior
|
||||
|
||||
Both [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) delegate to [`validation.ChainLeft`](../validation/monad.go:304), which provides:
|
||||
|
||||
### 1. Error Aggregation
|
||||
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid:
|
||||
|
||||
```go
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "original error"},
|
||||
})
|
||||
}
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "additional error"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(failingDecoder)
|
||||
result := decoder("input")
|
||||
// Result contains BOTH errors: ["original error", "additional error"]
|
||||
```
|
||||
|
||||
### 2. Success Pass-Through
|
||||
Success values pass through unchanged - the handler is never called:
|
||||
|
||||
```go
|
||||
successDecoder := Of[string](42)
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "never called"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(successDecoder)
|
||||
result := decoder("input")
|
||||
// Result: Success(42) - unchanged
|
||||
```
|
||||
|
||||
### 3. Error Recovery
|
||||
The handler can recover from failures by returning a successful decoder:
|
||||
|
||||
```go
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not found"},
|
||||
})
|
||||
}
|
||||
|
||||
recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "not found" {
|
||||
return Of[string](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
|
||||
decoder := recoverFromNotFound(failingDecoder)
|
||||
result := decoder("input")
|
||||
// Result: Success(0) - recovered from failure
|
||||
```
|
||||
|
||||
### 4. Access to Original Input
|
||||
The handler returns a `Decode[I, A]` function, giving it access to the original input:
|
||||
|
||||
```go
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
// Can access both errs and input here
|
||||
if input == "special" {
|
||||
return validation.Of(999)
|
||||
}
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Fallback Decoding (OrElse reads better)
|
||||
|
||||
```go
|
||||
// Primary decoder that may fail
|
||||
primaryDecoder := func(input string) Validation[int] {
|
||||
n, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not a valid integer"},
|
||||
})
|
||||
}
|
||||
return validation.Of(n)
|
||||
}
|
||||
|
||||
// Use OrElse for semantic clarity - "try primary, or else use default"
|
||||
withDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return Of[string](0) // default to 0 if decoding fails
|
||||
})
|
||||
|
||||
decoder := withDefault(primaryDecoder)
|
||||
|
||||
result1 := decoder("42") // Success(42)
|
||||
result2 := decoder("abc") // Success(0) - fallback
|
||||
```
|
||||
|
||||
### 2. Error Context Addition (ChainLeft reads better)
|
||||
|
||||
```go
|
||||
decodeUserAge := func(data map[string]any) Validation[int] {
|
||||
age, ok := data["age"].(int)
|
||||
if !ok {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: data["age"], Messsage: "invalid type"},
|
||||
})
|
||||
}
|
||||
return validation.Of(age)
|
||||
}
|
||||
|
||||
// Use ChainLeft when emphasizing error transformation
|
||||
addContext := ChainLeft(func(errs Errors) Decode[map[string]any, int] {
|
||||
return func(data map[string]any) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to decode user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := addContext(decodeUserAge)
|
||||
// Errors will include both original error and context
|
||||
```
|
||||
|
||||
### 3. Conditional Recovery Based on Input
|
||||
|
||||
```go
|
||||
decodePort := func(input string) Validation[int] {
|
||||
port, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
return validation.Of(port)
|
||||
}
|
||||
|
||||
// Recover with different defaults based on input
|
||||
smartDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
// Check input to determine appropriate default
|
||||
if strings.Contains(input, "http") {
|
||||
return validation.Of(80)
|
||||
}
|
||||
if strings.Contains(input, "https") {
|
||||
return validation.Of(443)
|
||||
}
|
||||
return validation.Of(8080)
|
||||
}
|
||||
})
|
||||
|
||||
decoder := smartDefault(decodePort)
|
||||
result1 := decoder("http-server") // Success(80)
|
||||
result2 := decoder("https-server") // Success(443)
|
||||
result3 := decoder("other") // Success(8080)
|
||||
```
|
||||
|
||||
### 4. Pipeline Composition
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
DatabaseURL string
|
||||
}
|
||||
|
||||
decodeConfig := func(data map[string]any) Validation[Config] {
|
||||
url, ok := data["db_url"].(string)
|
||||
if !ok {
|
||||
return either.Left[Config](validation.Errors{
|
||||
{Messsage: "missing db_url"},
|
||||
})
|
||||
}
|
||||
return validation.Of(Config{DatabaseURL: url})
|
||||
}
|
||||
|
||||
// Build a pipeline with multiple error handlers
|
||||
decoder := F.Pipe2(
|
||||
decodeConfig,
|
||||
OrElse(func(errs Errors) Decode[map[string]any, Config] {
|
||||
// Try environment variable as fallback
|
||||
return func(data map[string]any) Validation[Config] {
|
||||
if url := os.Getenv("DATABASE_URL"); url != "" {
|
||||
return validation.Of(Config{DatabaseURL: url})
|
||||
}
|
||||
return either.Left[Config](errs)
|
||||
}
|
||||
}),
|
||||
OrElse(func(errs Errors) Decode[map[string]any, Config] {
|
||||
// Final fallback to default
|
||||
return Of[map[string]any](Config{
|
||||
DatabaseURL: "localhost:5432",
|
||||
})
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
## Comparison with validation.ChainLeft
|
||||
|
||||
The decode package's [`ChainLeft`](monad.go:53) wraps [`validation.ChainLeft`](../validation/monad.go:304) using the Reader transformer pattern:
|
||||
|
||||
| Aspect | validation.ChainLeft | decode.ChainLeft |
|
||||
|--------|---------------------|------------------|
|
||||
| **Input** | `Validation[A]` | `Decode[I, A]` (function) |
|
||||
| **Handler** | `func(Errors) Validation[A]` | `func(Errors) Decode[I, A]` |
|
||||
| **Output** | `Validation[A]` | `Decode[I, A]` (function) |
|
||||
| **Context** | No input access | Access to original input `I` |
|
||||
| **Use Case** | Pure validation logic | Decoding with input-dependent recovery |
|
||||
|
||||
The key difference is that decode's version gives handlers access to the original input through the returned `Decode[I, A]` function.
|
||||
|
||||
## When to Use Which Name
|
||||
|
||||
### Use **OrElse** when:
|
||||
- Emphasizing fallback/alternative decoding logic
|
||||
- Providing default values on decode failure
|
||||
- The intent is "try this, or else try that"
|
||||
- Code reads more naturally with "or else"
|
||||
|
||||
### Use **ChainLeft** when:
|
||||
- Emphasizing technical error channel transformation
|
||||
- Adding context or enriching error information
|
||||
- The focus is on error handling mechanics
|
||||
- Working with other functional programming concepts
|
||||
|
||||
## Verification
|
||||
|
||||
The test suite in [`monad_test.go`](monad_test.go:385) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
|
||||
|
||||
- ✅ Identical behavior for Success values
|
||||
- ✅ Identical behavior for error recovery
|
||||
- ✅ Identical behavior for error aggregation
|
||||
- ✅ Identical behavior in pipeline composition
|
||||
- ✅ Identical behavior for multiple error scenarios
|
||||
- ✅ Both provide access to original input
|
||||
|
||||
Run the tests:
|
||||
```bash
|
||||
go test -v -run "TestChainLeft|TestOrElse" ./optics/codec/decode
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** in the decode package - they are aliases with identical implementations and behavior. Both:
|
||||
|
||||
1. **Delegate to validation.ChainLeft** for error handling logic
|
||||
2. **Aggregate errors** when transformations fail
|
||||
3. **Preserve successes** unchanged
|
||||
4. **Enable recovery** from decode failures
|
||||
5. **Provide access** to the original input
|
||||
|
||||
The choice between them is purely about **code readability and semantic intent**:
|
||||
- Use **`OrElse`** when emphasizing fallback/alternative decoding
|
||||
- Use **`ChainLeft`** when emphasizing error transformation
|
||||
|
||||
Both maintain the critical property of **error aggregation**, ensuring all validation failures are preserved and reported together.
|
||||
335
v2/optics/codec/decode/bind.go
Normal file
335
v2/optics/codec/decode/bind.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// 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 decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "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.
|
||||
// This is the starting point for building up a context using do-notation style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result struct {
|
||||
// x int
|
||||
// y string
|
||||
// }
|
||||
// result := Do(Result{})
|
||||
func Do[I, S any](
|
||||
empty S,
|
||||
) Decode[I, S] {
|
||||
return Of[I](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
// This is used in do-notation style to sequentially build up a context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// Bind(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, func(s State) Decode[string, int] {
|
||||
// return Of[string](42)
|
||||
// }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
func Bind[I, S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f Kleisli[I, S1, A],
|
||||
) Operator[I, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[I, S1, S2],
|
||||
Map[I, A, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value, not wrapped in Decode.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; computed int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{x: 5}),
|
||||
// Let[string](func(c int) func(State) State {
|
||||
// return func(s State) State { s.computed = c; return s }
|
||||
// }, func(s State) int { return s.x * 2 }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 5, computed: 10})
|
||||
func Let[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
f func(S1) B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.Let(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; name string }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// LetTo(func(n string) func(State) State {
|
||||
// return func(s State) State { s.name = n; return s }
|
||||
// }, "example"),
|
||||
// )
|
||||
func LetTo[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically used as the first operation after creating a Decode value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](42),
|
||||
// BindTo[string](func(x int) State { return State{value: x} }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{value: 42})
|
||||
func BindTo[I, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[I, T, S1] {
|
||||
return C.BindTo(
|
||||
Map[I, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
|
||||
// This uses the applicative functor pattern, allowing parallel composition.
|
||||
//
|
||||
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
|
||||
// and the value. If both validations fail, all errors are collected and returned together.
|
||||
// This is useful for validating multiple independent fields and reporting all errors at once.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// ApS(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, Of[string](42)),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
//
|
||||
// Error aggregation example:
|
||||
//
|
||||
// // Both decoders fail - errors are aggregated
|
||||
// decoder1 := func(input string) Validation[State] {
|
||||
// return validation.Failures[State](/* errors */)
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[int] {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// combined := ApS(setter, decoder2)(decoder1)
|
||||
// result := combined("input") // Contains BOTH sets of errors
|
||||
func ApS[I, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Decode[I, T],
|
||||
) Operator[I, S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, I, T],
|
||||
Map[I, S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
|
||||
// and the value fail validation, all errors are collected and returned together.
|
||||
// This enables comprehensive error reporting for complex nested structures.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the Address field
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// // Use ApSL to update the address
|
||||
// decoder := F.Pipe2(
|
||||
// Of[string](Person{Name: "Alice"}),
|
||||
// ApSL(
|
||||
// addressLens,
|
||||
// Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
// ),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Person{...})
|
||||
func ApSL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Decode[I, T],
|
||||
) Operator[I, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// a Validation that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// if v >= 100 {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// return validation.Success(v + 1)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 42}),
|
||||
// BindL(valueLens, increment),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 43})
|
||||
func BindL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[I, T, T],
|
||||
) Operator[I, S, S] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Validation).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 21}),
|
||||
// LetL(valueLens, double),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 42})
|
||||
func LetL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[I, S, S] {
|
||||
return Let[I](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Config{Debug: true, Timeout: 30}),
|
||||
// LetToL(debugLens, false),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[I, S, S] {
|
||||
return LetTo[I](lens.Set, b)
|
||||
}
|
||||
665
v2/optics/codec/decode/bind_test.go
Normal file
665
v2/optics/codec/decode/bind_test.go
Normal file
@@ -0,0 +1,665 @@
|
||||
package decode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates decoder with empty state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
decoder := Do[string](State{})
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{}, value)
|
||||
})
|
||||
|
||||
t.Run("creates decoder with initialized state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
initial := State{x: 42, y: "hello"}
|
||||
decoder := Do[string](initial)
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, initial, value)
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
intDecoder := Do[int](0)
|
||||
assert.True(t, either.IsRight(intDecoder(42)))
|
||||
|
||||
strDecoder := Do[string]("")
|
||||
assert.True(t, either.IsRight(strDecoder("test")))
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customDecoder := Do[[]byte](Custom{Value: 100})
|
||||
assert.True(t, either.IsRight(customDecoder([]byte("data"))))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("binds successful decode to state", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 42, y: 10}, value)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "y failed"},
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(State) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "y failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("can access previous state values", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
// y depends on x
|
||||
return Of[string](s.x * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 10, y: 20}, value)
|
||||
})
|
||||
|
||||
t.Run("can access input in decoder", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Decode[string, int] {
|
||||
return func(input string) validation.Validation[int] {
|
||||
// Use input to determine value
|
||||
if input == "large" {
|
||||
return validation.Success(100)
|
||||
}
|
||||
return validation.Success(10)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result1 := decoder("large")
|
||||
value1 := either.MonadFold(result1,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, 100, value1.x)
|
||||
|
||||
result2 := decoder("small")
|
||||
value2 := either.MonadFold(result2,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, 10, value2.x)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
computed int
|
||||
}
|
||||
|
||||
t.Run("attaches pure computation result to state", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, computed: 10}, value)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Let operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
z int
|
||||
}
|
||||
decoder := F.Pipe3(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
Let[string](func(z int) func(State) State {
|
||||
return func(s State) State { s.z = z; return s }
|
||||
}, func(s State) int { return s.y + 10 }),
|
||||
Let[string](func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) int { return s.z * 3 }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
name string
|
||||
}
|
||||
|
||||
t.Run("attaches constant value to state", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example"),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, name: "example"}, value)
|
||||
})
|
||||
|
||||
t.Run("sets multiple constant values", func(t *testing.T) {
|
||||
type State struct {
|
||||
name string
|
||||
version int
|
||||
active bool
|
||||
}
|
||||
decoder := F.Pipe3(
|
||||
Do[string](State{}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "app"),
|
||||
LetTo[string](func(v int) func(State) State {
|
||||
return func(s State) State { s.version = v; return s }
|
||||
}, 2),
|
||||
LetTo[string](func(a bool) func(State) State {
|
||||
return func(s State) State { s.active = a; return s }
|
||||
}, true),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{name: "app", version: 2, active: true}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("initializes state from value", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Of[string](42),
|
||||
BindTo[string](func(x int) State { return State{value: x} }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{value: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type StringState struct {
|
||||
text string
|
||||
}
|
||||
decoder := F.Pipe1(
|
||||
Of[string]("hello"),
|
||||
BindTo[string](func(s string) StringState { return StringState{text: s} }),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) StringState { return StringState{} },
|
||||
F.Identity[StringState],
|
||||
)
|
||||
assert.Equal(t, StringState{text: "hello"}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("attaches value using applicative pattern", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](42)),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from both decoders", func(t *testing.T) {
|
||||
stateDecoder := func(input string) validation.Validation[State] {
|
||||
return validation.Failures[State](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "state error"},
|
||||
})
|
||||
}
|
||||
valueDecoder := func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "value error"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder := ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, valueDecoder)(stateDecoder)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(State) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "state error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("combines multiple ApS operations", func(t *testing.T) {
|
||||
decoder := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](10)),
|
||||
ApS(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, Of[string](20)),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 10, y: 20}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
t.Run("updates nested structure using lens", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Person{Name: "Alice"}),
|
||||
ApSL(
|
||||
addressLens,
|
||||
Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Person { return Person{} },
|
||||
F.Identity[Person],
|
||||
)
|
||||
assert.Equal(t, "Alice", value.Name)
|
||||
assert.Equal(t, "Main St", value.Address.Street)
|
||||
assert.Equal(t, "NYC", value.Address.City)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
personDecoder := func(input string) validation.Validation[Person] {
|
||||
return validation.Failures[Person](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "person error"},
|
||||
})
|
||||
}
|
||||
addressDecoder := func(input string) validation.Validation[Address] {
|
||||
return validation.Failures[Address](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "address error"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder := ApSL(addressLens, addressDecoder)(personDecoder)
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(Person) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("updates field based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Decode[string, int] {
|
||||
return Of[string](v + 1)
|
||||
}
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, Counter{Value: 43}, value)
|
||||
})
|
||||
|
||||
t.Run("fails validation based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Decode[string, int] {
|
||||
return func(input string) validation.Validation[int] {
|
||||
if v >= 100 {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "exceeds limit"},
|
||||
})
|
||||
}
|
||||
return validation.Success(v + 1)
|
||||
}
|
||||
}
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(Counter) validation.Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "exceeds limit", errors[0].Messsage)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("transforms field with pure function", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Counter{Value: 21}),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, Counter{Value: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
add10 := func(v int) int { return v + 10 }
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
decoder := F.Pipe2(
|
||||
Of[string](Counter{Value: 5}),
|
||||
LetL[string](valueLens, add10),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Counter { return Counter{} },
|
||||
F.Identity[Counter],
|
||||
)
|
||||
assert.Equal(t, Counter{Value: 30}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("sets field to constant value", func(t *testing.T) {
|
||||
decoder := F.Pipe1(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, Config{Debug: false, Timeout: 30}, value)
|
||||
})
|
||||
|
||||
t.Run("sets multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
decoder := F.Pipe2(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
LetToL[string](timeoutLens, 60),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, Config{Debug: false, Timeout: 60}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindOperationsComposition(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
|
||||
decoder := F.Pipe4(
|
||||
Do[string](User{}),
|
||||
LetTo[string](func(n string) func(User) User {
|
||||
return func(u User) User { u.Name = n; return u }
|
||||
}, "Alice"),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Decode[string, int] {
|
||||
// Age validation
|
||||
if len(u.Name) > 0 {
|
||||
return Of[string](25)
|
||||
}
|
||||
return func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "name required"},
|
||||
})
|
||||
}
|
||||
}),
|
||||
Let[string](func(e string) func(User) User {
|
||||
return func(u User) User { u.Email = e; return u }
|
||||
}, func(u User) string {
|
||||
// Derive email from name
|
||||
return u.Name + "@example.com"
|
||||
}),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Decode[string, int] {
|
||||
// Validate age is positive
|
||||
if u.Age > 0 {
|
||||
return Of[string](u.Age)
|
||||
}
|
||||
return func(input string) validation.Validation[int] {
|
||||
return validation.Failures[int](validation.Errors{
|
||||
&validation.ValidationError{Messsage: "age must be positive"},
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) User { return User{} },
|
||||
F.Identity[User],
|
||||
)
|
||||
assert.Equal(t, "Alice", value.Name)
|
||||
assert.Equal(t, 25, value.Age)
|
||||
assert.Equal(t, "Alice@example.com", value.Email)
|
||||
})
|
||||
}
|
||||
@@ -50,6 +50,17 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Decode[I, A]](
|
||||
validation.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadMap transforms the decoded value using the provided function.
|
||||
// This is the functor map operation that applies a transformation to successful decode results.
|
||||
//
|
||||
|
||||
@@ -382,3 +382,400 @@ func TestFunctorLaws(t *testing.T) {
|
||||
assert.Equal(t, right(input), left(input))
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeft tests the ChainLeft function
|
||||
func TestChainLeft(t *testing.T) {
|
||||
t.Run("transforms failures while preserving successes", func(t *testing.T) {
|
||||
// Create a failing decoder
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "decode failed"},
|
||||
})
|
||||
}
|
||||
|
||||
// Handler that recovers from specific errors
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "decode failed" {
|
||||
return Of[string](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(failingDecoder)
|
||||
res := decoder("input")
|
||||
|
||||
assert.Equal(t, validation.Of(0), res, "Should recover from failure")
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successDecoder := Of[string](42)
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "should not be called"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(successDecoder)
|
||||
res := decoder("input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), res, "Success should pass through unchanged")
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "original error"},
|
||||
})
|
||||
}
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[string, string] {
|
||||
return func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "additional error"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := handler(failingDecoder)
|
||||
res := decoder("input")
|
||||
|
||||
assert.True(t, either.IsLeft(res))
|
||||
errors := either.MonadFold(res,
|
||||
func(e Errors) Errors { return e },
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "original error")
|
||||
assert.Contains(t, messages, "additional error")
|
||||
})
|
||||
|
||||
t.Run("adds context to errors", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "invalid format"},
|
||||
})
|
||||
}
|
||||
|
||||
addContext := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to decode user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
decoder := addContext(failingDecoder)
|
||||
res := decoder("abc")
|
||||
|
||||
assert.True(t, either.IsLeft(res))
|
||||
errors := either.MonadFold(res,
|
||||
func(e Errors) Errors { return e },
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2, "Should have both original and context errors")
|
||||
})
|
||||
|
||||
t.Run("can be composed in pipeline", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "error1"},
|
||||
})
|
||||
}
|
||||
|
||||
handler1 := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "error2"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
handler2 := ChainLeft(func(errs Errors) Decode[string, int] {
|
||||
// Check if we can recover
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "error1" {
|
||||
return Of[string](100) // recover
|
||||
}
|
||||
}
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
|
||||
// Compose handlers
|
||||
decoder := handler2(handler1(failingDecoder))
|
||||
res := decoder("input")
|
||||
|
||||
// Should recover because error1 is present
|
||||
assert.Equal(t, validation.Of(100), res)
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
failingDecoder := func(cfg Config) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: cfg.Port, Messsage: "invalid port"},
|
||||
})
|
||||
}
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Decode[Config, string] {
|
||||
return Of[Config]("default-value")
|
||||
})
|
||||
|
||||
decoder := handler(failingDecoder)
|
||||
res := decoder(Config{Port: 9999})
|
||||
|
||||
assert.Equal(t, validation.Of("default-value"), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrElse tests the OrElse function
|
||||
func TestOrElse(t *testing.T) {
|
||||
t.Run("OrElse is equivalent to ChainLeft - Success case", func(t *testing.T) {
|
||||
successDecoder := Of[string](42)
|
||||
|
||||
handler := func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "should not be called"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test with OrElse
|
||||
resultOrElse := OrElse(handler)(successDecoder)("input")
|
||||
// Test with ChainLeft
|
||||
resultChainLeft := ChainLeft(handler)(successDecoder)("input")
|
||||
|
||||
assert.Equal(t, resultChainLeft, resultOrElse, "OrElse and ChainLeft should produce identical results for Success")
|
||||
assert.Equal(t, validation.Of(42), resultOrElse)
|
||||
})
|
||||
|
||||
t.Run("OrElse is equivalent to ChainLeft - Failure recovery", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not found"},
|
||||
})
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Decode[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "not found" {
|
||||
return Of[string](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
}
|
||||
|
||||
// Test with OrElse
|
||||
resultOrElse := OrElse(handler)(failingDecoder)("input")
|
||||
// Test with ChainLeft
|
||||
resultChainLeft := ChainLeft(handler)(failingDecoder)("input")
|
||||
|
||||
assert.Equal(t, resultChainLeft, resultOrElse, "OrElse and ChainLeft should produce identical results for recovery")
|
||||
assert.Equal(t, validation.Of(0), resultOrElse)
|
||||
})
|
||||
|
||||
t.Run("OrElse is equivalent to ChainLeft - Error aggregation", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Value: input, Messsage: "original error"},
|
||||
})
|
||||
}
|
||||
|
||||
handler := func(errs Errors) Decode[string, string] {
|
||||
return func(input string) Validation[string] {
|
||||
return either.Left[string](validation.Errors{
|
||||
{Messsage: "additional error"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test with OrElse
|
||||
resultOrElse := OrElse(handler)(failingDecoder)("input")
|
||||
// Test with ChainLeft
|
||||
resultChainLeft := ChainLeft(handler)(failingDecoder)("input")
|
||||
|
||||
assert.Equal(t, resultChainLeft, resultOrElse, "OrElse and ChainLeft should produce identical results for error aggregation")
|
||||
|
||||
// Verify both aggregate errors
|
||||
assert.True(t, either.IsLeft(resultOrElse))
|
||||
errors := either.MonadFold(resultOrElse,
|
||||
func(e Errors) Errors { return e },
|
||||
func(string) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "original error")
|
||||
assert.Contains(t, messages, "additional error")
|
||||
})
|
||||
|
||||
t.Run("OrElse semantic meaning - fallback decoder", func(t *testing.T) {
|
||||
// OrElse provides a semantic name for fallback/alternative decoding
|
||||
// It reads naturally: "try this decoder, or else try this alternative"
|
||||
|
||||
primaryDecoder := func(input string) Validation[int] {
|
||||
if input == "valid" {
|
||||
return validation.Of(42)
|
||||
}
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "primary decode failed"},
|
||||
})
|
||||
}
|
||||
|
||||
// Use OrElse to provide a fallback: if decoding fails, use default value
|
||||
withDefault := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return Of[string](0) // default to 0 if decoding fails
|
||||
})
|
||||
|
||||
decoder := withDefault(primaryDecoder)
|
||||
|
||||
// Test success case
|
||||
resSuccess := decoder("valid")
|
||||
assert.Equal(t, validation.Of(42), resSuccess, "Should use primary decoder on success")
|
||||
|
||||
// Test fallback case
|
||||
resFallback := decoder("invalid")
|
||||
assert.Equal(t, validation.Of(0), resFallback, "OrElse provides fallback value")
|
||||
})
|
||||
|
||||
t.Run("OrElse in pipeline composition", func(t *testing.T) {
|
||||
failingDecoder := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "database error"},
|
||||
})
|
||||
}
|
||||
|
||||
addContext := OrElse(func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "context added"},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
recoverFromNotFound := OrElse(func(errs Errors) Decode[string, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "not found" {
|
||||
return Of[string](0)
|
||||
}
|
||||
}
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](errs)
|
||||
}
|
||||
})
|
||||
|
||||
// Test error aggregation in pipeline
|
||||
decoder1 := recoverFromNotFound(addContext(failingDecoder))
|
||||
res1 := decoder1("input")
|
||||
|
||||
assert.True(t, either.IsLeft(res1))
|
||||
errors := either.MonadFold(res1,
|
||||
func(e Errors) Errors { return e },
|
||||
func(int) Errors { return nil },
|
||||
)
|
||||
// Errors accumulate through the pipeline
|
||||
assert.Greater(t, len(errors), 1, "Should aggregate errors from pipeline")
|
||||
|
||||
// Test recovery in pipeline
|
||||
failingDecoder2 := func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Value: input, Messsage: "not found"},
|
||||
})
|
||||
}
|
||||
|
||||
decoder2 := recoverFromNotFound(addContext(failingDecoder2))
|
||||
res2 := decoder2("input")
|
||||
|
||||
assert.Equal(t, validation.Of(0), res2, "Should recover from 'not found' error")
|
||||
})
|
||||
|
||||
t.Run("OrElse vs ChainLeft - identical behavior verification", func(t *testing.T) {
|
||||
// Create various test scenarios
|
||||
scenarios := []struct {
|
||||
name string
|
||||
decoder Decode[string, int]
|
||||
handler func(Errors) Decode[string, int]
|
||||
}{
|
||||
{
|
||||
name: "Success value",
|
||||
decoder: Of[string](42),
|
||||
handler: func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{{Messsage: "error"}})
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Failure with recovery",
|
||||
decoder: func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{{Messsage: "error"}})
|
||||
},
|
||||
handler: func(errs Errors) Decode[string, int] {
|
||||
return Of[string](0)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Failure with error transformation",
|
||||
decoder: func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{{Messsage: "error1"}})
|
||||
},
|
||||
handler: func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{{Messsage: "error2"}})
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple errors aggregation",
|
||||
decoder: func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "error1"},
|
||||
{Messsage: "error2"},
|
||||
})
|
||||
},
|
||||
handler: func(errs Errors) Decode[string, int] {
|
||||
return func(input string) Validation[int] {
|
||||
return either.Left[int](validation.Errors{
|
||||
{Messsage: "error3"},
|
||||
{Messsage: "error4"},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.name, func(t *testing.T) {
|
||||
resultOrElse := OrElse(scenario.handler)(scenario.decoder)("test-input")
|
||||
resultChainLeft := ChainLeft(scenario.handler)(scenario.decoder)("test-input")
|
||||
|
||||
assert.Equal(t, resultChainLeft, resultOrElse,
|
||||
"OrElse and ChainLeft must produce identical results for: %s", scenario.name)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,30 +1,222 @@
|
||||
// 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 decode
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
Errors = validation.Errors
|
||||
|
||||
// Validation represents the result of a validation operation that may contain
|
||||
// validation errors or a successfully validated value of type A.
|
||||
// This is an alias for validation.Validation[A], which is Either[Errors, A].
|
||||
//
|
||||
// In the decode context:
|
||||
// - Left(Errors): Decoding failed with one or more validation errors
|
||||
// - Right(A): Successfully decoded value of type A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Success case
|
||||
// valid := validation.Success(42) // Right(42)
|
||||
//
|
||||
// // Failure case
|
||||
// invalid := validation.Failures[int](validation.Errors{
|
||||
// &validation.ValidationError{Messsage: "invalid format"},
|
||||
// }) // Left([...])
|
||||
Validation[A any] = validation.Validation[A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
// This is an alias for reader.Reader[R, A], which is func(R) A.
|
||||
//
|
||||
// In the decode context, Reader is used to access the input data being decoded.
|
||||
// The environment R is typically the raw input (e.g., JSON, string, bytes) that
|
||||
// needs to be decoded into a structured type A.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A reader that extracts a field from a map
|
||||
// getField := func(data map[string]any) string {
|
||||
// return data["name"].(string)
|
||||
// } // Reader[map[string]any, string]
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
// Decode is a function that decodes input I to type A with validation.
|
||||
// It returns a Validation result directly.
|
||||
// It combines the Reader pattern (for accessing input) with Validation (for error handling).
|
||||
//
|
||||
// Type: func(I) Validation[A]
|
||||
//
|
||||
// A Decode function:
|
||||
// 1. Takes raw input of type I (e.g., JSON, string, bytes)
|
||||
// 2. Attempts to decode/parse it into type A
|
||||
// 3. Returns a Validation[A] with either:
|
||||
// - Success(A): Successfully decoded value
|
||||
// - Failures(Errors): Validation errors describing what went wrong
|
||||
//
|
||||
// This type is the foundation of the decode package, enabling composable,
|
||||
// type-safe decoding with comprehensive error reporting.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Decode a string to an integer
|
||||
// decodeInt := func(input string) Validation[int] {
|
||||
// n, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return validation.Failures[int](validation.Errors{
|
||||
// &validation.ValidationError{
|
||||
// Value: input,
|
||||
// Messsage: "not a valid integer",
|
||||
// Cause: err,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// return validation.Success(n)
|
||||
// } // Decode[string, int]
|
||||
//
|
||||
// result := decodeInt("42") // Success(42)
|
||||
// result := decodeInt("abc") // Failures([...])
|
||||
Decode[I, A any] = Reader[I, Validation[A]]
|
||||
|
||||
// Kleisli represents a function from A to a decoded B given input type I.
|
||||
// It's a Reader that takes an input A and produces a Decode[I, B] function.
|
||||
// This enables composition of decoding operations in a functional style.
|
||||
//
|
||||
// Type: func(A) Decode[I, B]
|
||||
// which expands to: func(A) func(I) Validation[B]
|
||||
//
|
||||
// Kleisli arrows are the fundamental building blocks for composing decoders.
|
||||
// They allow you to chain decoding operations where each step can:
|
||||
// 1. Depend on the result of the previous step (the A parameter)
|
||||
// 2. Access the original input (the I parameter via Decode)
|
||||
// 3. Fail with validation errors (via Validation[B])
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Conditional decoding based on previously decoded values
|
||||
// - Multi-stage decoding pipelines
|
||||
// - Dependent field validation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Decode a user, then decode their age based on their type
|
||||
// decodeAge := func(userType string) Decode[map[string]any, int] {
|
||||
// return func(data map[string]any) Validation[int] {
|
||||
// if userType == "admin" {
|
||||
// // Admins must be 18+
|
||||
// age := data["age"].(int)
|
||||
// if age < 18 {
|
||||
// return validation.Failures[int](/* error */)
|
||||
// }
|
||||
// return validation.Success(age)
|
||||
// }
|
||||
// // Regular users can be any age
|
||||
// return validation.Success(data["age"].(int))
|
||||
// }
|
||||
// } // Kleisli[map[string]any, string, int]
|
||||
//
|
||||
// // Use with Chain to compose decoders
|
||||
// decoder := F.Pipe2(
|
||||
// decodeUserType, // Decode[map[string]any, string]
|
||||
// Chain(decodeAge), // Chains with Kleisli
|
||||
// Map(func(age int) User { // Transform to final type
|
||||
// return User{Age: age}
|
||||
// }),
|
||||
// )
|
||||
Kleisli[I, A, B any] = Reader[A, Decode[I, B]]
|
||||
|
||||
// Operator represents a decoding transformation that takes a decoded A and produces a decoded B.
|
||||
// It's a specialized Kleisli arrow for composing decode operations where the input is already decoded.
|
||||
// This allows chaining multiple decode transformations together.
|
||||
//
|
||||
// Type: func(Decode[I, A]) Decode[I, B]
|
||||
//
|
||||
// Operators are higher-order functions that transform one decoder into another.
|
||||
// They are the result of partially applying functions like Map, Chain, and Ap,
|
||||
// making them ideal for use in composition pipelines with F.Pipe.
|
||||
//
|
||||
// Key characteristics:
|
||||
// - Takes a Decode[I, A] as input
|
||||
// - Returns a Decode[I, B] as output
|
||||
// - Preserves the input type I (the raw data being decoded)
|
||||
// - Transforms the output type from A to B
|
||||
//
|
||||
// Common operators:
|
||||
// - Map(f): Transforms successful decode results
|
||||
// - Chain(f): Sequences dependent decode operations
|
||||
// - Ap(fa): Applies function decoders to value decoders
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create reusable operators
|
||||
// toString := Map(func(n int) string {
|
||||
// return strconv.Itoa(n)
|
||||
// }) // Operator[string, int, string]
|
||||
//
|
||||
// validatePositive := Chain(func(n int) Decode[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// if n <= 0 {
|
||||
// return validation.Failures[int](/* error */)
|
||||
// }
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// }) // Operator[string, int, int]
|
||||
//
|
||||
// // Compose operators in a pipeline
|
||||
// decoder := F.Pipe2(
|
||||
// decodeInt, // Decode[string, int]
|
||||
// validatePositive, // Operator[string, int, int]
|
||||
// toString, // Operator[string, int, string]
|
||||
// ) // Decode[string, string]
|
||||
//
|
||||
// result := decoder("42") // Success("42")
|
||||
// result := decoder("-5") // Failures([...])
|
||||
Operator[I, A, B any] = Kleisli[I, Decode[I, A], B]
|
||||
|
||||
// Endomorphism represents a function from a type to itself: func(A) A.
|
||||
// This is an alias for endomorphism.Endomorphism[A].
|
||||
//
|
||||
// In the decode context, endomorphisms are used with LetL to transform
|
||||
// decoded values using pure functions that don't change the type.
|
||||
//
|
||||
// Endomorphisms are useful for:
|
||||
// - Normalizing data (e.g., trimming strings, rounding numbers)
|
||||
// - Applying business rules (e.g., clamping values to ranges)
|
||||
// - Data sanitization (e.g., removing special characters)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Normalize a string by trimming and lowercasing
|
||||
// normalize := func(s string) string {
|
||||
// return strings.ToLower(strings.TrimSpace(s))
|
||||
// } // Endomorphism[string]
|
||||
//
|
||||
// // Clamp an integer to a range
|
||||
// clamp := func(n int) int {
|
||||
// if n < 0 { return 0 }
|
||||
// if n > 100 { return 100 }
|
||||
// return n
|
||||
// } // Endomorphism[int]
|
||||
//
|
||||
// // Use with LetL to transform decoded values
|
||||
// decoder := F.Pipe1(
|
||||
// decodeString,
|
||||
// LetL(nameLens, normalize),
|
||||
// )
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
)
|
||||
|
||||
277
v2/optics/codec/either.go
Normal file
277
v2/optics/codec/either.go
Normal file
@@ -0,0 +1,277 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
)
|
||||
|
||||
// encodeEither creates an encoder for Either[A, B] values.
|
||||
//
|
||||
// This function produces an encoder that handles both Left and Right cases of an Either value.
|
||||
// It uses the provided codecs to encode the Left (A) and Right (B) values respectively.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the Left value
|
||||
// - B: The type of the Right value
|
||||
// - O: The output type after encoding
|
||||
// - I: The input type for validation (not used in encoding)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - leftItem: The codec for encoding Left values of type A
|
||||
// - rightItem: The codec for encoding Right values of type B
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Encode function that takes an Either[A, B] and returns O by encoding
|
||||
// either the Left or Right value using the appropriate codec.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// stringCodec := String()
|
||||
// intCodec := Int()
|
||||
// encoder := encodeEither(stringCodec, intCodec)
|
||||
//
|
||||
// // Encode a Left value
|
||||
// leftResult := encoder(either.Left[int]("error"))
|
||||
// // leftResult contains the encoded string "error"
|
||||
//
|
||||
// // Encode a Right value
|
||||
// rightResult := encoder(either.Right[string](42))
|
||||
// // rightResult contains the encoded int 42
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Uses either.Fold to pattern match on the Either value
|
||||
// - Left values are encoded using leftItem.Encode
|
||||
// - Right values are encoded using rightItem.Encode
|
||||
func encodeEither[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Encode[either.Either[A, B], O] {
|
||||
return either.Fold(
|
||||
leftItem.Encode,
|
||||
rightItem.Encode,
|
||||
)
|
||||
}
|
||||
|
||||
// validateEither creates a validator for Either[A, B] values.
|
||||
//
|
||||
// This function produces a validator that attempts to validate the input as both
|
||||
// a Left (A) and Right (B) value. The validation strategy is:
|
||||
// 1. First, try to validate as a Right value (B)
|
||||
// 2. If Right validation succeeds, return Either.Right[A](B)
|
||||
// 3. If Right validation fails, try to validate as a Left value (A)
|
||||
// 4. If Left validation succeeds, return Either.Left[B](A)
|
||||
// 5. If both validations fail, concatenate all errors from both attempts
|
||||
//
|
||||
// This approach ensures that the validator tries both branches and provides
|
||||
// comprehensive error information when both fail.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the Left value
|
||||
// - B: The type of the Right value
|
||||
// - O: The output type after encoding (not used in validation)
|
||||
// - I: The input type to validate
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - leftItem: The codec for validating Left values of type A
|
||||
// - rightItem: The codec for validating Right values of type B
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate function that takes an input I and returns a Decode function.
|
||||
// The Decode function takes a Context and returns a Validation[Either[A, B]].
|
||||
//
|
||||
// # Validation Logic
|
||||
//
|
||||
// The validator follows this decision tree:
|
||||
//
|
||||
// Input I
|
||||
// |
|
||||
// +--> Validate as Right (B)
|
||||
// |
|
||||
// +-- Success --> Return Either.Right[A](B)
|
||||
// |
|
||||
// +-- Failure --> Validate as Left (A)
|
||||
// |
|
||||
// +-- Success --> Return Either.Left[B](A)
|
||||
// |
|
||||
// +-- Failure --> Return all errors (Left + Right)
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// stringCodec := String()
|
||||
// intCodec := Int()
|
||||
// validator := validateEither(stringCodec, intCodec)
|
||||
//
|
||||
// // Validate a string (will succeed as Left)
|
||||
// result1 := validator("hello")(validation.Context{})
|
||||
// // result1 is Success(Either.Left[int]("hello"))
|
||||
//
|
||||
// // Validate an int (will succeed as Right)
|
||||
// result2 := validator(42)(validation.Context{})
|
||||
// // result2 is Success(Either.Right[string](42))
|
||||
//
|
||||
// // Validate something that's neither (will fail with both errors)
|
||||
// result3 := validator([]int{1, 2, 3})(validation.Context{})
|
||||
// // result3 is Failure with errors from both string and int validation
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Prioritizes Right validation over Left validation
|
||||
// - Accumulates errors from both branches when both fail
|
||||
// - Uses the validation context to provide detailed error messages
|
||||
// - The validator is lazy: it only evaluates Left if Right fails
|
||||
func validateEither[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Validate[I, either.Either[A, B]] {
|
||||
|
||||
return func(i I) Decode[Context, either.Either[A, B]] {
|
||||
valRight := rightItem.Validate(i)
|
||||
valLeft := leftItem.Validate(i)
|
||||
|
||||
return func(ctx Context) Validation[either.Either[A, B]] {
|
||||
|
||||
resRight := valRight(ctx)
|
||||
|
||||
return either.Fold(
|
||||
func(rightErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
resLeft := valLeft(ctx)
|
||||
return either.Fold(
|
||||
func(leftErrors validate.Errors) Validation[either.Either[A, B]] {
|
||||
return validation.Failures[either.Either[A, B]](array.Concat(leftErrors)(rightErrors))
|
||||
},
|
||||
F.Flow2(either.Left[B, A], validation.Of),
|
||||
)(resLeft)
|
||||
},
|
||||
F.Flow2(either.Right[A, B], validation.Of),
|
||||
)(resRight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either creates a codec for Either[A, B] values.
|
||||
//
|
||||
// This function constructs a complete codec that can encode, decode, and validate
|
||||
// Either values. An Either represents a value that can be one of two types: Left (A)
|
||||
// or Right (B). This is commonly used for error handling, where Left represents an
|
||||
// error and Right represents a success value.
|
||||
//
|
||||
// The codec handles both branches of the Either type using the provided codecs for
|
||||
// each branch. During validation, it attempts to validate the input as both types
|
||||
// and succeeds if either validation passes.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the Left value
|
||||
// - B: The type of the Right value
|
||||
// - O: The output type after encoding
|
||||
// - I: The input type for validation
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - leftItem: The codec for handling Left values of type A
|
||||
// - rightItem: The codec for handling Right values of type B
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Type[either.Either[A, B], O, I] that can encode, decode, and validate Either values.
|
||||
//
|
||||
// # Codec Behavior
|
||||
//
|
||||
// Encoding:
|
||||
// - Left values are encoded using leftItem.Encode
|
||||
// - Right values are encoded using rightItem.Encode
|
||||
//
|
||||
// Validation:
|
||||
// - First attempts to validate as Right (B)
|
||||
// - If Right fails, attempts to validate as Left (A)
|
||||
// - If both fail, returns all accumulated errors
|
||||
// - If either succeeds, returns the corresponding Either value
|
||||
//
|
||||
// Type Checking:
|
||||
// - Uses Is[either.Either[A, B]]() to verify the value is an Either
|
||||
//
|
||||
// Naming:
|
||||
// - The codec name is "Either[<leftName>, <rightName>]"
|
||||
// - Example: "Either[string, int]"
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Create a codec for Either[string, int]
|
||||
// stringCodec := String()
|
||||
// intCodec := Int()
|
||||
// eitherCodec := Either(stringCodec, intCodec)
|
||||
//
|
||||
// // Encode a Left value
|
||||
// leftEncoded := eitherCodec.Encode(either.Left[int]("error"))
|
||||
// // leftEncoded contains the encoded string
|
||||
//
|
||||
// // Encode a Right value
|
||||
// rightEncoded := eitherCodec.Encode(either.Right[string](42))
|
||||
// // rightEncoded contains the encoded int
|
||||
//
|
||||
// // Decode/validate an input
|
||||
// result := eitherCodec.Decode("hello")
|
||||
// // result is Success(Either.Left[int]("hello"))
|
||||
//
|
||||
// result2 := eitherCodec.Decode(42)
|
||||
// // result2 is Success(Either.Right[string](42))
|
||||
//
|
||||
// // Get the codec name
|
||||
// name := eitherCodec.Name()
|
||||
// // name is "Either[string, int]"
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Error handling: Either[Error, Value]
|
||||
// - Alternative values: Either[DefaultValue, CustomValue]
|
||||
// - Union types: Either[TypeA, TypeB]
|
||||
// - Validation results: Either[ValidationError, ValidatedValue]
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The codec prioritizes Right validation over Left validation
|
||||
// - Both branches must have compatible encoding output types (O)
|
||||
// - Both branches must have compatible validation input types (I)
|
||||
// - The codec name includes the names of both branch codecs
|
||||
// - This is a building block for more complex sum types
|
||||
func Either[A, B, O, I any](
|
||||
leftItem Type[A, O, I],
|
||||
rightItem Type[B, O, I],
|
||||
) Type[either.Either[A, B], O, I] {
|
||||
name := fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name())
|
||||
isEither := Is[either.Either[A, B]]()
|
||||
|
||||
return MakeType(
|
||||
name,
|
||||
isEither,
|
||||
validateEither(leftItem, rightItem),
|
||||
encodeEither(leftItem, rightItem),
|
||||
)
|
||||
}
|
||||
347
v2/optics/codec/either_test.go
Normal file
347
v2/optics/codec/either_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestEitherWithIdentityCodecs tests the Either function with identity codecs
|
||||
// where both branches have the same output and input types
|
||||
func TestEitherWithIdentityCodecs(t *testing.T) {
|
||||
t.Run("creates codec with correct name", func(t *testing.T) {
|
||||
// The Either function is designed for cases where both branches encode to the same type
|
||||
// For example, both encode to string or both encode to JSON
|
||||
|
||||
// Create codecs that both encode to string
|
||||
stringToString := Id[string]()
|
||||
intToString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringToString, intToString)
|
||||
|
||||
assert.Equal(t, "Either[string, IntFromString]", eitherCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherEncode tests encoding of Either values
|
||||
func TestEitherEncode(t *testing.T) {
|
||||
// Create codecs that both encode to string
|
||||
stringToString := Id[string]()
|
||||
intToString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringToString, intToString)
|
||||
|
||||
t.Run("encodes Left value", func(t *testing.T) {
|
||||
leftValue := either.Left[int]("hello")
|
||||
encoded := eitherCodec.Encode(leftValue)
|
||||
|
||||
assert.Equal(t, "hello", encoded)
|
||||
})
|
||||
|
||||
t.Run("encodes Right value", func(t *testing.T) {
|
||||
rightValue := either.Right[string](42)
|
||||
encoded := eitherCodec.Encode(rightValue)
|
||||
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherDecode tests decoding/validation of Either values
|
||||
func TestEitherDecode(t *testing.T) {
|
||||
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, either.Either[string, int]](either.Left[int]("")))
|
||||
|
||||
// Create codecs that both work with string input
|
||||
stringCodec := Id[string]()
|
||||
intFromString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringCodec, intFromString)
|
||||
|
||||
t.Run("decodes integer string as Right", func(t *testing.T) {
|
||||
result := eitherCodec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode integer string")
|
||||
|
||||
value := getOrElseNull(result)
|
||||
assert.True(t, either.IsRight(value), "should be Right")
|
||||
|
||||
rightValue := either.MonadFold(value,
|
||||
func(string) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, rightValue)
|
||||
})
|
||||
|
||||
t.Run("decodes non-integer string as Left", func(t *testing.T) {
|
||||
result := eitherCodec.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result), "should successfully decode string")
|
||||
|
||||
value := getOrElseNull(result)
|
||||
assert.True(t, either.IsLeft(value), "should be Left")
|
||||
|
||||
leftValue := either.MonadFold(value,
|
||||
F.Identity[string],
|
||||
func(int) string { return "" },
|
||||
)
|
||||
assert.Equal(t, "hello", leftValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherValidation tests validation behavior
|
||||
func TestEitherValidation(t *testing.T) {
|
||||
t.Run("validates with custom codecs", func(t *testing.T) {
|
||||
// Create a codec that only accepts non-empty strings
|
||||
nonEmptyString := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok || len(s) == 0 {
|
||||
return either.Left[string](fmt.Errorf("not a non-empty string"))
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) == 0 {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
// Create a codec that only accepts positive integers from strings
|
||||
positiveIntFromString := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](fmt.Errorf("not a positive integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "%d", &n)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "expected integer string")(err)(c)
|
||||
}
|
||||
if n <= 0 {
|
||||
return validation.FailureWithMessage[int](n, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
func(n int) string {
|
||||
return fmt.Sprintf("%d", n)
|
||||
},
|
||||
)
|
||||
|
||||
eitherCodec := Either(nonEmptyString, positiveIntFromString)
|
||||
|
||||
// Valid non-empty string
|
||||
validLeft := eitherCodec.Decode("hello")
|
||||
assert.True(t, either.IsRight(validLeft))
|
||||
|
||||
// Valid positive integer
|
||||
validRight := eitherCodec.Decode("42")
|
||||
assert.True(t, either.IsRight(validRight))
|
||||
|
||||
// Invalid empty string - should fail both validations
|
||||
invalidEmpty := eitherCodec.Decode("")
|
||||
assert.True(t, either.IsLeft(invalidEmpty))
|
||||
|
||||
// Invalid zero - should fail Right validation, succeed as Left
|
||||
zeroResult := eitherCodec.Decode("0")
|
||||
// "0" is a valid non-empty string, so it should succeed as Left
|
||||
assert.True(t, either.IsRight(zeroResult))
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherRoundTrip tests encoding and decoding round trips
|
||||
func TestEitherRoundTrip(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
intFromString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringCodec, intFromString)
|
||||
|
||||
t.Run("round-trip Left value", func(t *testing.T) {
|
||||
original := "hello"
|
||||
|
||||
// Decode
|
||||
decodeResult := eitherCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := eitherCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip Right value", func(t *testing.T) {
|
||||
original := "42"
|
||||
|
||||
// Decode
|
||||
decodeResult := eitherCodec.Decode(original)
|
||||
require.True(t, either.IsRight(decodeResult))
|
||||
|
||||
decoded := either.MonadFold(decodeResult,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Right[string](0) },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := eitherCodec.Encode(decoded)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherPrioritization tests that Right validation is prioritized over Left
|
||||
func TestEitherPrioritization(t *testing.T) {
|
||||
stringCodec := Id[string]()
|
||||
intFromString := IntFromString()
|
||||
|
||||
eitherCodec := Either(stringCodec, intFromString)
|
||||
|
||||
t.Run("prioritizes Right over Left when both could succeed", func(t *testing.T) {
|
||||
// "42" can be validated as both string (Left) and int (Right)
|
||||
// The codec should prioritize Right
|
||||
result := eitherCodec.Decode("42")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Should be Right because int validation succeeds and is prioritized
|
||||
assert.True(t, either.IsRight(value))
|
||||
|
||||
rightValue := either.MonadFold(value,
|
||||
func(string) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, rightValue)
|
||||
})
|
||||
|
||||
t.Run("falls back to Left when Right fails", func(t *testing.T) {
|
||||
// "hello" can only be validated as string (Left), not as int (Right)
|
||||
result := eitherCodec.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
|
||||
F.Identity[either.Either[string, int]],
|
||||
)
|
||||
|
||||
// Should be Left because int validation failed
|
||||
assert.True(t, either.IsLeft(value))
|
||||
|
||||
leftValue := either.MonadFold(value,
|
||||
F.Identity[string],
|
||||
func(int) string { return "" },
|
||||
)
|
||||
assert.Equal(t, "hello", leftValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherErrorAccumulation tests that errors from both branches are accumulated
|
||||
func TestEitherErrorAccumulation(t *testing.T) {
|
||||
// Create codecs with specific validation rules that will both fail
|
||||
nonEmptyString := MakeType(
|
||||
"NonEmptyString",
|
||||
func(u any) either.Either[error, string] {
|
||||
s, ok := u.(string)
|
||||
if !ok || len(s) == 0 {
|
||||
return either.Left[string](fmt.Errorf("not a non-empty string"))
|
||||
}
|
||||
return either.Of[error](s)
|
||||
},
|
||||
func(s string) Decode[Context, string] {
|
||||
return func(c Context) Validation[string] {
|
||||
if len(s) == 0 {
|
||||
return validation.FailureWithMessage[string](s, "must not be empty")(c)
|
||||
}
|
||||
return validation.Success(s)
|
||||
}
|
||||
},
|
||||
F.Identity[string],
|
||||
)
|
||||
|
||||
positiveIntFromString := MakeType(
|
||||
"PositiveInt",
|
||||
func(u any) either.Either[error, int] {
|
||||
i, ok := u.(int)
|
||||
if !ok || i <= 0 {
|
||||
return either.Left[int](fmt.Errorf("not a positive integer"))
|
||||
}
|
||||
return either.Of[error](i)
|
||||
},
|
||||
func(s string) Decode[Context, int] {
|
||||
return func(c Context) Validation[int] {
|
||||
var n int
|
||||
_, err := fmt.Sscanf(s, "%d", &n)
|
||||
if err != nil {
|
||||
return validation.FailureWithError[int](s, "expected integer string")(err)(c)
|
||||
}
|
||||
if n <= 0 {
|
||||
return validation.FailureWithMessage[int](n, "must be positive")(c)
|
||||
}
|
||||
return validation.Success(n)
|
||||
}
|
||||
},
|
||||
strconv.Itoa,
|
||||
)
|
||||
|
||||
eitherCodec := Either(nonEmptyString, positiveIntFromString)
|
||||
|
||||
t.Run("accumulates errors from both branches when both fail", func(t *testing.T) {
|
||||
// Empty string will fail both validations
|
||||
result := eitherCodec.Decode("")
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[validation.Errors],
|
||||
func(either.Either[string, int]) validation.Errors { return nil },
|
||||
)
|
||||
|
||||
require.NotNil(t, errors)
|
||||
// Should have errors from both string and int validation attempts
|
||||
assert.NotEmpty(t, errors)
|
||||
})
|
||||
}
|
||||
335
v2/optics/codec/validate/bind.go
Normal file
335
v2/optics/codec/validate/bind.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// 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 validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "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.
|
||||
// This is the starting point for building up a context using do-notation style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result struct {
|
||||
// x int
|
||||
// y string
|
||||
// }
|
||||
// result := Do(Result{})
|
||||
func Do[I, S any](
|
||||
empty S,
|
||||
) Validate[I, S] {
|
||||
return Of[I](empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
// This is used in do-notation style to sequentially build up a context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// Bind(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, func(s State) Validate[string, int] {
|
||||
// return Of[string](42)
|
||||
// }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
func Bind[I, S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f Kleisli[I, S1, A],
|
||||
) Operator[I, S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[I, S1, S2],
|
||||
Map[I, A, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value, not wrapped in Validate.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; computed int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{x: 5}),
|
||||
// Let[string](func(c int) func(State) State {
|
||||
// return func(s State) State { s.computed = c; return s }
|
||||
// }, func(s State) int { return s.x * 2 }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 5, computed: 10})
|
||||
func Let[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
f func(S1) B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.Let(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; name string }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// LetTo(func(n string) func(State) State {
|
||||
// return func(s State) State { s.name = n; return s }
|
||||
// }, "example"),
|
||||
// )
|
||||
func LetTo[I, S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
) Operator[I, S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[I, S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically used as the first operation after creating a Validate value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](42),
|
||||
// BindTo[string](func(x int) State { return State{value: x} }),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{value: 42})
|
||||
func BindTo[I, S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[I, T, S1] {
|
||||
return C.BindTo(
|
||||
Map[I, T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
|
||||
// This uses the applicative functor pattern, allowing parallel composition.
|
||||
//
|
||||
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
|
||||
// and the value. If both validations fail, all errors are collected and returned together.
|
||||
// This is useful for validating multiple independent fields and reporting all errors at once.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// decoder := F.Pipe2(
|
||||
// Do[string](State{}),
|
||||
// ApS(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, Of[string](42)),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(State{x: 42})
|
||||
//
|
||||
// Error aggregation example:
|
||||
//
|
||||
// // Both decoders fail - errors are aggregated
|
||||
// decoder1 := func(input string) Validation[State] {
|
||||
// return validation.Failures[State](/* errors */)
|
||||
// }
|
||||
// decoder2 := func(input string) Validation[int] {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// combined := ApS(setter, decoder2)(decoder1)
|
||||
// result := combined("input") // Contains BOTH sets of errors
|
||||
func ApS[I, S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Validate[I, T],
|
||||
) Operator[I, S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, I, T],
|
||||
Map[I, S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
|
||||
// and the value fail validation, all errors are collected and returned together.
|
||||
// This enables comprehensive error reporting for complex nested structures.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the Address field
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// // Use ApSL to update the address
|
||||
// decoder := F.Pipe2(
|
||||
// Of[string](Person{Name: "Alice"}),
|
||||
// ApSL(
|
||||
// addressLens,
|
||||
// Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
// ),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Person{...})
|
||||
func ApSL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Validate[I, T],
|
||||
) Operator[I, S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// a Validation that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) Validate[string, int] {
|
||||
// return func(input string) Validation[int] {
|
||||
// if v >= 100 {
|
||||
// return validation.Failures[int](/* errors */)
|
||||
// }
|
||||
// return validation.Success(v + 1)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 42}),
|
||||
// BindL(valueLens, increment),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 43})
|
||||
func BindL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[I, T, T],
|
||||
) Operator[I, S, S] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Validation).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Counter{Value: 21}),
|
||||
// LetL(valueLens, double),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Counter{Value: 42})
|
||||
func LetL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[I, S, S] {
|
||||
return Let[I](lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// decoder := F.Pipe1(
|
||||
// Of[string](Config{Debug: true, Timeout: 30}),
|
||||
// LetToL(debugLens, false),
|
||||
// )
|
||||
// result := decoder("input") // Returns validation.Success(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[I, S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[I, S, S] {
|
||||
return LetTo[I](lens.Set, b)
|
||||
}
|
||||
733
v2/optics/codec/validate/bind_test.go
Normal file
733
v2/optics/codec/validate/bind_test.go
Normal file
@@ -0,0 +1,733 @@
|
||||
// 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 validate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates successful validation with empty state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
validator := Do[string](State{})
|
||||
result := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, either.Of[Errors](State{}), result)
|
||||
})
|
||||
|
||||
t.Run("creates successful validation with initialized state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
initial := State{x: 42, y: "hello"}
|
||||
validator := Do[string](initial)
|
||||
result := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, either.Of[Errors](initial), result)
|
||||
})
|
||||
|
||||
t.Run("works with different input types", func(t *testing.T) {
|
||||
intValidator := Do[int](0)
|
||||
assert.Equal(t, either.Of[Errors](0), intValidator(42)(nil))
|
||||
|
||||
strValidator := Do[string]("")
|
||||
assert.Equal(t, either.Of[Errors](""), strValidator("test")(nil))
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customValidator := Do[[]byte](Custom{Value: 100})
|
||||
assert.Equal(t, either.Of[Errors](Custom{Value: 100}), customValidator([]byte("data"))(nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("binds successful validation to state", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 42, y: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](42)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "y failed"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "y failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("can access previous state values", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
return Of[string](10)
|
||||
}),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validate[string, int] {
|
||||
// y depends on x
|
||||
return Of[string](s.x * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 10, y: 20}), result)
|
||||
})
|
||||
|
||||
t.Run("can access input value", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[int](State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validate[int, int] {
|
||||
return func(input int) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Success(input * 2)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator(21)(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 42}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
computed int
|
||||
}
|
||||
|
||||
t.Run("attaches pure computation result to state", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, computed: 10}, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[State]] {
|
||||
return func(ctx Context) Validation[State] {
|
||||
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := Let[string](func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 })
|
||||
|
||||
result := validator(failure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Let operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
z int
|
||||
}
|
||||
validator := F.Pipe3(
|
||||
Do[string](State{x: 5}),
|
||||
Let[string](func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
Let[string](func(z int) func(State) State {
|
||||
return func(s State) State { s.z = z; return s }
|
||||
}, func(s State) int { return s.y + 10 }),
|
||||
Let[string](func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) int { return s.z * 3 }),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
name string
|
||||
}
|
||||
|
||||
t.Run("attaches constant value to state", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[string](State{x: 5}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example"),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, name: "example"}, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[State]] {
|
||||
return func(ctx Context) Validation[State] {
|
||||
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example")
|
||||
|
||||
result := validator(failure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple constant values", func(t *testing.T) {
|
||||
type State struct {
|
||||
name string
|
||||
version int
|
||||
active bool
|
||||
}
|
||||
validator := F.Pipe3(
|
||||
Do[string](State{}),
|
||||
LetTo[string](func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "app"),
|
||||
LetTo[string](func(v int) func(State) State {
|
||||
return func(s State) State { s.version = v; return s }
|
||||
}, 2),
|
||||
LetTo[string](func(a bool) func(State) State {
|
||||
return func(s State) State { s.active = a; return s }
|
||||
}, true),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{name: "app", version: 2, active: true}, value)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("initializes state from value", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Of[string](42),
|
||||
BindTo[string](func(x int) State { return State{value: x} }),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{value: 42}, value)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := BindTo[string](func(x int) State { return State{value: x} })
|
||||
|
||||
result := validator(failure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type StringState struct {
|
||||
text string
|
||||
}
|
||||
validator := F.Pipe1(
|
||||
Of[int]("hello"),
|
||||
BindTo[int](func(s string) StringState { return StringState{text: s} }),
|
||||
)
|
||||
|
||||
result := validator(42)(nil)
|
||||
assert.Equal(t, either.Of[Errors](StringState{text: "hello"}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("attaches value using applicative pattern", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](42)),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from both validations", func(t *testing.T) {
|
||||
stateFailure := func(input string) Reader[Context, Validation[State]] {
|
||||
return func(ctx Context) Validation[State] {
|
||||
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "state error"}})
|
||||
}
|
||||
}
|
||||
valueFailure := func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "value error"}})
|
||||
}
|
||||
}
|
||||
|
||||
validator := ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, valueFailure)
|
||||
|
||||
result := validator(stateFailure)("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "state error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("combines multiple ApS operations", func(t *testing.T) {
|
||||
validator := F.Pipe2(
|
||||
Do[string](State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Of[string](10)),
|
||||
ApS(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, Of[string](20)),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](State{x: 10, y: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
t.Run("updates nested structure using lens", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Person{Name: "Alice"}),
|
||||
ApSL(
|
||||
addressLens,
|
||||
Of[string](Address{Street: "Main St", City: "NYC"}),
|
||||
),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
expected := Person{
|
||||
Name: "Alice",
|
||||
Address: Address{Street: "Main St", City: "NYC"},
|
||||
}
|
||||
assert.Equal(t, either.Of[Errors](expected), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
personFailure := func(input string) Reader[Context, Validation[Person]] {
|
||||
return func(ctx Context) Validation[Person] {
|
||||
return validation.Failures[Person](Errors{&validation.ValidationError{Messsage: "person error"}})
|
||||
}
|
||||
}
|
||||
addressFailure := func(input string) Reader[Context, Validation[Address]] {
|
||||
return func(ctx Context) Validation[Address] {
|
||||
return validation.Failures[Address](Errors{&validation.ValidationError{Messsage: "address error"}})
|
||||
}
|
||||
}
|
||||
|
||||
validator := ApSL(addressLens, addressFailure)
|
||||
result := validator(personFailure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Person) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("updates field based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validate[string, int] {
|
||||
return Of[string](v + 1)
|
||||
}
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Counter{Value: 43}), result)
|
||||
})
|
||||
|
||||
t.Run("fails validation based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validate[string, int] {
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
if v >= 100 {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "exceeds limit"}})
|
||||
}
|
||||
return validation.Success(v + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Counter) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "exceeds limit", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
increment := func(v int) Validate[string, int] {
|
||||
return Of[string](v + 1)
|
||||
}
|
||||
|
||||
failure := func(input string) Reader[Context, Validation[Counter]] {
|
||||
return func(ctx Context) Validation[Counter] {
|
||||
return validation.Failures[Counter](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := BindL(valueLens, increment)
|
||||
result := validator(failure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("transforms field with pure function", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
validator := F.Pipe1(
|
||||
Of[string](Counter{Value: 21}),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Counter{Value: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
failure := func(input string) Reader[Context, Validation[Counter]] {
|
||||
return func(ctx Context) Validation[Counter] {
|
||||
return validation.Failures[Counter](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := LetL[string](valueLens, double)
|
||||
result := validator(failure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
add10 := func(v int) int { return v + 10 }
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
validator := F.Pipe2(
|
||||
Of[string](Counter{Value: 5}),
|
||||
LetL[string](valueLens, add10),
|
||||
LetL[string](valueLens, double),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Counter{Value: 30}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("sets field to constant value", func(t *testing.T) {
|
||||
validator := F.Pipe1(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Config{Debug: false, Timeout: 30}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := func(input string) Reader[Context, Validation[Config]] {
|
||||
return func(ctx Context) Validation[Config] {
|
||||
return validation.Failures[Config](Errors{&validation.ValidationError{Messsage: "error"}})
|
||||
}
|
||||
}
|
||||
validator := LetToL[string](debugLens, false)
|
||||
result := validator(failure)("input")(nil)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
validator := F.Pipe2(
|
||||
Of[string](Config{Debug: true, Timeout: 30}),
|
||||
LetToL[string](debugLens, false),
|
||||
LetToL[string](timeoutLens, 60),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.Equal(t, either.Of[Errors](Config{Debug: false, Timeout: 60}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindOperationsComposition(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
|
||||
validator := F.Pipe4(
|
||||
Do[string](User{}),
|
||||
LetTo[string](func(n string) func(User) User {
|
||||
return func(u User) User { u.Name = n; return u }
|
||||
}, "Alice"),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validate[string, int] {
|
||||
// Age validation
|
||||
if len(u.Name) > 0 {
|
||||
return Of[string](25)
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "name required"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
Let[string](func(e string) func(User) User {
|
||||
return func(u User) User { u.Email = e; return u }
|
||||
}, func(u User) string {
|
||||
// Derive email from name
|
||||
return u.Name + "@example.com"
|
||||
}),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validate[string, int] {
|
||||
// Validate age is positive
|
||||
if u.Age > 0 {
|
||||
return Of[string](u.Age)
|
||||
}
|
||||
return func(input string) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "age must be positive"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
expected := User{
|
||||
Name: "Alice",
|
||||
Age: 25,
|
||||
Email: "Alice@example.com",
|
||||
}
|
||||
assert.Equal(t, either.Of[Errors](expected), result)
|
||||
})
|
||||
|
||||
t.Run("validates with input-dependent logic", func(t *testing.T) {
|
||||
type Config struct {
|
||||
MaxValue int
|
||||
Value int
|
||||
}
|
||||
|
||||
validator := F.Pipe2(
|
||||
Do[int](Config{}),
|
||||
Bind(func(max int) func(Config) Config {
|
||||
return func(c Config) Config { c.MaxValue = max; return c }
|
||||
}, func(c Config) Validate[int, int] {
|
||||
// Extract max from input
|
||||
return func(input int) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
return validation.Success(input)
|
||||
}
|
||||
}
|
||||
}),
|
||||
Bind(func(val int) func(Config) Config {
|
||||
return func(c Config) Config { c.Value = val; return c }
|
||||
}, func(c Config) Validate[int, int] {
|
||||
// Validate value against max
|
||||
return func(input int) Reader[Context, Validation[int]] {
|
||||
return func(ctx Context) Validation[int] {
|
||||
if input/2 <= c.MaxValue {
|
||||
return validation.Success(input / 2)
|
||||
}
|
||||
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "value exceeds max"}})
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := validator(100)(nil)
|
||||
assert.Equal(t, either.Of[Errors](Config{MaxValue: 100, Value: 50}), result)
|
||||
})
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package validate
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
@@ -90,25 +91,72 @@ type (
|
||||
// "at user.address.zipCode: expected string, got number"
|
||||
Context = validation.Context
|
||||
|
||||
Decode[I, A any] = decode.Decode[I, A]
|
||||
|
||||
// Validate is a function that validates input I to produce type A with full context tracking.
|
||||
// Decode represents a decoding operation that transforms input I into output A
|
||||
// within a validation context.
|
||||
//
|
||||
// Type structure:
|
||||
// Validate[I, A] = Reader[I, Decode[Context, A]]
|
||||
// Decode[I, A] = Reader[Context, Validation[A]]
|
||||
//
|
||||
// This means:
|
||||
// 1. Takes an input of type I
|
||||
// 2. Returns a Reader that depends on validation Context
|
||||
// 3. That Reader produces a Validation[A] (Either[Errors, A])
|
||||
// 1. Takes a validation Context (path through nested structures)
|
||||
// 2. Returns a Validation[A] (Either[Errors, A])
|
||||
//
|
||||
// The layered structure enables:
|
||||
// - Access to the input value being validated
|
||||
// - Context tracking through nested structures
|
||||
// - Error accumulation with detailed paths
|
||||
// - Composition with other validators
|
||||
// Decode is used as the foundation for validation operations, providing:
|
||||
// - Context-aware error reporting with detailed paths
|
||||
// - Error accumulation across multiple validations
|
||||
// - Composable validation logic
|
||||
//
|
||||
// The Decode type is typically not used directly but through the Validate type,
|
||||
// which adds an additional Reader layer for accessing the input value.
|
||||
//
|
||||
// Example:
|
||||
// decoder := func(ctx Context) Validation[int] {
|
||||
// // Perform validation and return result
|
||||
// return validation.Success(42)
|
||||
// }
|
||||
// // decoder is a Decode[any, int]
|
||||
Decode[I, A any] = decode.Decode[I, A]
|
||||
|
||||
// Validate represents a composable validator that transforms input I to output A
|
||||
// with comprehensive error tracking and context propagation.
|
||||
//
|
||||
// # Type Structure
|
||||
//
|
||||
// Validate[I, A] = Reader[I, Decode[Context, A]]
|
||||
// = Reader[I, Reader[Context, Validation[A]]]
|
||||
// = func(I) func(Context) Either[Errors, A]
|
||||
//
|
||||
// This three-layer structure provides:
|
||||
// 1. Input access: The outer Reader[I, ...] gives access to the input value I
|
||||
// 2. Context tracking: The middle Reader[Context, ...] tracks the validation path
|
||||
// 3. Error handling: The inner Validation[A] accumulates errors or produces value A
|
||||
//
|
||||
// # Purpose
|
||||
//
|
||||
// Validate is the core type for building type-safe, composable validators that:
|
||||
// - Transform and validate data from one type to another
|
||||
// - Track the path through nested structures for detailed error messages
|
||||
// - Accumulate multiple validation errors instead of failing fast
|
||||
// - Compose with other validators using functional patterns
|
||||
//
|
||||
// # Key Features
|
||||
//
|
||||
// - Context-aware: Automatically tracks validation path (e.g., "user.address.zipCode")
|
||||
// - Error accumulation: Collects all validation errors, not just the first one
|
||||
// - Type-safe: Leverages Go's type system to ensure correctness
|
||||
// - Composable: Validators can be combined using Map, Chain, Ap, and other operators
|
||||
//
|
||||
// # Algebraic Structure
|
||||
//
|
||||
// Validate forms several algebraic structures:
|
||||
// - Functor: Transform successful results with Map
|
||||
// - Applicative: Combine independent validators in parallel with Ap
|
||||
// - Monad: Chain dependent validators sequentially with Chain
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Basic validator:
|
||||
//
|
||||
// Example usage:
|
||||
// validatePositive := func(n int) Reader[Context, Validation[int]] {
|
||||
// return func(ctx Context) Validation[int] {
|
||||
// if n > 0 {
|
||||
@@ -119,10 +167,33 @@ type (
|
||||
// }
|
||||
// // validatePositive is a Validate[int, int]
|
||||
//
|
||||
// The Validate type forms:
|
||||
// - A Functor: Can map over successful results
|
||||
// - An Applicative: Can combine validators in parallel
|
||||
// - A Monad: Can chain dependent validations
|
||||
// Composing validators:
|
||||
//
|
||||
// // Transform the result of a validator
|
||||
// doubled := Map[int, int, int](func(x int) int { return x * 2 })(validatePositive)
|
||||
//
|
||||
// // Chain dependent validations
|
||||
// validateRange := func(n int) Validate[int, int] {
|
||||
// return func(input int) Reader[Context, Validation[int]] {
|
||||
// return func(ctx Context) Validation[int] {
|
||||
// if n <= 100 {
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// return validation.FailureWithMessage[int](n, "must be <= 100")(ctx)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// combined := Chain(validateRange)(validatePositive)
|
||||
//
|
||||
// # Integration
|
||||
//
|
||||
// Validate integrates with the broader optics/codec ecosystem:
|
||||
// - Works with Decode for decoding operations
|
||||
// - Uses Validation for error handling
|
||||
// - Leverages Context for detailed error reporting
|
||||
// - Composes with other codec types for complete encode/decode pipelines
|
||||
//
|
||||
// See the package documentation for more examples and patterns.
|
||||
Validate[I, A any] = Reader[I, Decode[Context, A]]
|
||||
|
||||
// Errors is a collection of validation errors that occurred during validation.
|
||||
@@ -174,4 +245,30 @@ type (
|
||||
// // toUpper is an Operator[string, string, string]
|
||||
// // It can be applied to any string validator to uppercase the result
|
||||
Operator[I, A, B any] = Kleisli[I, Validate[I, A], B]
|
||||
|
||||
// Endomorphism represents a function from a type to itself.
|
||||
//
|
||||
// Type: Endomorphism[A] = func(A) A
|
||||
//
|
||||
// An endomorphism is a morphism (structure-preserving map) where the source
|
||||
// and target are the same type. In simpler terms, it's a function that takes
|
||||
// a value of type A and returns a value of the same type A.
|
||||
//
|
||||
// Endomorphisms are useful for:
|
||||
// - Transformations that preserve type (e.g., string normalization)
|
||||
// - Composable updates and modifications
|
||||
// - Building pipelines of same-type transformations
|
||||
// - Implementing the Monoid pattern (composition as binary operation)
|
||||
//
|
||||
// Endomorphisms form a Monoid under function composition:
|
||||
// - Identity: func(a A) A { return a }
|
||||
// - Concat: func(f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// return func(a A) A { return f(g(a)) }
|
||||
// }
|
||||
//
|
||||
// Example:
|
||||
// trim := strings.TrimSpace // Endomorphism[string]
|
||||
// lower := strings.ToLower // Endomorphism[string]
|
||||
// normalize := compose(trim, lower) // Endomorphism[string]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
)
|
||||
|
||||
@@ -309,6 +309,225 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
|
||||
)
|
||||
}
|
||||
|
||||
// ChainLeft sequences a computation on the failure (Left) channel of a validation.
|
||||
//
|
||||
// This function operates on the error path of validation, allowing you to transform,
|
||||
// enrich, or recover from validation failures. It's the dual of Chain - while Chain
|
||||
// operates on success values, ChainLeft operates on error values.
|
||||
//
|
||||
// # Key Behavior
|
||||
//
|
||||
// **Critical difference from standard Either operations**: This validation-specific
|
||||
// implementation **aggregates errors** using the Errors monoid. When the transformation
|
||||
// function returns a failure, both the original errors AND the new errors are combined,
|
||||
// ensuring comprehensive error reporting.
|
||||
//
|
||||
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
|
||||
// the success value passes through unchanged.
|
||||
//
|
||||
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
|
||||
// validation, converting Left to Right.
|
||||
//
|
||||
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
|
||||
// errors and the new errors are combined using the Errors monoid.
|
||||
//
|
||||
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
|
||||
// to the original input value I for context-aware error handling.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
|
||||
// is called only when validation fails, receiving the accumulated errors.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, A] that transforms validators by handling their error cases.
|
||||
//
|
||||
// # Example: Error Recovery
|
||||
//
|
||||
// // Validator that may fail
|
||||
// validatePositive := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// if n > 0 {
|
||||
// return validation.Success(n)
|
||||
// }
|
||||
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Recover from specific errors with a default value
|
||||
// withDefault := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "must be positive" {
|
||||
// return Of[int](0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return either.Left[int](errs)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// validator := withDefault(validatePositive)
|
||||
// result := validator(-5)(nil)
|
||||
// // Result: Success(0) - recovered from failure
|
||||
//
|
||||
// # Example: Error Context Addition
|
||||
//
|
||||
// // Add contextual information to errors
|
||||
// addContext := ChainLeft(func(errs Errors) Validate[string, int] {
|
||||
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// return either.Left[int](validation.Errors{
|
||||
// {
|
||||
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to validate user age",
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// validator := addContext(someValidator)
|
||||
// // Errors will include both original error and context
|
||||
//
|
||||
// # Example: Input-Dependent Recovery
|
||||
//
|
||||
// // Recover with different defaults based on input
|
||||
// smartDefault := ChainLeft(func(errs Errors) Validate[string, int] {
|
||||
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
// return func(ctx validation.Context) validation.Validation[int] {
|
||||
// // Use input to determine appropriate default
|
||||
// if strings.Contains(input, "http") {
|
||||
// return validation.Of(80)
|
||||
// }
|
||||
// if strings.Contains(input, "https") {
|
||||
// return validation.Of(443)
|
||||
// }
|
||||
// return validation.Of(8080)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
|
||||
// - The handler has access to both the errors and the original input
|
||||
// - Success values bypass the handler completely
|
||||
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
|
||||
// - Use OrElse as a semantic alias when emphasizing fallback/alternative logic
|
||||
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return readert.Chain[Validate[I, A]](
|
||||
decode.ChainLeft,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// OrElse provides an alternative validation when the primary validation fails.
|
||||
//
|
||||
// This is a semantic alias for ChainLeft with identical behavior. The name "OrElse"
|
||||
// emphasizes the intent of providing fallback or alternative validation logic, making
|
||||
// code more readable when that's the primary use case.
|
||||
//
|
||||
// # Relationship to ChainLeft
|
||||
//
|
||||
// **OrElse and ChainLeft are functionally identical** - they produce exactly the same
|
||||
// results for all inputs. The choice between them is purely about code readability:
|
||||
//
|
||||
// - Use **OrElse** when emphasizing fallback/alternative validation logic
|
||||
// - Use **ChainLeft** when emphasizing technical error channel transformation
|
||||
//
|
||||
// Both maintain the critical property of **error aggregation**, ensuring all validation
|
||||
// failures are preserved and reported together.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type
|
||||
// - A: The type of the validation result
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
|
||||
// is called only when validation fails, receiving the accumulated errors.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// An Operator[I, A, A] that transforms validators by providing alternative validation.
|
||||
//
|
||||
// # Example: Fallback Validation
|
||||
//
|
||||
// // Primary validator that may fail
|
||||
// validateFromConfig := func(key string) Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// // Try to get value from config
|
||||
// if value, ok := config[key]; ok {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return validation.FailureWithMessage[string](key, "not found in config")(ctx)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Use OrElse for semantic clarity - "try config, or else use environment"
|
||||
// withEnvFallback := OrElse(func(errs Errors) Validate[string, string] {
|
||||
// return func(key string) Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if value := os.Getenv(key); value != "" {
|
||||
// return validation.Success(value)
|
||||
// }
|
||||
// return either.Left[string](errs) // propagate original errors
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// validator := withEnvFallback(validateFromConfig)
|
||||
// result := validator("DATABASE_URL")(nil)
|
||||
// // Tries config first, falls back to environment variable
|
||||
//
|
||||
// # Example: Default Value on Failure
|
||||
//
|
||||
// // Provide a default value when validation fails
|
||||
// withDefault := OrElse(func(errs Errors) Validate[int, int] {
|
||||
// return Of[int](0) // default to 0 on any failure
|
||||
// })
|
||||
//
|
||||
// validator := withDefault(someValidator)
|
||||
// result := validator(input)(nil)
|
||||
// // Always succeeds, using default value if validation fails
|
||||
//
|
||||
// # Example: Pipeline with Multiple Fallbacks
|
||||
//
|
||||
// // Build a validation pipeline with multiple fallback strategies
|
||||
// validator := F.Pipe2(
|
||||
// validateFromDatabase,
|
||||
// OrElse(func(errs Errors) Validate[string, Config] {
|
||||
// // Try cache as first fallback
|
||||
// return validateFromCache
|
||||
// }),
|
||||
// OrElse(func(errs Errors) Validate[string, Config] {
|
||||
// // Use default config as final fallback
|
||||
// return Of[string](defaultConfig)
|
||||
// }),
|
||||
// )
|
||||
// // Tries database, then cache, then default
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Identical behavior to ChainLeft - they are aliases
|
||||
// - Errors are accumulated when transformations fail
|
||||
// - Success values pass through unchanged
|
||||
// - The handler has access to both errors and original input
|
||||
// - Choose OrElse for better readability when providing alternatives
|
||||
// - See ChainLeft documentation for detailed behavior and additional examples
|
||||
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
// MonadAp applies a validator containing a function to a validator containing a value.
|
||||
//
|
||||
// This is the applicative apply operation for Validate. It allows you to apply
|
||||
|
||||
@@ -849,3 +849,428 @@ func TestFunctorLaws(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestChainLeft tests the ChainLeft function
|
||||
func TestChainLeft(t *testing.T) {
|
||||
t.Run("transforms failures while preserving successes", func(t *testing.T) {
|
||||
// Create a failing validator
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "validation failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler that recovers from specific errors
|
||||
handler := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "validation failed" {
|
||||
return Of[int](0) // recover with default
|
||||
}
|
||||
}
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(failingValidator)
|
||||
result := validator(-5)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(0), result, "Should recover from failure")
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successValidator := Of[int](42)
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "should not be called")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(successValidator)
|
||||
result := validator(100)(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result, "Success should pass through unchanged")
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "original error")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
handler := ChainLeft(func(errs Errors) Validate[string, string] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](input, "additional error")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(failingValidator)
|
||||
result := validator("test")(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "original error")
|
||||
assert.Contains(t, messages, "additional error")
|
||||
})
|
||||
|
||||
t.Run("adds context to errors", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "invalid value")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
addContext := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return E.Left[int](validation.Errors{
|
||||
{
|
||||
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
Messsage: "failed to validate user age",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := addContext(failingValidator)
|
||||
result := validator(150)(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 2, "Should have both original and context errors")
|
||||
})
|
||||
|
||||
t.Run("can be composed in pipeline", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "error1")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
handler1 := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "error2")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
handler2 := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "error3")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler2(handler1(failingValidator))
|
||||
result := validator(42)(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.GreaterOrEqual(t, len(errors), 2, "Should accumulate errors through pipeline")
|
||||
})
|
||||
|
||||
t.Run("provides access to original input", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler uses input to determine recovery strategy
|
||||
handler := ChainLeft(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
// Use input value to decide on recovery
|
||||
if input < 0 {
|
||||
return validation.Of(0)
|
||||
}
|
||||
if input > 100 {
|
||||
return validation.Of(100)
|
||||
}
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(failingValidator)
|
||||
|
||||
result1 := validator(-10)(nil)
|
||||
assert.Equal(t, validation.Of(0), result1, "Should recover negative to 0")
|
||||
|
||||
result2 := validator(150)(nil)
|
||||
assert.Equal(t, validation.Of(100), result2, "Should recover large to 100")
|
||||
})
|
||||
|
||||
t.Run("works with different input and output types", func(t *testing.T) {
|
||||
// Validator that converts string to int
|
||||
parseValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Handler that provides default based on input string
|
||||
handler := ChainLeft(func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if input == "default" {
|
||||
return validation.Of(42)
|
||||
}
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := handler(parseValidator)
|
||||
result := validator("default")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrElse tests the OrElse function
|
||||
func TestOrElse(t *testing.T) {
|
||||
t.Run("provides fallback for failing validation", func(t *testing.T) {
|
||||
// Primary validator that fails
|
||||
primaryValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "not found")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Use OrElse to provide fallback
|
||||
withFallback := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return Of[string]("default value")
|
||||
})
|
||||
|
||||
validator := withFallback(primaryValidator)
|
||||
result := validator("missing")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("default value"), result)
|
||||
})
|
||||
|
||||
t.Run("preserves success values unchanged", func(t *testing.T) {
|
||||
successValidator := Of[string]("success")
|
||||
|
||||
withFallback := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return Of[string]("fallback")
|
||||
})
|
||||
|
||||
validator := withFallback(successValidator)
|
||||
result := validator("input")(nil)
|
||||
|
||||
assert.Equal(t, validation.Of("success"), result, "Should not use fallback for success")
|
||||
})
|
||||
|
||||
t.Run("aggregates errors when fallback also fails", func(t *testing.T) {
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "primary failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
withFallback := OrElse(func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](input, "fallback failed")(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := withFallback(failingValidator)
|
||||
result := validator(42)(nil)
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, errors := E.Unwrap(result)
|
||||
assert.Len(t, errors, 2, "Should aggregate both errors")
|
||||
|
||||
messages := make([]string, len(errors))
|
||||
for i, err := range errors {
|
||||
messages[i] = err.Messsage
|
||||
}
|
||||
assert.Contains(t, messages, "primary failed")
|
||||
assert.Contains(t, messages, "fallback failed")
|
||||
})
|
||||
|
||||
t.Run("supports multiple fallback strategies", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
return validation.FailureWithMessage[string](s, "not in database")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// First fallback: try cache
|
||||
tryCache := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[string]] {
|
||||
return func(ctx validation.Context) validation.Validation[string] {
|
||||
if input == "cached" {
|
||||
return validation.Of("from cache")
|
||||
}
|
||||
return E.Left[string](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Second fallback: use default
|
||||
useDefault := OrElse(func(errs Errors) Validate[string, string] {
|
||||
return Of[string]("default")
|
||||
})
|
||||
|
||||
// Compose fallbacks
|
||||
validator := useDefault(tryCache(failingValidator))
|
||||
|
||||
// Test with cached value
|
||||
result1 := validator("cached")(nil)
|
||||
assert.Equal(t, validation.Of("from cache"), result1)
|
||||
|
||||
// Test with non-cached value (should use default)
|
||||
result2 := validator("other")(nil)
|
||||
assert.Equal(t, validation.Of("default"), result2)
|
||||
})
|
||||
|
||||
t.Run("provides input-dependent fallback", func(t *testing.T) {
|
||||
failingValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback with different defaults based on input
|
||||
smartFallback := OrElse(func(errs Errors) Validate[string, int] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
// Provide context-aware defaults
|
||||
if input == "http" {
|
||||
return validation.Of(80)
|
||||
}
|
||||
if input == "https" {
|
||||
return validation.Of(443)
|
||||
}
|
||||
return validation.Of(8080)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
validator := smartFallback(failingValidator)
|
||||
|
||||
result1 := validator("http")(nil)
|
||||
assert.Equal(t, validation.Of(80), result1)
|
||||
|
||||
result2 := validator("https")(nil)
|
||||
assert.Equal(t, validation.Of(443), result2)
|
||||
|
||||
result3 := validator("other")(nil)
|
||||
assert.Equal(t, validation.Of(8080), result3)
|
||||
})
|
||||
|
||||
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
|
||||
// Create identical handlers
|
||||
handler := func(errs Errors) Validate[int, int] {
|
||||
return func(input int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
if input < 0 {
|
||||
return validation.Of(0)
|
||||
}
|
||||
return E.Left[int](errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
|
||||
return func(ctx validation.Context) validation.Validation[int] {
|
||||
return validation.FailureWithMessage[int](n, "failed")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply with ChainLeft
|
||||
withChainLeft := ChainLeft(handler)(failingValidator)
|
||||
|
||||
// Apply with OrElse
|
||||
withOrElse := OrElse(handler)(failingValidator)
|
||||
|
||||
// Test with same inputs
|
||||
inputs := []int{-10, 0, 10, -5, 100}
|
||||
for _, input := range inputs {
|
||||
result1 := withChainLeft(input)(nil)
|
||||
result2 := withOrElse(input)(nil)
|
||||
|
||||
// Results should be identical
|
||||
assert.Equal(t, E.IsLeft(result1), E.IsLeft(result2))
|
||||
if E.IsRight(result1) {
|
||||
val1, _ := E.Unwrap(result1)
|
||||
val2, _ := E.Unwrap(result2)
|
||||
assert.Equal(t, val1, val2, "OrElse and ChainLeft should produce identical results")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works in complex validation pipeline", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
Host string
|
||||
}
|
||||
|
||||
// Validator that tries to parse config
|
||||
parseConfig := func(s string) Reader[validation.Context, validation.Validation[Config]] {
|
||||
return func(ctx validation.Context) validation.Validation[Config] {
|
||||
return validation.FailureWithMessage[Config](s, "invalid config")(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to environment variables
|
||||
tryEnv := OrElse(func(errs Errors) Validate[string, Config] {
|
||||
return func(input string) Reader[validation.Context, validation.Validation[Config]] {
|
||||
return func(ctx validation.Context) validation.Validation[Config] {
|
||||
// Simulate env var lookup
|
||||
if input == "from_env" {
|
||||
return validation.Of(Config{Port: 8080, Host: "localhost"})
|
||||
}
|
||||
return E.Left[Config](errs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Final fallback to defaults
|
||||
useDefaults := OrElse(func(errs Errors) Validate[string, Config] {
|
||||
return Of[string](Config{Port: 3000, Host: "0.0.0.0"})
|
||||
})
|
||||
|
||||
// Build pipeline
|
||||
validator := useDefaults(tryEnv(parseConfig))
|
||||
|
||||
// Test with env fallback
|
||||
result1 := validator("from_env")(nil)
|
||||
assert.True(t, E.IsRight(result1))
|
||||
if E.IsRight(result1) {
|
||||
cfg, _ := E.Unwrap(result1)
|
||||
assert.Equal(t, 8080, cfg.Port)
|
||||
assert.Equal(t, "localhost", cfg.Host)
|
||||
}
|
||||
|
||||
// Test with default fallback
|
||||
result2 := validator("other")(nil)
|
||||
assert.True(t, E.IsRight(result2))
|
||||
if E.IsRight(result2) {
|
||||
cfg, _ := E.Unwrap(result2)
|
||||
assert.Equal(t, 3000, cfg.Port)
|
||||
assert.Equal(t, "0.0.0.0", cfg.Host)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
162
v2/optics/codec/validation/OrElse_explanation.md
Normal file
162
v2/optics/codec/validation/OrElse_explanation.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# OrElse is Equivalent to ChainLeft
|
||||
|
||||
## Overview
|
||||
|
||||
In [`optics/codec/validation/monad.go`](monad.go:474-476), the [`OrElse`](monad.go:474) function is defined as a simple alias for [`ChainLeft`](monad.go:304):
|
||||
|
||||
```go
|
||||
//go:inline
|
||||
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
```
|
||||
|
||||
This means **`OrElse` and `ChainLeft` are functionally identical** - they produce exactly the same results for all inputs.
|
||||
|
||||
## Why Have Both?
|
||||
|
||||
While they are technically the same, they serve different **semantic purposes**:
|
||||
|
||||
### ChainLeft - Technical Perspective
|
||||
[`ChainLeft`](monad.go:304-309) emphasizes the **technical operation**: it chains a computation on the Left (failure) channel of the Either/Validation monad. This name comes from category theory and functional programming terminology.
|
||||
|
||||
### OrElse - Semantic Perspective
|
||||
[`OrElse`](monad.go:474-476) emphasizes the **intent**: it provides an alternative or fallback when validation fails. The name reads naturally in code: "try this validation, **or else** try this alternative."
|
||||
|
||||
## Key Behavior
|
||||
|
||||
Both functions share the same critical behavior that distinguishes them from standard Either operations:
|
||||
|
||||
### Error Aggregation
|
||||
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid. This ensures no validation errors are lost.
|
||||
|
||||
```go
|
||||
// Example: Error aggregation
|
||||
result := OrElse(func(errs Errors) Validation[string] {
|
||||
return Failures[string](Errors{
|
||||
&ValidationError{Messsage: "additional error"},
|
||||
})
|
||||
})(Failures[string](Errors{
|
||||
&ValidationError{Messsage: "original error"},
|
||||
}))
|
||||
|
||||
// Result contains BOTH errors: ["original error", "additional error"]
|
||||
```
|
||||
|
||||
### Success Pass-Through
|
||||
Success values pass through unchanged - the function is never called:
|
||||
|
||||
```go
|
||||
result := OrElse(func(errs Errors) Validation[int] {
|
||||
return Failures[int](Errors{
|
||||
&ValidationError{Messsage: "never called"},
|
||||
})
|
||||
})(Success(42))
|
||||
|
||||
// Result: Success(42) - unchanged
|
||||
```
|
||||
|
||||
### Error Recovery
|
||||
The function can recover from failures by returning a Success:
|
||||
|
||||
```go
|
||||
recoverFromNotFound := OrElse(func(errs Errors) Validation[int] {
|
||||
for _, err := range errs {
|
||||
if err.Messsage == "not found" {
|
||||
return Success(0) // recover with default
|
||||
}
|
||||
}
|
||||
return Failures[int](errs)
|
||||
})
|
||||
|
||||
result := recoverFromNotFound(Failures[int](Errors{
|
||||
&ValidationError{Messsage: "not found"},
|
||||
}))
|
||||
|
||||
// Result: Success(0) - recovered from failure
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Fallback Validation (OrElse reads better)
|
||||
```go
|
||||
validatePositive := func(x int) Validation[int] {
|
||||
if x > 0 {
|
||||
return Success(x)
|
||||
}
|
||||
return Failures[int](Errors{
|
||||
&ValidationError{Messsage: "must be positive"},
|
||||
})
|
||||
}
|
||||
|
||||
// Use OrElse for semantic clarity
|
||||
withDefault := OrElse(func(errs Errors) Validation[int] {
|
||||
return Success(1) // default to 1 if validation fails
|
||||
})
|
||||
|
||||
result := F.Pipe1(validatePositive(-5), withDefault)
|
||||
// Result: Success(1)
|
||||
```
|
||||
|
||||
### 2. Error Context Addition (ChainLeft reads better)
|
||||
```go
|
||||
addContext := ChainLeft(func(errs Errors) Validation[string] {
|
||||
return Failures[string](Errors{
|
||||
&ValidationError{
|
||||
Messsage: "validation failed in user.email field",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
result := F.Pipe1(
|
||||
Failures[string](Errors{
|
||||
&ValidationError{Messsage: "invalid format"},
|
||||
}),
|
||||
addContext,
|
||||
)
|
||||
// Result contains: ["invalid format", "validation failed in user.email field"]
|
||||
```
|
||||
|
||||
### 3. Pipeline Composition
|
||||
Both can be used in pipelines, with errors accumulating at each step:
|
||||
|
||||
```go
|
||||
result := F.Pipe2(
|
||||
Failures[int](Errors{
|
||||
&ValidationError{Messsage: "database error"},
|
||||
}),
|
||||
OrElse(func(errs Errors) Validation[int] {
|
||||
return Failures[int](Errors{
|
||||
&ValidationError{Messsage: "context added"},
|
||||
})
|
||||
}),
|
||||
OrElse(func(errs Errors) Validation[int] {
|
||||
return Failures[int](errs) // propagate
|
||||
}),
|
||||
)
|
||||
// Errors accumulate at each step in the pipeline
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
The test suite in [`monad_test.go`](monad_test.go:1698) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
|
||||
|
||||
- ✅ Identical behavior for Success values
|
||||
- ✅ Identical behavior for error recovery
|
||||
- ✅ Identical behavior for error aggregation
|
||||
- ✅ Identical behavior in pipeline composition
|
||||
- ✅ Identical behavior for multiple error scenarios
|
||||
|
||||
Run the tests:
|
||||
```bash
|
||||
go test -v -run TestOrElse ./optics/codec/validation
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**:
|
||||
|
||||
- Use **`OrElse`** when emphasizing fallback/alternative validation logic
|
||||
- Use **`ChainLeft`** when emphasizing technical error channel transformation
|
||||
|
||||
Both maintain the critical validation property of **error aggregation**, ensuring all validation failures are preserved and reported together.
|
||||
318
v2/optics/codec/validation/bind.go
Normal file
318
v2/optics/codec/validation/bind.go
Normal file
@@ -0,0 +1,318 @@
|
||||
// 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 validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "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.
|
||||
// This is the starting point for building up a context using do-notation style.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Result struct {
|
||||
// x int
|
||||
// y string
|
||||
// }
|
||||
// result := Do(Result{})
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Validation[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context S1 to produce a context S2.
|
||||
// This is used in do-notation style to sequentially build up a context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{}),
|
||||
// Bind(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, func(s State) Validation[int] { return Success(42) }),
|
||||
// )
|
||||
func Bind[S1, S2, A any](
|
||||
setter func(A) func(S1) S2,
|
||||
f Kleisli[S1, A],
|
||||
) Operator[S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[A, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
|
||||
// Unlike Bind, the computation function returns a plain value, not an Option.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; computed int }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// Let(func(c int) func(State) State {
|
||||
// return func(s State) State { s.computed = c; return s }
|
||||
// }, func(s State) int { return s.x * 2 }),
|
||||
// )
|
||||
func Let[S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
f func(S1) B,
|
||||
) Operator[S1, S2] {
|
||||
return F.Let(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo attaches a constant value to a context S1 to produce a context S2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; name string }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{x: 5}),
|
||||
// LetTo(func(n string) func(State) State {
|
||||
// return func(s State) State { s.name = n; return s }
|
||||
// }, "example"),
|
||||
// )
|
||||
func LetTo[S1, S2, B any](
|
||||
key func(B) func(S1) S2,
|
||||
b B,
|
||||
) Operator[S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state S1 from a value T.
|
||||
// This is typically used as the first operation after creating a Validation value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { value int }
|
||||
// result := F.Pipe1(
|
||||
// Success(42),
|
||||
// BindTo(func(x int) State { return State{value: x} }),
|
||||
// )
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return C.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
|
||||
// This uses the applicative functor pattern, allowing parallel composition.
|
||||
//
|
||||
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
|
||||
// and the value. If both validations fail, all errors are collected and returned together.
|
||||
// This is useful for validating multiple independent fields and reporting all errors at once.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct { x int; y int }
|
||||
// result := F.Pipe2(
|
||||
// Do(State{}),
|
||||
// ApS(func(x int) func(State) State {
|
||||
// return func(s State) State { s.x = x; return s }
|
||||
// }, Success(42)),
|
||||
// )
|
||||
//
|
||||
// Error aggregation example:
|
||||
//
|
||||
// stateFailure := Failures[State](Errors{&ValidationError{Messsage: "state error"}})
|
||||
// valueFailure := Failures[int](Errors{&ValidationError{Messsage: "value error"}})
|
||||
// result := ApS(setter, valueFailure)(stateFailure)
|
||||
// // Result contains BOTH errors: ["state error", "value error"]
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Validation[T],
|
||||
) Operator[S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL attaches a value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines ApS with a lens, allowing you to use
|
||||
// optics to update nested structures in a more composable way.
|
||||
//
|
||||
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
|
||||
// and the value fail validation, all errors are collected and returned together.
|
||||
// This enables comprehensive error reporting for complex nested structures.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// This eliminates the need to manually write setter functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Address struct {
|
||||
// Street string
|
||||
// City string
|
||||
// }
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Address Address
|
||||
// }
|
||||
//
|
||||
// // Create a lens for the Address field
|
||||
// addressLens := lens.MakeLens(
|
||||
// func(p Person) Address { return p.Address },
|
||||
// func(p Person, a Address) Person { p.Address = a; return p },
|
||||
// )
|
||||
//
|
||||
// // Use ApSL to update the address
|
||||
// result := F.Pipe2(
|
||||
// Success(Person{Name: "Alice"}),
|
||||
// ApSL(
|
||||
// addressLens,
|
||||
// Success(Address{Street: "Main St", City: "NYC"}),
|
||||
// ),
|
||||
// )
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa Validation[T],
|
||||
) Operator[S, S] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL attaches the result of a computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Bind with a lens, allowing you to use
|
||||
// optics to update nested structures based on their current values.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The computation function f receives the current value of the focused field and returns
|
||||
// a Validation that produces the new value.
|
||||
//
|
||||
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
|
||||
// the current value of the focused field.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Increment the counter, but fail if it would exceed 100
|
||||
// increment := func(v int) Validation[int] {
|
||||
// if v >= 100 {
|
||||
// return Failures[int](Errors{&ValidationError{Messsage: "exceeds limit"}})
|
||||
// }
|
||||
// return Success(v + 1)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Success(Counter{Value: 42}),
|
||||
// BindL(valueLens, increment),
|
||||
// ) // Success(Counter{Value: 43})
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Operator[S, S] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL attaches the result of a pure computation to a context using a lens-based setter.
|
||||
// This is a convenience function that combines Let with a lens, allowing you to use
|
||||
// optics to update nested structures with pure transformations.
|
||||
//
|
||||
// The lens parameter provides both the getter and setter for a field within the structure S.
|
||||
// The transformation function f receives the current value of the focused field and returns
|
||||
// the new value directly (not wrapped in Validation).
|
||||
//
|
||||
// This is useful for pure transformations that cannot fail, such as mathematical operations,
|
||||
// string manipulations, or other deterministic updates.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.MakeLens(
|
||||
// func(c Counter) int { return c.Value },
|
||||
// func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
// )
|
||||
//
|
||||
// // Double the counter value
|
||||
// double := func(v int) int { return v * 2 }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Success(Counter{Value: 21}),
|
||||
// LetL(valueLens, double),
|
||||
// ) // Success(Counter{Value: 42})
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Operator[S, S] {
|
||||
return Let(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL attaches a constant value to a context using a lens-based setter.
|
||||
// This is a convenience function that combines LetTo with a lens, allowing you to use
|
||||
// optics to set nested fields to specific values.
|
||||
//
|
||||
// The lens parameter provides the setter for a field within the structure S.
|
||||
// Unlike LetL which transforms the current value, LetToL simply replaces it with
|
||||
// the provided constant value b.
|
||||
//
|
||||
// This is useful for resetting fields, initializing values, or setting fields to
|
||||
// predetermined constants.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// Debug bool
|
||||
// Timeout int
|
||||
// }
|
||||
//
|
||||
// debugLens := lens.MakeLens(
|
||||
// func(c Config) bool { return c.Debug },
|
||||
// func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
// )
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Success(Config{Debug: true, Timeout: 30}),
|
||||
// LetToL(debugLens, false),
|
||||
// ) // Success(Config{Debug: false, Timeout: 30})
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
) Operator[S, S] {
|
||||
return LetTo(lens.Set, b)
|
||||
}
|
||||
540
v2/optics/codec/validation/bind_test.go
Normal file
540
v2/optics/codec/validation/bind_test.go
Normal file
@@ -0,0 +1,540 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates successful validation with empty state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
result := Do(State{})
|
||||
|
||||
assert.Equal(t, Of(State{}), result)
|
||||
})
|
||||
|
||||
t.Run("creates successful validation with initialized state", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y string
|
||||
}
|
||||
initial := State{x: 42, y: "hello"}
|
||||
result := Do(initial)
|
||||
|
||||
assert.Equal(t, Of(initial), result)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
intResult := Do(0)
|
||||
assert.Equal(t, Of(0), intResult)
|
||||
|
||||
strResult := Do("")
|
||||
assert.Equal(t, Of(""), strResult)
|
||||
|
||||
type Custom struct{ Value int }
|
||||
customResult := Do(Custom{Value: 100})
|
||||
assert.Equal(t, Of(Custom{Value: 100}), customResult)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("binds successful validation to state", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validation[int] { return Success(42) }),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validation[int] { return Success(10) }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 42, y: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validation[int] { return Success(42) }),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validation[int] {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "y failed"}})
|
||||
}),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "y failed", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("can access previous state values", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
Bind(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) Validation[int] { return Success(10) }),
|
||||
Bind(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) Validation[int] {
|
||||
// y depends on x
|
||||
return Success(s.x * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
assert.Equal(t, Success(State{x: 10, y: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
computed int
|
||||
}
|
||||
|
||||
t.Run("attaches pure computation result to state", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Do(State{x: 5}),
|
||||
Let(func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 5, computed: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[State](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := Let(func(c int) func(State) State {
|
||||
return func(s State) State { s.computed = c; return s }
|
||||
}, func(s State) int { return s.x * 2 })(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("chains multiple Let operations", func(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
z int
|
||||
}
|
||||
result := F.Pipe3(
|
||||
Do(State{x: 5}),
|
||||
Let(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, func(s State) int { return s.x * 2 }),
|
||||
Let(func(z int) func(State) State {
|
||||
return func(s State) State { s.z = z; return s }
|
||||
}, func(s State) int { return s.y + 10 }),
|
||||
Let(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, func(s State) int { return s.z * 3 }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 60, y: 10, z: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
name string
|
||||
}
|
||||
|
||||
t.Run("attaches constant value to state", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Do(State{x: 5}),
|
||||
LetTo(func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example"),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 5, name: "example"}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[State](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := LetTo(func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "example")(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple constant values", func(t *testing.T) {
|
||||
type State struct {
|
||||
name string
|
||||
version int
|
||||
active bool
|
||||
}
|
||||
result := F.Pipe3(
|
||||
Do(State{}),
|
||||
LetTo(func(n string) func(State) State {
|
||||
return func(s State) State { s.name = n; return s }
|
||||
}, "app"),
|
||||
LetTo(func(v int) func(State) State {
|
||||
return func(s State) State { s.version = v; return s }
|
||||
}, 2),
|
||||
LetTo(func(a bool) func(State) State {
|
||||
return func(s State) State { s.active = a; return s }
|
||||
}, true),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{name: "app", version: 2, active: true}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
}
|
||||
|
||||
t.Run("initializes state from value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Success(42),
|
||||
BindTo(func(x int) State { return State{value: x} }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{value: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[int](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := BindTo(func(x int) State { return State{value: x} })(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "error", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
type StringState struct {
|
||||
text string
|
||||
}
|
||||
result := F.Pipe1(
|
||||
Success("hello"),
|
||||
BindTo(func(s string) StringState { return StringState{text: s} }),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(StringState{text: "hello"}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
type State struct {
|
||||
x int
|
||||
y int
|
||||
}
|
||||
|
||||
t.Run("attaches value using applicative pattern", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Do(State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Success(42)),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors from both validations", func(t *testing.T) {
|
||||
stateFailure := Failures[State](Errors{&ValidationError{Messsage: "state error"}})
|
||||
valueFailure := Failures[int](Errors{&ValidationError{Messsage: "value error"}})
|
||||
|
||||
result := ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, valueFailure)(stateFailure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(State) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
messages := []string{errors[0].Messsage, errors[1].Messsage}
|
||||
assert.Contains(t, messages, "state error")
|
||||
assert.Contains(t, messages, "value error")
|
||||
})
|
||||
|
||||
t.Run("combines multiple ApS operations", func(t *testing.T) {
|
||||
result := F.Pipe2(
|
||||
Do(State{}),
|
||||
ApS(func(x int) func(State) State {
|
||||
return func(s State) State { s.x = x; return s }
|
||||
}, Success(10)),
|
||||
ApS(func(y int) func(State) State {
|
||||
return func(s State) State { s.y = y; return s }
|
||||
}, Success(20)),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(State{x: 10, y: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApSL(t *testing.T) {
|
||||
type Address struct {
|
||||
Street string
|
||||
City string
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Address Address
|
||||
}
|
||||
|
||||
t.Run("updates nested structure using lens", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Person{Name: "Alice"}),
|
||||
ApSL(
|
||||
addressLens,
|
||||
Success(Address{Street: "Main St", City: "NYC"}),
|
||||
),
|
||||
)
|
||||
|
||||
expected := Person{
|
||||
Name: "Alice",
|
||||
Address: Address{Street: "Main St", City: "NYC"},
|
||||
}
|
||||
assert.Equal(t, Of(expected), result)
|
||||
})
|
||||
|
||||
t.Run("accumulates errors", func(t *testing.T) {
|
||||
addressLens := L.MakeLens(
|
||||
func(p Person) Address { return p.Address },
|
||||
func(p Person, a Address) Person { p.Address = a; return p },
|
||||
)
|
||||
|
||||
personFailure := Failures[Person](Errors{&ValidationError{Messsage: "person error"}})
|
||||
addressFailure := Failures[Address](Errors{&ValidationError{Messsage: "address error"}})
|
||||
|
||||
result := ApSL(addressLens, addressFailure)(personFailure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Person) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("updates field based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validation[int] {
|
||||
return Success(v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Counter{Value: 42}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Counter{Value: 43}), result)
|
||||
})
|
||||
|
||||
t.Run("fails validation based on current value", func(t *testing.T) {
|
||||
increment := func(v int) Validation[int] {
|
||||
if v >= 100 {
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "exceeds limit"}})
|
||||
}
|
||||
return Success(v + 1)
|
||||
}
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Counter{Value: 100}),
|
||||
BindL(valueLens, increment),
|
||||
)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result,
|
||||
F.Identity[Errors],
|
||||
func(Counter) Errors { return nil },
|
||||
)
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "exceeds limit", errors[0].Messsage)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
increment := func(v int) Validation[int] {
|
||||
return Success(v + 1)
|
||||
}
|
||||
|
||||
failure := Failures[Counter](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := BindL(valueLens, increment)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetL(t *testing.T) {
|
||||
type Counter struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
valueLens := L.MakeLens(
|
||||
func(c Counter) int { return c.Value },
|
||||
func(c Counter, v int) Counter { c.Value = v; return c },
|
||||
)
|
||||
|
||||
t.Run("transforms field with pure function", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe1(
|
||||
Success(Counter{Value: 21}),
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Counter{Value: 42}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
failure := Failures[Counter](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := LetL(valueLens, double)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("chains multiple transformations", func(t *testing.T) {
|
||||
add10 := func(v int) int { return v + 10 }
|
||||
double := func(v int) int { return v * 2 }
|
||||
|
||||
result := F.Pipe2(
|
||||
Success(Counter{Value: 5}),
|
||||
LetL(valueLens, add10),
|
||||
LetL(valueLens, double),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Counter{Value: 30}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLetToL(t *testing.T) {
|
||||
type Config struct {
|
||||
Debug bool
|
||||
Timeout int
|
||||
}
|
||||
|
||||
debugLens := L.MakeLens(
|
||||
func(c Config) bool { return c.Debug },
|
||||
func(c Config, d bool) Config { c.Debug = d; return c },
|
||||
)
|
||||
|
||||
t.Run("sets field to constant value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Success(Config{Debug: true, Timeout: 30}),
|
||||
LetToL(debugLens, false),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Config{Debug: false, Timeout: 30}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
failure := Failures[Config](Errors{&ValidationError{Messsage: "error"}})
|
||||
result := LetToL(debugLens, false)(failure)
|
||||
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("sets multiple fields", func(t *testing.T) {
|
||||
timeoutLens := L.MakeLens(
|
||||
func(c Config) int { return c.Timeout },
|
||||
func(c Config, t int) Config { c.Timeout = t; return c },
|
||||
)
|
||||
|
||||
result := F.Pipe2(
|
||||
Success(Config{Debug: true, Timeout: 30}),
|
||||
LetToL(debugLens, false),
|
||||
LetToL(timeoutLens, 60),
|
||||
)
|
||||
|
||||
assert.Equal(t, Of(Config{Debug: false, Timeout: 60}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindOperationsComposition(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Email string
|
||||
}
|
||||
|
||||
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
|
||||
result := F.Pipe4(
|
||||
Do(User{}),
|
||||
LetTo(func(n string) func(User) User {
|
||||
return func(u User) User { u.Name = n; return u }
|
||||
}, "Alice"),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validation[int] {
|
||||
// Age validation
|
||||
if len(u.Name) > 0 {
|
||||
return Success(25)
|
||||
}
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "name required"}})
|
||||
}),
|
||||
Let(func(e string) func(User) User {
|
||||
return func(u User) User { u.Email = e; return u }
|
||||
}, func(u User) string {
|
||||
// Derive email from name
|
||||
return u.Name + "@example.com"
|
||||
}),
|
||||
Bind(func(a int) func(User) User {
|
||||
return func(u User) User { u.Age = a; return u }
|
||||
}, func(u User) Validation[int] {
|
||||
// Validate age is positive
|
||||
if u.Age > 0 {
|
||||
return Success(u.Age)
|
||||
}
|
||||
return Failures[int](Errors{&ValidationError{Messsage: "age must be positive"}})
|
||||
}),
|
||||
)
|
||||
|
||||
expected := User{Name: "Alice", Age: 25, Email: "Alice@example.com"}
|
||||
assert.Equal(t, Of(expected), result)
|
||||
})
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/applicative"
|
||||
)
|
||||
|
||||
var errorsMonoid = ErrorsMonoid()
|
||||
|
||||
// Of creates a successful validation result containing the given value.
|
||||
// This is the pure/return operation for the Validation monad.
|
||||
//
|
||||
@@ -28,37 +32,376 @@ func Of[A any](a A) Validation[A] {
|
||||
// return func(age int) User { return User{name, age} }
|
||||
// }))(validateName))(validateAge)
|
||||
func Ap[B, A any](fa Validation[A]) Operator[func(A) B, B] {
|
||||
return either.ApV[B, A](ErrorsMonoid())(fa)
|
||||
return either.ApV[B, A](errorsMonoid)(fa)
|
||||
}
|
||||
|
||||
// MonadAp applies a validation containing a function to a validation containing a value.
|
||||
// This is the applicative apply operation that **accumulates errors** from both validations.
|
||||
//
|
||||
// **Key behavior**: Unlike Either's MonadAp which fails fast (returns first error),
|
||||
// this validation-specific implementation **accumulates all errors** using the Errors monoid.
|
||||
// When both the function validation and value validation fail, all errors from both are combined.
|
||||
//
|
||||
// This error accumulation is the defining characteristic of the Validation applicative,
|
||||
// making it ideal for scenarios where you want to collect all validation failures at once
|
||||
// rather than stopping at the first error.
|
||||
//
|
||||
// Behavior:
|
||||
// - Both succeed: applies the function to the value → Success(result)
|
||||
// - Function fails, value succeeds: returns function's errors → Failure(func errors)
|
||||
// - Function succeeds, value fails: returns value's errors → Failure(value errors)
|
||||
// - Both fail: **combines all errors** → Failure(func errors + value errors)
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Form validation: collect all field errors at once
|
||||
// - Configuration validation: report all invalid settings together
|
||||
// - Data validation: accumulate all constraint violations
|
||||
// - Multi-field validation: validate independent fields in parallel
|
||||
//
|
||||
// Example - Both succeed:
|
||||
//
|
||||
// double := func(x int) int { return x * 2 }
|
||||
// result := MonadAp(Of(double), Of(21))
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Error accumulation (key feature):
|
||||
//
|
||||
// funcValidation := Failures[func(int) int](Errors{
|
||||
// &ValidationError{Messsage: "function error"},
|
||||
// })
|
||||
// valueValidation := Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "value error"},
|
||||
// })
|
||||
// result := MonadAp(funcValidation, valueValidation)
|
||||
// // Result: Failure with BOTH errors: ["function error", "value error"]
|
||||
//
|
||||
// Example - Validating multiple fields:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// makeUser := func(name string) func(int) User {
|
||||
// return func(age int) User { return User{name, age} }
|
||||
// }
|
||||
//
|
||||
// nameValidation := validateName("ab") // Fails: too short
|
||||
// ageValidation := validateAge(16) // Fails: too young
|
||||
//
|
||||
// // First apply name
|
||||
// step1 := MonadAp(Of(makeUser), nameValidation)
|
||||
// // Then apply age
|
||||
// result := MonadAp(step1, ageValidation)
|
||||
// // Result contains ALL validation errors from both fields
|
||||
func MonadAp[B, A any](fab Validation[func(A) B], fa Validation[A]) Validation[B] {
|
||||
return either.MonadApV[B, A](ErrorsMonoid())(fab, fa)
|
||||
return either.MonadApV[B, A](errorsMonoid)(fab, fa)
|
||||
}
|
||||
|
||||
// Map transforms the value inside a successful validation using the provided function.
|
||||
// If the validation is a failure, the errors are preserved unchanged.
|
||||
// This is the functor map operation for Validation.
|
||||
//
|
||||
// Example:
|
||||
// Map is used for transforming successful values without changing the validation context.
|
||||
// It's the most basic operation for working with validated values and forms the foundation
|
||||
// for more complex validation pipelines.
|
||||
//
|
||||
// Behavior:
|
||||
// - Success: applies function to value → Success(f(value))
|
||||
// - Failure: preserves errors unchanged → Failure(same errors)
|
||||
//
|
||||
// This is useful for:
|
||||
// - Type transformations: converting validated values to different types
|
||||
// - Value transformations: normalizing, formatting, or computing derived values
|
||||
// - Pipeline composition: chaining multiple transformations
|
||||
// - Preserving validation context: errors pass through unchanged
|
||||
//
|
||||
// Example - Transform successful value:
|
||||
//
|
||||
// doubled := Map(func(x int) int { return x * 2 })(Of(21))
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Failure preserved:
|
||||
//
|
||||
// result := Map(func(x int) int { return x * 2 })(
|
||||
// Failures[int](Errors{&ValidationError{Messsage: "invalid"}}),
|
||||
// )
|
||||
// // Result: Failure with same error: ["invalid"]
|
||||
//
|
||||
// Example - Type transformation:
|
||||
//
|
||||
// toString := Map(func(x int) string { return fmt.Sprintf("%d", x) })
|
||||
// result := toString(Of(42))
|
||||
// // Result: Success("42")
|
||||
//
|
||||
// Example - Chaining transformations:
|
||||
//
|
||||
// result := F.Pipe3(
|
||||
// Of(5),
|
||||
// Map(func(x int) int { return x + 10 }), // 15
|
||||
// Map(func(x int) int { return x * 2 }), // 30
|
||||
// Map(func(x int) string { return fmt.Sprintf("%d", x) }), // "30"
|
||||
// )
|
||||
// // Result: Success("30")
|
||||
func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
return either.Map[Errors](f)
|
||||
}
|
||||
|
||||
// MonadMap transforms the value inside a successful validation using the provided function.
|
||||
// If the validation is a failure, the errors are preserved unchanged.
|
||||
// This is the non-curried version of [Map].
|
||||
//
|
||||
// MonadMap is useful when you have both the validation and the transformation function
|
||||
// available at the same time, rather than needing to create a reusable operator.
|
||||
//
|
||||
// Behavior:
|
||||
// - Success: applies function to value → Success(f(value))
|
||||
// - Failure: preserves errors unchanged → Failure(same errors)
|
||||
//
|
||||
// Example - Transform successful value:
|
||||
//
|
||||
// result := MonadMap(Of(21), func(x int) int { return x * 2 })
|
||||
// // Result: Success(42)
|
||||
//
|
||||
// Example - Failure preserved:
|
||||
//
|
||||
// result := MonadMap(
|
||||
// Failures[int](Errors{&ValidationError{Messsage: "invalid"}}),
|
||||
// func(x int) int { return x * 2 },
|
||||
// )
|
||||
// // Result: Failure with same error: ["invalid"]
|
||||
//
|
||||
// Example - Type transformation:
|
||||
//
|
||||
// result := MonadMap(Of(42), func(x int) string {
|
||||
// return fmt.Sprintf("Value: %d", x)
|
||||
// })
|
||||
// // Result: Success("Value: 42")
|
||||
//
|
||||
// Example - Computing derived values:
|
||||
//
|
||||
// type User struct { FirstName, LastName string }
|
||||
// result := MonadMap(
|
||||
// Of(User{"John", "Doe"}),
|
||||
// func(u User) string { return u.FirstName + " " + u.LastName },
|
||||
// )
|
||||
// // Result: Success("John Doe")
|
||||
func MonadMap[A, B any](fa Validation[A], f func(A) B) Validation[B] {
|
||||
return either.MonadMap(fa, f)
|
||||
}
|
||||
|
||||
// Chain is the curried version of [MonadChain].
|
||||
// Sequences two validation computations where the second depends on the first.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// validatePositive := func(x int) Validation[int] {
|
||||
// if x > 0 { return Success(x) }
|
||||
// return Failure("must be positive")
|
||||
// }
|
||||
// result := Chain(validatePositive)(Success(42)) // Success(42)
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return either.Chain(f)
|
||||
}
|
||||
|
||||
// MonadChain sequences two validation computations where the second depends on the first.
|
||||
// If the first validation fails, returns the failure without executing the second.
|
||||
// This is the monadic bind operation for Validation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// result := MonadChain(
|
||||
// Success(42),
|
||||
// func(x int) Validation[string] {
|
||||
// return Success(fmt.Sprintf("Value: %d", x))
|
||||
// },
|
||||
// ) // Success("Value: 42")
|
||||
func MonadChain[A, B any](fa Validation[A], f Kleisli[A, B]) Validation[B] {
|
||||
return either.MonadChain(fa, f)
|
||||
}
|
||||
|
||||
// chainErrors is an internal helper that chains error transformations while accumulating errors.
|
||||
// When the transformation function f returns a failure, it concatenates the original errors (e1)
|
||||
// with the new errors (e2) using the Errors monoid, ensuring all validation errors are preserved.
|
||||
func chainErrors[A any](f Kleisli[Errors, A]) func(Errors) Validation[A] {
|
||||
return func(e1 Errors) Validation[A] {
|
||||
return either.MonadFold(
|
||||
f(e1),
|
||||
function.Flow2(array.Concat(e1), either.Left[A]),
|
||||
Of[A],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// Returns a function that transforms validation failures while preserving successes.
|
||||
//
|
||||
// Unlike the standard Either ChainLeft which replaces errors, this validation-specific
|
||||
// implementation **aggregates errors** using the Errors monoid. When the transformation
|
||||
// function returns a failure, both the original errors and the new errors are combined,
|
||||
// ensuring no validation errors are lost.
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Error recovery with fallback validation
|
||||
// - Adding contextual information to existing errors
|
||||
// - Transforming error types while preserving all error details
|
||||
// - Building error handling pipelines that accumulate failures
|
||||
//
|
||||
// Key behavior:
|
||||
// - Success values pass through unchanged
|
||||
// - When transforming failures, if the transformation also fails, **all errors are aggregated**
|
||||
// - If the transformation succeeds, it recovers from the original failure
|
||||
//
|
||||
// Example - Error recovery with aggregation:
|
||||
//
|
||||
// recoverFromNotFound := ChainLeft(func(errs Errors) Validation[int] {
|
||||
// // Check if this is a "not found" error
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Success(0) // recover with default
|
||||
// }
|
||||
// }
|
||||
// // Add context to existing errors
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "recovery failed"},
|
||||
// })
|
||||
// // Result will contain BOTH original errors AND "recovery failed"
|
||||
// })
|
||||
//
|
||||
// result := recoverFromNotFound(Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "database error"},
|
||||
// }))
|
||||
// // Result contains: ["database error", "recovery failed"]
|
||||
//
|
||||
// Example - Adding context to errors:
|
||||
//
|
||||
// addContext := ChainLeft(func(errs Errors) Validation[string] {
|
||||
// // Add contextual information
|
||||
// return Failures[string](Errors{
|
||||
// &ValidationError{
|
||||
// Messsage: "validation failed in user.email field",
|
||||
// },
|
||||
// })
|
||||
// // Original errors are preserved and new context is added
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Failures[string](Errors{
|
||||
// &ValidationError{Messsage: "invalid format"},
|
||||
// }),
|
||||
// addContext,
|
||||
// )
|
||||
// // Result contains: ["invalid format", "validation failed in user.email field"]
|
||||
//
|
||||
// Example - Success values pass through:
|
||||
//
|
||||
// handler := ChainLeft(func(errs Errors) Validation[int] {
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "never called"},
|
||||
// })
|
||||
// })
|
||||
// result := handler(Success(42)) // Success(42) - unchanged
|
||||
func ChainLeft[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
return either.Fold(
|
||||
chainErrors(f),
|
||||
Of[A],
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft sequences a computation on the failure (Left) channel of a Validation.
|
||||
// If the Validation is a failure, applies the function to transform or recover from the errors.
|
||||
// If the Validation is a success, returns the success value unchanged.
|
||||
//
|
||||
// **Critical difference from Either.MonadChainLeft**: This validation-specific implementation
|
||||
// **aggregates errors** using the Errors monoid. When the transformation function returns a
|
||||
// failure, both the original errors and the new errors are combined, ensuring comprehensive
|
||||
// error reporting.
|
||||
//
|
||||
// This is the dual of [MonadChain] - while Chain operates on success values, ChainLeft
|
||||
// operates on failure values. It's particularly useful for:
|
||||
// - Error recovery: converting specific errors into successful values
|
||||
// - Error enrichment: adding context or transforming error messages
|
||||
// - Fallback logic: providing alternative validations when the first fails
|
||||
// - Error aggregation: combining multiple validation failures
|
||||
//
|
||||
// The function parameter receives the collection of validation errors and must return
|
||||
// a new Validation[A]. This allows you to:
|
||||
// - Recover by returning Success(value)
|
||||
// - Transform errors by returning Failures(newErrors) - **original errors are preserved**
|
||||
// - Implement conditional error handling based on error content
|
||||
//
|
||||
// Example - Error recovery:
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "not found"},
|
||||
// }),
|
||||
// func(errs Errors) Validation[int] {
|
||||
// // Check if we can recover
|
||||
// for _, err := range errs {
|
||||
// if err.Messsage == "not found" {
|
||||
// return Success(0) // recover with default value
|
||||
// }
|
||||
// }
|
||||
// return Failures[int](errs) // propagate errors
|
||||
// },
|
||||
// ) // Success(0)
|
||||
//
|
||||
// Example - Error aggregation (key feature):
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Failures[string](Errors{
|
||||
// &ValidationError{Messsage: "error 1"},
|
||||
// &ValidationError{Messsage: "error 2"},
|
||||
// }),
|
||||
// func(errs Errors) Validation[string] {
|
||||
// // Transformation also fails
|
||||
// return Failures[string](Errors{
|
||||
// &ValidationError{Messsage: "error 3"},
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
|
||||
// // This is different from Either.MonadChainLeft which would only keep "error 3"
|
||||
//
|
||||
// Example - Adding context to errors:
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Failures[int](Errors{
|
||||
// &ValidationError{Value: "abc", Messsage: "invalid number"},
|
||||
// }),
|
||||
// func(errs Errors) Validation[int] {
|
||||
// // Add contextual information
|
||||
// contextErrors := Errors{
|
||||
// &ValidationError{
|
||||
// Context: []ContextEntry{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
|
||||
// Messsage: "failed to parse user age",
|
||||
// },
|
||||
// }
|
||||
// return Failures[int](contextErrors)
|
||||
// },
|
||||
// )
|
||||
// // Result contains both original error and context:
|
||||
// // ["invalid number", "failed to parse user age"]
|
||||
//
|
||||
// Example - Success values pass through:
|
||||
//
|
||||
// result := MonadChainLeft(
|
||||
// Success(42),
|
||||
// func(errs Errors) Validation[int] {
|
||||
// return Failures[int](Errors{
|
||||
// &ValidationError{Messsage: "never called"},
|
||||
// })
|
||||
// },
|
||||
// ) // Success(42) - unchanged
|
||||
func MonadChainLeft[A any](fa Validation[A], f Kleisli[Errors, A]) Validation[A] {
|
||||
return either.MonadFold(
|
||||
fa,
|
||||
chainErrors(f),
|
||||
Of[A],
|
||||
)
|
||||
}
|
||||
|
||||
// Applicative creates an Applicative instance for Validation with error accumulation.
|
||||
//
|
||||
// This returns a lawful Applicative that accumulates validation errors using the Errors monoid.
|
||||
@@ -123,6 +466,11 @@ func MonadChain[A, B any](fa Validation[A], f Kleisli[A, B]) Validation[B] {
|
||||
// An Applicative instance with Of, Map, and Ap operations that accumulate errors
|
||||
func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Validation[B], Validation[func(A) B]] {
|
||||
return either.ApplicativeV[Errors, A, B](
|
||||
ErrorsMonoid(),
|
||||
errorsMonoid,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
|
||||
return ChainLeft(f)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,12 +74,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
t.Run("empty returns successful validation with empty string", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
|
||||
assert.True(t, either.IsRight(empty))
|
||||
value := either.MonadFold(empty,
|
||||
func(Errors) string { return "ERROR" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, Success(""), empty)
|
||||
})
|
||||
|
||||
t.Run("concat combines successful validations", func(t *testing.T) {
|
||||
@@ -88,12 +83,7 @@ func TestApplicativeMonoid(t *testing.T) {
|
||||
|
||||
result := m.Concat(v1, v2)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "Hello World", value)
|
||||
assert.Equal(t, Success("Hello World"), result)
|
||||
})
|
||||
|
||||
t.Run("concat with failure returns failure", func(t *testing.T) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user