1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-31 11:19:23 +02:00

Compare commits

..

19 Commits

Author SHA1 Message Date
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
Dr. Carsten Leue
eafc008798 fix: doc for lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:00:11 +01:00
Dr. Carsten Leue
46bf065e34 fix: migrate CLI to github.com/urfave/v3
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 12:56:23 +01:00
renovate[bot]
b4e303423b chore(deps): update actions/checkout action to v6.0.2 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 12:39:53 +01:00
renovate[bot]
7afc098f58 fix(deps): update go dependencies (major) (#144)
* fix(deps): update go dependencies

* fix: fix renovate config

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:28:56 +01:00
Dr. Carsten Leue
617e43de19 fix: improve codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:12:40 +01:00
Dr. Carsten Leue
0f7a6c0589 fix: prism doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-22 13:57:07 +01:00
179 changed files with 24557 additions and 809 deletions

View File

@@ -28,11 +28,11 @@ jobs:
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
@@ -66,11 +66,11 @@ jobs:
matrix:
go-version: ['1.24.x', '1.25.x']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
@@ -126,17 +126,17 @@ jobs:
steps:
# full checkout for semantic-release
- name: Full checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ env.LATEST_GO_VERSION }}
cache: true # Enable Go module caching

16
go.sum
View File

@@ -1,7 +1,3 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -10,20 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=

View File

