1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-06 11:37:45 +02:00

Compare commits

..

19 Commits

Author SHA1 Message Date
Dr. Carsten Leue
49deb57d24 fix: OrElse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 17:54:43 +01:00
Dr. Carsten Leue
abb55ddbd0 fix: validation logic and ChainLeft
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 14:00:44 +01:00
Dr. Carsten Leue
f6b01dffdc fix: add ModifiyReaderIOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-02 09:09:04 +01:00
Dr. Carsten Leue
43b666edbb fix: add bind to codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-31 11:47:50 +01:00
Dr. Carsten Leue
e42d765852 fix: readeriooption
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 16:59:32 +01:00
Dr. Carsten Leue
d2da8a32b4 fix: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 11:45:45 +01:00
Dr. Carsten Leue
7484af664b fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:12:27 +01:00
Dr. Carsten Leue
ae38e3f8f4 fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:07:12 +01:00
Dr. Carsten Leue
e0f854bda3 fix: executes_all_IO_operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:25:39 +01:00
Dr. Carsten Leue
34786c3cd8 fix: more tests and lens generation fix for prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:11:46 +01:00
Dr. Carsten Leue
a7aa7e3560 fix: better DI example
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 22:45:17 +01:00
Dr. Carsten Leue
ff2a4299b2 fix: add some useful lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 17:39:34 +01:00
Dr. Carsten Leue
edd66d63e6 fix: more codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 14:51:35 +01:00
Dr. Carsten Leue
909aec8eba fix: better sequence iter
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-26 10:41:25 +01:00
Obed Tetteh
da0344f9bd feat(iterator): add Last function with Option return type (#155)
- Add Last function to retrieve the final element from an iterator,
  returning Some(element) for non-empty sequences and None for empty ones.
- Includes tests covering simple types and  complex types
- Add documentation including example code
2026-01-26 09:04:51 +01:00
Dr. Carsten Leue
cd79dd56b9 fix: simplify tests a bit
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 17:56:28 +01:00
Dr. Carsten Leue
df07599a9e fix: some docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:40:45 +01:00
Dr. Carsten Leue
30ad0e4dd8 doc: add validation docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:26:53 +01:00
Dr. Carsten Leue
2374d7f1e4 fix: support unexported fields for lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:18:44 +01:00
160 changed files with 28192 additions and 1171 deletions

1
v2/.bob/mcp.json Normal file
View File

@@ -0,0 +1 @@
{"mcpServers":{}}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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))
}

View File

@@ -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](

View File

@@ -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
View 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

View 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)
}

View 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))
}

View File

@@ -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 (
@@ -536,9 +597,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
fieldTypeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := fieldTypeName
@@ -698,9 +759,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
typeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := typeName
isComparable := false

View File

@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
}
func TestParseFileWithUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Config struct {
PublicName string
privateName string
PublicValue int
privateValue *int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Config struct
config := structs[0]
assert.Equal(t, "Config", config.Name)
assert.Len(t, config.Fields, 4, "Should include both exported and unexported fields")
// Check exported field
assert.Equal(t, "PublicName", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Check unexported field
assert.Equal(t, "privateName", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.False(t, config.Fields[1].IsOptional)
// Check exported int field
assert.Equal(t, "PublicValue", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.False(t, config.Fields[2].IsOptional)
// Check unexported pointer field
assert.Equal(t, "privateValue", config.Fields[3].Name)
assert.Equal(t, "*int", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
}
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type MixedStruct struct {
PublicField string
privateField int
OptionalPrivate *string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "MixedStructLenses")
assert.Contains(t, contentStr, "MakeMixedStructLenses")
// Check that lenses are generated for all fields (exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
// Check lens constructors
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
// Check setters
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
}
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type PrivateConfig struct {
name string
value int
enabled bool
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check PrivateConfig struct
config := structs[0]
assert.Equal(t, "PrivateConfig", config.Name)
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
// Check all fields are unexported
assert.Equal(t, "name", config.Fields[0].Name)
assert.Equal(t, "value", config.Fields[1].Name)
assert.Equal(t, "enabled", config.Fields[2].Name)
}
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
type BaseConfig struct {
publicBase string
privateBase int
}
// fp-go:Lens
type ExtendedConfig struct {
BaseConfig
PublicField string
privateField bool
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "ExtendedConfigLenses")
// Check that lenses are generated for embedded unexported fields
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
// Check that lenses are generated for direct fields (both exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
}
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
// Create a temporary test file with various field visibility patterns
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type ComplexStruct struct {
// Exported fields
Name string
Age int
Email *string
// Unexported fields
password string
secretKey []byte
internalID *int
// Mixed with tags
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check ComplexStruct
complex := structs[0]
assert.Equal(t, "ComplexStruct", complex.Name)
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
// Verify field names and types
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
for i, expectedName := range fieldNames {
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
}
// Check optional fields
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
}

View File

@@ -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"))

View File

@@ -0,0 +1,7 @@
package builder
import "github.com/IBM/fp-go/v2/function"
type (
Void = function.Void
)

View File

@@ -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]),

View File

@@ -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),
)
)

View File

@@ -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],

View File

@@ -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],
)),
)
}

View File

@@ -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
)

View 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)
}

View 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))
})
}

View File

@@ -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)
}

View 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)
}

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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]
)

View File

@@ -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]),
)
}

View File

@@ -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)
})
}

View File

@@ -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 {

View File

@@ -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)
})

View File

@@ -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"))

View File

@@ -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
)

View File

@@ -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]),

View File

@@ -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]

View File

@@ -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],
)
}

View File

@@ -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
)

View File

@@ -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,

View File

@@ -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]())
}

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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
})

View File

@@ -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
}
}

View File

@@ -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.
//

View File

@@ -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)
})
}

View File

@@ -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)
})

View File

@@ -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"))

View File

@@ -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
)

View File

@@ -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.
//

View File

@@ -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)
})
}

View File

@@ -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
)

View File

@@ -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
View 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)())
})
}

View File

@@ -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]
)

View File

@@ -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)
})

View File

@@ -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"))

View File

@@ -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
)

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
View 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)
}

View 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
}

View File

@@ -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)
}

View File

@@ -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
)

View File

@@ -230,8 +230,16 @@ type Person struct {
Email string
Phone *string // Optional field
}
// fp-go:Lens
type Config struct {
PublicField string
privateField int // Unexported fields are supported!
}
```
**Note:** The generator supports both exported (uppercase) and unexported (lowercase) fields. Generated lenses for unexported fields will have lowercase names and can only be used within the same package as the struct.
2. **Run `go generate`**:
```bash
@@ -268,6 +276,7 @@ The generator supports:
- ✅ Embedded structs (fields are promoted)
- ✅ Optional fields (pointers and `omitempty` tags)
- ✅ Custom package imports
-**Unexported fields** (lowercase names) - lenses will have lowercase names matching the field names
See [samples/lens](../samples/lens) for complete examples.

View File

@@ -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
View 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,
)
}

View 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())
})
}

View 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.

View 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)
}

View 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)
})
}

View File

@@ -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.
//

View File

@@ -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)
})
}
})
}

View File

@@ -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
View 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),
)
}

View 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)
})
}

View 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)
}

View 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)
})
}

View File

@@ -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]
)

View File

@@ -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

View File

@@ -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)
}
})
}

View 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.

View 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)
}

View 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)
})
}

View File

@@ -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)
}

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