@@ -22,7 +22,8 @@
"matchDepTypes": [
"golang"
],
"enabled": false
"enabled": false,
"description": "Disable updates to the go directive in go.mod files - the directive identifies the minimum compatible Go version and should stay as small as possible for maximum compatibility"
},
{
"matchUpdateTypes": [

View File

@@ -465,7 +465,7 @@ func process() IOResult[string] {
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
#### Idiomatic Packages (Tuple-based, High Performance)
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples

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

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateTraverseTuple(f *os.File, i int) {
@@ -422,10 +423,10 @@ func ApplyCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateApplyHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func createCombinations(n int, all, prev []int) [][]int {
@@ -284,10 +285,10 @@ func BindCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateBindHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func Commands() []*C.Command {

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"strings"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// Deprecated:
@@ -261,10 +262,10 @@ func ContextReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateContextReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateMakeProvider(f *os.File, i int) {
@@ -221,10 +222,10 @@ func DICommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateDIHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func eitherHKT(typeE string) func(typeA string) string {
@@ -190,10 +191,10 @@ func EitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func identityHKT(typeA string) string {
@@ -93,10 +94,10 @@ func IdentityCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIdentityHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIO(param string) string {
@@ -102,10 +103,10 @@ func IOCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// [GA ~func() ET.Either[E, A], GB ~func() ET.Either[E, B], GTAB ~func() ET.Either[E, T.Tuple2[A, B]], E, A, B any](a GA, b GB) GTAB {
@@ -273,10 +274,10 @@ func IOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIOOption(param string) string {
@@ -107,10 +108,10 @@ func IOOptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -17,6 +17,7 @@ package cli
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/token"
@@ -28,7 +29,7 @@ import (
"text/template"
S "github.com/IBM/fp-go/v2/string"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (
@@ -86,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}}
@@ -100,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}}
@@ -111,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}}
@@ -157,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}}
@@ -195,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}}
@@ -235,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 (
@@ -535,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
@@ -697,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
@@ -934,12 +996,12 @@ func LensCommand() *C.Command {
flagVerbose,
flagIncludeTestFiles,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateLensHelpers(
ctx.String(keyLensDir),
ctx.String(keyFilename),
ctx.Bool(keyVerbose),
ctx.Bool(keyIncludeTestFile),
cmd.String(keyLensDir),
cmd.String(keyFilename),
cmd.Bool(keyVerbose),
cmd.Bool(keyIncludeTestFile),
)
},
}

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

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func optionHKT(typeA string) string {
@@ -200,10 +201,10 @@ func OptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateUnsliced(f *os.File, i int) {
@@ -423,10 +424,10 @@ func PipeCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generatePipeHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderFrom(f, fg *os.File, i int) {
@@ -154,10 +155,10 @@ func ReaderCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderIOEitherFrom(f, fg *os.File, i int) {
@@ -284,10 +285,10 @@ func ReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"strings"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func writeTupleType(f *os.File, symbol string, i int) {
@@ -615,10 +616,10 @@ func TupleCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateTupleHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

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[C](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[C](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[C](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[C](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[C](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[C](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[C](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[C](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[C](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[C](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[C](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[TestContext](
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, int](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[TestContext](
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, string]("alice@example.com")
},
)(Bind[TestContext](
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, int](30)
},
)(Bind[TestContext](
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, string]("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[TestContext](
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[TestContext](
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, int](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, int](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, int](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, int](30)
eff := ApS[TestContext](
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[TestContext](
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, int](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, error](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[int](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[TestContext](
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[TestContext](
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, int](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, error](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, int](30)
eff := ApSL[TestContext](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[TestContext](
ageLens,
func(age int) Effect[TestContext, int] {
return Of[TestContext, int](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[TestContext](
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, int](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[TestContext](
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, int](25)
},
)(BindTo[TestContext](func(name string) ComplexState {
return ComplexState{Name: name}
})(Of[TestContext, string]("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, string]("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[string](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[InnerContext](func(_ string) Effect[InnerContext, string] {
return Of[InnerContext, string]("inner value")
})(Of[InnerContext, string]("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[string](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[string](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, string]("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[string](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, string]("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[string](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, int](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[int](localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapReader := RunSync[int](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, string]("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[string](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[int](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, string]("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[string](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, string]("query result")
// Transform AppConfig to DatabaseConfig effectfully
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
ConnectionString: "loaded from " + app.ConfigPath,
})
}
// Apply the transformation
transform := LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync[string](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, string]("success")
expectedErr := assert.AnError
// Context transformation that fails
failingTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Fail[OuterCtx, InnerCtx](expectedErr)
}
transform := LocalEffectK[string, InnerCtx, OuterCtx](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](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](InnerCtx{Value: outer.Path})
}
transformK := LocalEffectK[string, InnerCtx, OuterCtx](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](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[Config](func(cfg Config) Effect[Config, string] {
return Of[Config, string]("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](Config{
Data: "loaded from " + app.ConfigFile,
})
}
transform := LocalEffectK[string, Config, AppContext](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync[string](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, string]("deep result")
// Transform Level2 -> Level3 effectfully
transform23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2, Level3](Level3{C: l2.B + "-c"})
})
// Transform Level1 -> Level2 effectfully
transform12 := LocalEffectK[string, Level2, Level1](func(l1 Level1) Effect[Level1, Level2] {
return Of[Level1, Level2](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[string](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[DatabaseConfig](func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
return Of[DatabaseConfig, string](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](DatabaseConfig{
Host: prefix + app.DBHost,
Port: app.DBPort,
})
}
transform := LocalEffectK[string, DatabaseConfig, AppConfig](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
})(appEffect)
readerResult := RunSync[string](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, string]("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](ValidatedConfig{
APIKey: raw.APIKey,
})
}
transform := LocalEffectK[string, ValidatedConfig, RawConfig](validateConfig)
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync[string](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, string]("result")
// Use LocalEffectK for first transformation (effectful)
localEffectK23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2, Level3](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[string](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[InnerCtx](func(ctx InnerCtx) Effect[InnerCtx, int] {
return Of[InnerCtx, int](ctx.Value * 2)
})(readerreaderioresult.Ask[InnerCtx]())
// Complex transformation with nested effects
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx, InnerCtx](InnerCtx{
Value: outer.Multiplier * 10,
})
}
transform := LocalEffectK[int, InnerCtx, OuterCtx](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync[int](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[A](ioResult)
return readerResult(context.Background())
}
func TestSucceed(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Succeed[TestContext, int](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, string]("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](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, int](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, string](value)
eff2 := Succeed[TestContext, string](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, int](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, int](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, int](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, int](10)
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](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, int](42)
chained := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](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, int](10)
chained := Chain[TestContext](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, int](5)
result := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x + 10)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](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(int) int](func(x int) int {
return x * 2
})
value := Of[TestContext, int](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(int) string](func(x int) string {
return fmt.Sprintf("value: %d", x)
})
value := Of[TestContext, int](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, int](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(int) int](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[TestContext, int](func() Effect[TestContext, int] {
callCount++
return Of[TestContext, int](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[TestContext, int](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[TestContext, int](func() Effect[TestContext, int] {
return Of[TestContext, int](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, int](42)
tapped := Tap[TestContext](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[TestContext](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, int](42)
tapped := Tap[TestContext](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, int](10)
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
values = append(values, x+2)
return Of[TestContext, any](nil)
})(Tap[TestContext](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[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("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[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("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[TestContext, int, string](
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, string]("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[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("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, int](5)
result := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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, int](10)
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
sideEffect = x
return Of[TestContext, any](nil)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](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[int](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, string](stringMonoid)
eff1 := Of[TestContext, string]("Hello")
eff2 := Of[TestContext, string](" ")
eff3 := Of[TestContext, string]("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, int](intMonoid)
eff1 := Of[TestContext, int](10)
eff2 := Of[TestContext, int](20)
eff3 := Of[TestContext, int](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, string](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, string](stringMonoid)
eff1 := Fail[TestContext, string](expectedErr)
eff2 := Of[TestContext, string]("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, string](stringMonoid)
eff1 := Of[TestContext, string]("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, int](intMonoid)
effects := []Effect[TestContext, int]{
Of[TestContext, int](2),
Of[TestContext, int](3),
Of[TestContext, int](4),
Of[TestContext, int](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, Counter](counterMonoid)
eff1 := Of[TestContext, Counter](Counter{Count: 5})
eff2 := Of[TestContext, Counter](Counter{Count: 10})
eff3 := Of[TestContext, Counter](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, string](stringMonoid)
eff1 := Of[TestContext, string]("First")
eff2 := Of[TestContext, string]("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, string](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first failed"))
eff2 := Of[TestContext, string]("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, string](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, string](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, int](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Fail[TestContext, int](errors.New("error 2"))
eff3 := Of[TestContext, int](42)
eff4 := Of[TestContext, int](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, Result](resultMonoid)
eff1 := Fail[TestContext, Result](errors.New("failed"))
eff2 := Of[TestContext, Result](Result{Value: "success", Code: 200})
eff3 := Of[TestContext, Result](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, string](stringMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("A")
eff2 := Of[TestContext, string]("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, int](intMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, int](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Of[TestContext, int](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, string]("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, string]("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[TestContext, string](
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, int](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, int](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, string]("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, string]("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, string]("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, string]("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[TestContext, string](
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, string]("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](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("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, int](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, string]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](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, string]("connected")
ioResult := Provide[Config, string](cfg)(eff)
readerResult := RunSync[string](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[string](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, int](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
readerResult := RunSync[int](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("result")
})(Of[TestContext, int](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](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, int](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](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, int](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](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, string]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](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[int](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[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x + 10)
})(Of[TestContext, int](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](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, int](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](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](user)
ioResult := Provide[TestContext, User](ctx)(eff)
readerResult := RunSync[User](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, string]("API call successful")
// Provide config and run
result, err := RunSync[string](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[string](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[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(Of[TestContext, int](21)))
result, err := RunSync[string](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[TestContext](
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, int](s.X * 2)
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext, int](10)))
result, err := RunSync[State](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, string]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync[string](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[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(input)
result, err := RunSync[[]int](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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[TestContext](func(s string) Effect[TestContext, int] {
return Of[TestContext, int](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[TestContext](func(x int) Effect[TestContext, string] {
if x == 3 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](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[TestContext](func(id int) Effect[TestContext, User] {
return Of[TestContext, User](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[TestContext](func(strings []string) Effect[TestContext, int] {
total := 0
for _, s := range strings {
val, _ := strconv.Atoi(s)
total += val
}
return Of[TestContext, int](total)
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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[TestContext](func(x int) Effect[TestContext, string] {
return Chain[TestContext](func(ctx TestContext) Effect[TestContext, string] {
return Of[TestContext, string](fmt.Sprintf("%s-%d", ctx.Value, x))
})(Of[TestContext, 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[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](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[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
// Second traversal: string -> int (length)
kleisli2 := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
return Of[TestContext, int](len(s))
})
eff := Chain[TestContext](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](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[TestContext](func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](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[TestContext](func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](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

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

@@ -253,7 +253,7 @@ func Second[T1, T2 any](_ T1, t2 T2) T2 {
}
// Zero returns the zero value of the given type.
func Zero[A comparable]() A {
func Zero[A any]() A {
var zero A
return zero
}

View File

@@ -4,14 +4,11 @@ go 1.24
require (
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.27.7
github.com/urfave/cli/v3 v3.6.2
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,17 +1,11 @@
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

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

@@ -0,0 +1,70 @@
package readert
import (
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
// ApplySemigroup lifts a Semigroup[A] into a Semigroup[Reader[R, A]].
// This allows you to combine two Readers that produce semigroup values by combining
// their results using the semigroup's concat operation.
//
// The _map and _ap parameters are the Map and Ap operations for the Reader type,
// typically obtained from the reader package.
//
// Example:
//
// type Config struct { Multiplier int }
// // Using the additive semigroup for integers
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
// readerSemigroup := reader.ApplySemigroup(
// reader.MonadMap[Config, int, func(int) int],
// reader.MonadAp[int, Config, int],
// intSemigroup,
// )
//
// r1 := reader.Of[Config](5)
// r2 := reader.Of[Config](3)
// combined := readerSemigroup.Concat(r1, r2)
// result := combined(Config{Multiplier: 1}) // 8
func ApplySemigroup[R, A any](
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
_ap func(func(R, func(A) A), func(R) A) func(R) A,
s S.Semigroup[A],
) S.Semigroup[func(R) A] {
return S.ApplySemigroup(_map, _ap, s)
}
// ApplicativeMonoid lifts a Monoid[A] into a Monoid[Reader[R, A]].
// This allows you to combine Readers that produce monoid values, with an empty/identity Reader.
//
// The _of parameter is the Of operation (pure/return) for the Reader type.
// The _map and _ap parameters are the Map and Ap operations for the Reader type.
//
// Example:
//
// type Config struct { Prefix string }
// // Using the string concatenation monoid
// stringMonoid := monoid.MakeMonoid("", func(a, b string) string { return a + b })
// readerMonoid := reader.ApplicativeMonoid(
// reader.Of[Config, string],
// reader.MonadMap[Config, string, func(string) string],
// reader.MonadAp[string, Config, string],
// stringMonoid,
// )
//
// r1 := reader.Asks(func(c Config) string { return c.Prefix })
// r2 := reader.Of[Config]("hello")
// combined := readerMonoid.Concat(r1, r2)
// result := combined(Config{Prefix: ">> "}) // ">> hello"
// empty := readerMonoid.Empty()(Config{Prefix: "any"}) // ""
func ApplicativeMonoid[R, A any](
_of func(A) func(R) A,
_map func(func(R) A, func(A) func(A) A) func(R, func(A) A),
_ap func(func(R, func(A) A), func(R) A) func(R) A,
m M.Monoid[A],
) M.Monoid[func(R) A] {
return M.ApplicativeMonoid(_of, _map, _ap, m)
}

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

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,6 +16,7 @@
package ioref
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
)
@@ -124,15 +125,56 @@ 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
}
}
@@ -167,12 +209,61 @@ 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)
}

340
v2/ioref/ioref_test.go Normal file
View File

@@ -0,0 +1,340 @@
// 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/stretchr/testify/assert"
)
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

@@ -70,5 +70,20 @@ type (
// It's commonly used with Modify to transform the value in an IORef.
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.
//
// 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)
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

@@ -17,23 +17,28 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
app := &C.App{
app := &C.Command{
Name: "fp-go",
Usage: "Code generation for fp-go",
Commands: cli.Commands(),
}
if err := app.Run(os.Args); err != nil {
if err := app.Run(ctx, os.Args); err != nil {
log.Fatal(err)
}
}

View File

@@ -221,7 +221,7 @@ Lenses can be automatically generated using the `fp-go` CLI tool and a simple an
1. **Annotate your struct** with the `fp-go:Lens` comment:
```go
//go:generate go run github.com/IBM/fp-go/v2/main.go lens --dir . --filename gen_lens.go
//go:generate go run github.com/IBM/fp-go/v2 lens --dir . --filename gen_lens.go
// fp-go:Lens
type Person struct {
@@ -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.
@@ -293,13 +302,23 @@ More specific optics can be converted to more general ones.
## 📦 Package Structure
### Core Optics
- **[optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)**: Lenses for product types (structs)
- **[optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)**: Prisms for sum types ([`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either), [`Result`](https://pkg.go.dev/github.com/IBM/fp-go/v2/result), etc.)
- **[optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)**: Isomorphisms for equivalent types
- **[optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)**: Optional optics for maybe values
- **[optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)**: Traversals for multiple values
Each package includes specialized sub-packages for common patterns:
### Utilities
- **[optics/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/builder)**: Builder pattern for constructing complex optics
- **[optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec)**: Type-safe encoding/decoding with validation
- Provides `Type[A, O, I]` for bidirectional transformations with validation
- Includes codecs for primitives (String, Int, Bool), collections (Array), and sum types (Either)
- Supports refinement types and codec composition via `Pipe`
- Integrates validation errors with context tracking
### Specialized Sub-packages
Each core optics package includes specialized sub-packages for common patterns:
- **array**: Optics for arrays/slices
- **either**: Optics for [`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either) types
- **option**: Optics for [`Option`](https://pkg.go.dev/github.com/IBM/fp-go/v2/option) types

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