1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-24 12:57:26 +02:00

Compare commits

..

14 Commits

Author SHA1 Message Date
Dr. Carsten Leue
47727fd514 fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:51:34 +01:00
Dr. Carsten Leue
ece7d088ea fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:50:30 +01:00
Dr. Carsten Leue
13d25eca32 fix: add composition logic to Iso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:46:41 +01:00
Dr. Carsten Leue
a68e32308d fix: add filterable to Either and Result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 09:28:42 +01:00
Dr. Carsten Leue
61b948425b fix: cleaner use of Kleisli
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-11 16:24:11 +01:00
Dr. Carsten Leue
a276f3acff fix: add llms.txt
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 09:48:19 +01:00
Dr. Carsten Leue
8c656a4297 fix: more Alt tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 08:52:39 +01:00
Dr. Carsten Leue
bd9a642e93 fix: implement Alt for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 18:31:00 +01:00
Dr. Carsten Leue
3b55cae265 fix: implement alternative monoid for codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 09:59:17 +01:00
Dr. Carsten Leue
1472fa5a50 fix: add some more validation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-04 17:58:08 +01:00
Dr. Carsten Leue
49deb57d24 fix: OrElse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 17:54:43 +01:00
Dr. Carsten Leue
abb55ddbd0 fix: validation logic and ChainLeft
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 14:00:44 +01:00
Dr. Carsten Leue
f6b01dffdc fix: add ModifiyReaderIOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-02 09:09:04 +01:00
Dr. Carsten Leue
43b666edbb fix: add bind to codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-31 11:47:50 +01:00
83 changed files with 21806 additions and 1193 deletions

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
@@ -64,7 +64,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24.x', '1.25.x']
go-version: ['1.24.x', '1.25.x', '1.26.x']
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:

View File

@@ -21,7 +21,7 @@ import (
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// From constructs an array from a set of variadic arguments
@@ -163,11 +163,11 @@ func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return G.FilterMapWithIndex[[]A, []B](f)
}
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
//
//go:inline
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.FilterChain[[]A](f)
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.ChainOptionK[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
@@ -453,7 +453,7 @@ func Size[A any](as []A) int {
// the second contains elements for which it returns true.
//
//go:inline
func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
return G.MonadPartition(as, pred)
}
@@ -461,7 +461,7 @@ func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
// for which the predicate returns false, the right one those for which the predicate returns true
//
//go:inline
func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] {
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
return G.Partition[[]A](pred)
}

View File

@@ -24,8 +24,8 @@ import (
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
return n > 2
}
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
}
func TestFilterChain(t *testing.T) {
func TestChainOptionK(t *testing.T) {
src := From(1, 2, 3)
f := func(i int) O.Option[[]string] {
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
return O.None[[]string]()
}
res := FilterChain(f)(src)
res := ChainOptionK(f)(src)
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
}

View File

@@ -21,7 +21,7 @@ import (
FC "github.com/IBM/fp-go/v2/internal/functor"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// Of constructs a single element array
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
}
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
return F.Flow2(
FilterMap[GA, []GB](f),
Flatten[[]GB],
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
}
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
left := Empty[GA]()
right := Empty[GA]()
array.Reduce(as, func(c bool, a A) bool {
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
return c
}, true)
// returns the partition
return tuple.MakeTuple2(left, right)
return pair.MakePair(left, right)
}
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
return F.Bind2nd(MonadPartition[GA, A], pred)
}

View File

@@ -18,7 +18,7 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
// longer array are discarded
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
}
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2
as[i] = pair.Head(t)
bs[i] = pair.Tail(t)
}
return T.MakeTuple2(as, bs)
return pair.MakePair(as, bs)
}

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays,
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
// // Result: [(a, 1), (b, 2)]
//
//go:inline
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
}
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
// ages := result.Tail // [30, 25, 35]
//
//go:inline
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
return G.Unzip[[]A, []B](cs)
}

View File

@@ -19,7 +19,7 @@ import (
"fmt"
"testing"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
res := Zip[string](left)(right)
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
}
func TestUnzip(t *testing.T) {
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
unzipped := Unzip(zipped)
assert.Equal(t, right, unzipped.F1)
assert.Equal(t, left, unzipped.F2)
assert.Equal(t, right, pair.Head(unzipped))
assert.Equal(t, left, pair.Tail(unzipped))
}

View File

@@ -87,8 +87,8 @@ var (
// assembleProviders constructs the provider map for item and non-item providers
assembleProviders = F.Flow3(
A.Partition(isItemProvider),
T.Map2(collectProviders, collectItemProviders),
T.Tupled2(mergeProviders.Concat),
pair.BiMap(collectProviders, collectItemProviders),
pair.Paired(mergeProviders.Concat),
)
)

View File

@@ -68,7 +68,7 @@ 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)
return readerreaderioresult.ApS(setter, fa)
}
//go:inline
@@ -76,7 +76,7 @@ func ApSL[C, S, T any](
lens Lens[S, T],
fa Effect[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApSL[C](lens, fa)
return readerreaderioresult.ApSL(lens, fa)
}
//go:inline
@@ -84,7 +84,7 @@ 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)
return readerreaderioresult.BindL(lens, f)
}
//go:inline
@@ -132,7 +132,7 @@ 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)
return readerreaderioresult.BindReaderK(setter, f)
}
//go:inline
@@ -140,7 +140,7 @@ 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)
return readerreaderioresult.BindReaderIOK(setter, f)
}
//go:inline
@@ -172,7 +172,7 @@ 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)
return readerreaderioresult.BindReaderKL(lens, f)
}
//go:inline
@@ -180,7 +180,7 @@ 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)
return readerreaderioresult.BindReaderIOKL(lens, f)
}
//go:inline
@@ -204,7 +204,7 @@ 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)
return readerreaderioresult.ApReaderS(setter, fa)
}
//go:inline
@@ -212,7 +212,7 @@ 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)
return readerreaderioresult.ApReaderIOS(setter, fa)
}
//go:inline
@@ -244,7 +244,7 @@ func ApReaderSL[C, S, T any](
lens Lens[S, T],
fa Reader[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderSL[C](lens, fa)
return readerreaderioresult.ApReaderSL(lens, fa)
}
//go:inline
@@ -252,7 +252,7 @@ func ApReaderIOSL[C, S, T any](
lens Lens[S, T],
fa ReaderIO[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderIOSL[C](lens, fa)
return readerreaderioresult.ApReaderIOSL(lens, fa)
}
//go:inline

View File

@@ -61,7 +61,7 @@ func TestBind(t *testing.T) {
t.Run("binds effect result to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := Bind[TestContext](
eff := Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -69,7 +69,7 @@ func TestBind(t *testing.T) {
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](30)
return Of[TestContext](30)
},
)(Do[TestContext](initial))
@@ -83,7 +83,7 @@ func TestBind(t *testing.T) {
t.Run("chains multiple binds", func(t *testing.T) {
initial := BindState{}
eff := Bind[TestContext](
eff := Bind(
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
@@ -91,9 +91,9 @@ func TestBind(t *testing.T) {
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext, string]("alice@example.com")
return Of[TestContext]("alice@example.com")
},
)(Bind[TestContext](
)(Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -101,9 +101,9 @@ func TestBind(t *testing.T) {
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](30)
return Of[TestContext](30)
},
)(Bind[TestContext](
)(Bind(
func(name string) func(BindState) BindState {
return func(s BindState) BindState {
s.Name = name
@@ -111,7 +111,7 @@ func TestBind(t *testing.T) {
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext, string]("Alice")
return Of[TestContext]("Alice")
},
)(Do[TestContext](initial))))
@@ -127,7 +127,7 @@ func TestBind(t *testing.T) {
expectedErr := errors.New("bind error")
initial := BindState{Name: "Alice"}
eff := Bind[TestContext](
eff := Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -182,7 +182,7 @@ func TestLet(t *testing.T) {
func(s BindState) string {
return s.Name + "@example.com"
},
)(Bind[TestContext](
)(Bind(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -190,7 +190,7 @@ func TestLet(t *testing.T) {
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](25)
return Of[TestContext](25)
},
)(Do[TestContext](initial)))
@@ -270,7 +270,7 @@ func TestBindTo(t *testing.T) {
eff := BindTo[TestContext](func(v int) SimpleState {
return SimpleState{Value: v}
})(Of[TestContext, int](42))
})(Of[TestContext](42))
result, err := runEffect(eff, TestContext{Value: "test"})
@@ -296,7 +296,7 @@ func TestBindTo(t *testing.T) {
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext, int](10)))
})(Of[TestContext](10)))
result, err := runEffect(eff, TestContext{Value: "test"})
@@ -309,9 +309,9 @@ func TestBindTo(t *testing.T) {
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)
ageEffect := Of[TestContext](30)
eff := ApS[TestContext](
eff := ApS(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -333,7 +333,7 @@ func TestApS(t *testing.T) {
initial := BindState{Name: "Alice"}
ageEffect := Fail[TestContext, int](expectedErr)
eff := ApS[TestContext](
eff := ApS(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -388,7 +388,7 @@ func TestBindIOEitherK(t *testing.T) {
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Of[error, int](30)
return ioeither.Of[error](30)
},
)(Do[TestContext](initial))
@@ -411,7 +411,7 @@ func TestBindIOEitherK(t *testing.T) {
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Left[int, error](expectedErr)
return ioeither.Left[int](expectedErr)
},
)(Do[TestContext](initial))
@@ -434,7 +434,7 @@ func TestBindIOResultK(t *testing.T) {
}
},
func(s BindState) ioresult.IOResult[int] {
return ioresult.Of[int](30)
return ioresult.Of(30)
},
)(Do[TestContext](initial))
@@ -450,7 +450,7 @@ func TestBindReaderK(t *testing.T) {
t.Run("binds Reader operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderK[TestContext](
eff := BindReaderK(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -476,7 +476,7 @@ func TestBindReaderIOK(t *testing.T) {
t.Run("binds ReaderIO operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderIOK[TestContext](
eff := BindReaderIOK(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -512,7 +512,7 @@ func TestBindEitherK(t *testing.T) {
}
},
func(s BindState) either.Either[error, int] {
return either.Of[error, int](30)
return either.Of[error](30)
},
)(Do[TestContext](initial))
@@ -535,7 +535,7 @@ func TestBindEitherK(t *testing.T) {
}
},
func(s BindState) either.Either[error, int] {
return either.Left[int, error](expectedErr)
return either.Left[int](expectedErr)
},
)(Do[TestContext](initial))
@@ -566,9 +566,9 @@ func TestLensOperations(t *testing.T) {
t.Run("ApSL applies effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
ageEffect := Of[TestContext, int](30)
ageEffect := Of[TestContext](30)
eff := ApSL[TestContext](ageLens, ageEffect)(Do[TestContext](initial))
eff := ApSL(ageLens, ageEffect)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
@@ -580,10 +580,10 @@ func TestLensOperations(t *testing.T) {
t.Run("BindL binds effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := BindL[TestContext](
eff := BindL(
ageLens,
func(age int) Effect[TestContext, int] {
return Of[TestContext, int](age + 5)
return Of[TestContext](age + 5)
},
)(Do[TestContext](initial))
@@ -667,7 +667,7 @@ func TestApOperations(t *testing.T) {
initial := BindState{Name: "Alice"}
readerEffect := func(ctx TestContext) int { return 30 }
eff := ApReaderS[TestContext](
eff := ApReaderS(
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
@@ -685,7 +685,7 @@ func TestApOperations(t *testing.T) {
t.Run("ApEitherS applies Either effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eitherEffect := either.Of[error, int](30)
eitherEffect := either.Of[error](30)
eff := ApEitherS[TestContext](
func(age int) func(BindState) BindState {
@@ -742,7 +742,7 @@ func TestComplexBindChain(t *testing.T) {
func(s ComplexState) string {
return s.Name + "@example.com"
},
)(Bind[TestContext](
)(Bind(
func(age int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Age = age
@@ -750,11 +750,11 @@ func TestComplexBindChain(t *testing.T) {
}
},
func(s ComplexState) Effect[TestContext, int] {
return Of[TestContext, int](25)
return Of[TestContext](25)
},
)(BindTo[TestContext](func(name string) ComplexState {
return ComplexState{Name: name}
})(Of[TestContext, string]("Alice"))))))
})(Of[TestContext]("Alice"))))))
result, err := runEffect(eff, TestContext{Value: "test"})

View File

@@ -36,7 +36,7 @@ type InnerContext struct {
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")
innerEffect := Of[InnerContext]("result")
// Transform OuterContext to InnerContext
accessor := func(outer OuterContext) InnerContext {
@@ -52,7 +52,7 @@ func TestLocal(t *testing.T) {
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -61,9 +61,9 @@ func TestLocal(t *testing.T) {
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"))
innerEffect := Chain(func(_ string) Effect[InnerContext, string] {
return Of[InnerContext]("inner value")
})(Of[InnerContext]("start"))
// Transform context
accessor := func(outer OuterContext) InnerContext {
@@ -78,7 +78,7 @@ func TestLocal(t *testing.T) {
Value: "original",
Number: 100,
})(outerEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -100,7 +100,7 @@ func TestLocal(t *testing.T) {
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
@@ -119,7 +119,7 @@ func TestLocal(t *testing.T) {
}
// Effect at deepest level
level3Effect := Of[Level3, string]("deep result")
level3Effect := Of[Level3]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
@@ -137,7 +137,7 @@ func TestLocal(t *testing.T) {
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -158,7 +158,7 @@ func TestLocal(t *testing.T) {
}
// Effect that needs only DatabaseConfig
dbEffect := Of[DatabaseConfig, string]("connected")
dbEffect := Of[DatabaseConfig]("connected")
// Extract DB config from AppConfig
accessor := func(app AppConfig) DatabaseConfig {
@@ -178,7 +178,7 @@ func TestLocal(t *testing.T) {
APIKey: "secret",
Timeout: 30,
})(appEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -188,7 +188,7 @@ func TestLocal(t *testing.T) {
func TestContramap(t *testing.T) {
t.Run("is equivalent to Local", func(t *testing.T) {
innerEffect := Of[InnerContext, int](42)
innerEffect := Of[InnerContext](42)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
@@ -206,11 +206,11 @@ func TestContramap(t *testing.T) {
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localReader := RunSync[int](localIO)
localReader := RunSync(localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapReader := RunSync[int](contramapIO)
contramapReader := RunSync(contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
assert.NoError(t, localErr)
@@ -219,7 +219,7 @@ func TestContramap(t *testing.T) {
})
t.Run("transforms context correctly", func(t *testing.T) {
innerEffect := Of[InnerContext, string]("success")
innerEffect := Of[InnerContext]("success")
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value + " modified"}
@@ -232,7 +232,7 @@ func TestContramap(t *testing.T) {
Value: "original",
Number: 50,
})(outerEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -254,7 +254,7 @@ func TestContramap(t *testing.T) {
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[int](ioResult)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
@@ -275,7 +275,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
}
// Effect at deepest level
effect3 := Of[Config3, string]("result")
effect3 := Of[Config3]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
@@ -293,7 +293,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -312,24 +312,24 @@ func TestLocalEffectK(t *testing.T) {
}
// Effect that needs DatabaseConfig
dbEffect := Of[DatabaseConfig, string]("query result")
dbEffect := Of[DatabaseConfig]("query result")
// Transform AppConfig to DatabaseConfig effectfully
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
return Of[AppConfig](DatabaseConfig{
ConnectionString: "loaded from " + app.ConfigPath,
})
}
// Apply the transformation
transform := LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
transform := LocalEffectK[string](loadConfig)
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -345,7 +345,7 @@ func TestLocalEffectK(t *testing.T) {
Path string
}
innerEffect := Of[InnerCtx, string]("success")
innerEffect := Of[InnerCtx]("success")
expectedErr := assert.AnError
// Context transformation that fails
@@ -353,11 +353,11 @@ func TestLocalEffectK(t *testing.T) {
return Fail[OuterCtx, InnerCtx](expectedErr)
}
transform := LocalEffectK[string, InnerCtx, OuterCtx](failingTransform)
transform := LocalEffectK[string](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
@@ -378,14 +378,14 @@ func TestLocalEffectK(t *testing.T) {
// Successful context transformation
transform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx, InnerCtx](InnerCtx{Value: outer.Path})
return Of[OuterCtx](InnerCtx{Value: outer.Path})
}
transformK := LocalEffectK[string, InnerCtx, OuterCtx](transform)
transformK := LocalEffectK[string](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
@@ -402,25 +402,25 @@ func TestLocalEffectK(t *testing.T) {
}
// Effect that uses Config
configEffect := Chain[Config](func(cfg Config) Effect[Config, string] {
return Of[Config, string]("processed: " + cfg.Data)
configEffect := Chain(func(cfg Config) Effect[Config, string] {
return Of[Config]("processed: " + cfg.Data)
})(readerreaderioresult.Ask[Config]())
// Effectful transformation that simulates loading config
loadConfigEffect := func(app AppContext) Effect[AppContext, Config] {
// Simulate IO operation (e.g., reading file)
return Of[AppContext, Config](Config{
return Of[AppContext](Config{
Data: "loaded from " + app.ConfigFile,
})
}
transform := LocalEffectK[string, Config, AppContext](loadConfigEffect)
transform := LocalEffectK[string](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -439,16 +439,16 @@ func TestLocalEffectK(t *testing.T) {
}
// Effect at deepest level
level3Effect := Of[Level3, string]("deep result")
level3Effect := Of[Level3]("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"})
transform23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2](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"})
transform12 := LocalEffectK[string](func(l1 Level1) Effect[Level1, Level2] {
return Of[Level1](Level2{B: l1.A + "-b"})
})
// Compose transformations
@@ -457,7 +457,7 @@ func TestLocalEffectK(t *testing.T) {
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -477,8 +477,8 @@ func TestLocalEffectK(t *testing.T) {
}
// 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))
dbEffect := Chain(func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
return Of[DatabaseConfig](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DatabaseConfig]())
// Transform using outer context
@@ -488,13 +488,13 @@ func TestLocalEffectK(t *testing.T) {
if app.Environment == "prod" {
prefix = "prod-"
}
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
return Of[AppConfig](DatabaseConfig{
Host: prefix + app.DBHost,
Port: app.DBPort,
})
}
transform := LocalEffectK[string, DatabaseConfig, AppConfig](transformWithContext)
transform := LocalEffectK[string](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
@@ -502,7 +502,7 @@ func TestLocalEffectK(t *testing.T) {
DBHost: "localhost",
DBPort: 5432,
})(appEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -518,31 +518,31 @@ func TestLocalEffectK(t *testing.T) {
APIKey string
}
innerEffect := Of[ValidatedConfig, string]("success")
innerEffect := Of[ValidatedConfig]("success")
// Validation that can fail
validateConfig := func(raw RawConfig) Effect[RawConfig, ValidatedConfig] {
if raw.APIKey == "" {
return Fail[RawConfig, ValidatedConfig](assert.AnError)
}
return Of[RawConfig, ValidatedConfig](ValidatedConfig{
return Of[RawConfig](ValidatedConfig{
APIKey: raw.APIKey,
})
}
transform := LocalEffectK[string, ValidatedConfig, RawConfig](validateConfig)
transform := LocalEffectK[string](validateConfig)
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync[string](ioResult2)
readerResult2 := RunSync(ioResult2)
result, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
@@ -561,11 +561,11 @@ func TestLocalEffectK(t *testing.T) {
}
// Effect at deepest level
effect3 := Of[Level3, string]("result")
effect3 := Of[Level3]("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})
localEffectK23 := LocalEffectK[string](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2](Level3{Info: l2.Data})
})
// Use Local for second transformation (pure)
@@ -579,7 +579,7 @@ func TestLocalEffectK(t *testing.T) {
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -596,22 +596,22 @@ func TestLocalEffectK(t *testing.T) {
}
// Effect that uses InnerCtx
innerEffect := Chain[InnerCtx](func(ctx InnerCtx) Effect[InnerCtx, int] {
return Of[InnerCtx, int](ctx.Value * 2)
innerEffect := Chain(func(ctx InnerCtx) Effect[InnerCtx, int] {
return Of[InnerCtx](ctx.Value * 2)
})(readerreaderioresult.Ask[InnerCtx]())
// Complex transformation with nested effects
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx, InnerCtx](InnerCtx{
return Of[OuterCtx](InnerCtx{
Value: outer.Multiplier * 10,
})
}
transform := LocalEffectK[int, InnerCtx, OuterCtx](complexTransform)
transform := LocalEffectK[int](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync[int](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)

View File

@@ -31,13 +31,13 @@ type TestContext struct {
func runEffect[A any](eff Effect[TestContext, A], ctx TestContext) (A, error) {
ioResult := Provide[TestContext, A](ctx)(eff)
readerResult := RunSync[A](ioResult)
readerResult := RunSync(ioResult)
return readerResult(context.Background())
}
func TestSucceed(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Succeed[TestContext, int](42)
eff := Succeed[TestContext](42)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
@@ -45,7 +45,7 @@ func TestSucceed(t *testing.T) {
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestContext, string]("hello")
eff := Succeed[TestContext]("hello")
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
@@ -58,7 +58,7 @@ func TestSucceed(t *testing.T) {
Age int
}
user := User{Name: "Alice", Age: 30}
eff := Succeed[TestContext, User](user)
eff := Succeed[TestContext](user)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
@@ -88,7 +88,7 @@ func TestFail(t *testing.T) {
func TestOf(t *testing.T) {
t.Run("lifts value into effect", func(t *testing.T) {
eff := Of[TestContext, int](100)
eff := Of[TestContext](100)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
@@ -97,8 +97,8 @@ func TestOf(t *testing.T) {
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test value"
eff1 := Of[TestContext, string](value)
eff2 := Succeed[TestContext, string](value)
eff1 := Of[TestContext](value)
eff2 := Succeed[TestContext](value)
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
result2, err2 := runEffect(eff2, TestContext{Value: "test"})
@@ -111,7 +111,7 @@ func TestOf(t *testing.T) {
func TestMap(t *testing.T) {
t.Run("maps over successful effect", func(t *testing.T) {
eff := Of[TestContext, int](10)
eff := Of[TestContext](10)
mapped := Map[TestContext](func(x int) int {
return x * 2
})(eff)
@@ -123,7 +123,7 @@ func TestMap(t *testing.T) {
})
t.Run("maps to different type", func(t *testing.T) {
eff := Of[TestContext, int](42)
eff := Of[TestContext](42)
mapped := Map[TestContext](func(x int) string {
return fmt.Sprintf("value: %d", x)
})(eff)
@@ -148,7 +148,7 @@ func TestMap(t *testing.T) {
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := Of[TestContext, int](5)
eff := Of[TestContext](5)
result := Map[TestContext](func(x int) int {
return x + 1
})(Map[TestContext](func(x int) int {
@@ -164,9 +164,9 @@ func TestMap(t *testing.T) {
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 := Of[TestContext](10)
chained := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
@@ -176,9 +176,9 @@ func TestChain(t *testing.T) {
})
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 := Of[TestContext](42)
chained := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext](fmt.Sprintf("number: %d", x))
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
@@ -190,8 +190,8 @@ func TestChain(t *testing.T) {
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)
chained := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
@@ -202,8 +202,8 @@ func TestChain(t *testing.T) {
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] {
eff := Of[TestContext](10)
chained := Chain(func(x int) Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})(eff)
@@ -214,11 +214,11 @@ func TestChain(t *testing.T) {
})
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 := Of[TestContext](5)
result := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x + 10)
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
@@ -230,10 +230,10 @@ func TestChain(t *testing.T) {
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 {
fn := Of[TestContext](func(x int) int {
return x * 2
})
value := Of[TestContext, int](21)
value := Of[TestContext](21)
result := Ap[int](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
@@ -243,10 +243,10 @@ func TestAp(t *testing.T) {
})
t.Run("applies function to different type", func(t *testing.T) {
fn := Of[TestContext, func(int) string](func(x int) string {
fn := Of[TestContext](func(x int) string {
return fmt.Sprintf("value: %d", x)
})
value := Of[TestContext, int](42)
value := Of[TestContext](42)
result := Ap[string](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
@@ -258,7 +258,7 @@ func TestAp(t *testing.T) {
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)
value := Of[TestContext](42)
result := Ap[int](value)(fn)
_, err := runEffect(result, TestContext{Value: "test"})
@@ -269,7 +269,7 @@ func TestAp(t *testing.T) {
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 {
fn := Of[TestContext](func(x int) int {
return x * 2
})
value := Fail[TestContext, int](expectedErr)
@@ -285,9 +285,9 @@ func TestAp(t *testing.T) {
func TestSuspend(t *testing.T) {
t.Run("suspends effect computation", func(t *testing.T) {
callCount := 0
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
eff := Suspend(func() Effect[TestContext, int] {
callCount++
return Of[TestContext, int](42)
return Of[TestContext](42)
})
// Effect not executed yet
@@ -302,7 +302,7 @@ func TestSuspend(t *testing.T) {
t.Run("suspends failing effect", func(t *testing.T) {
expectedErr := errors.New("suspended error")
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
eff := Suspend(func() Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})
@@ -314,8 +314,8 @@ func TestSuspend(t *testing.T) {
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)
eff := Suspend(func() Effect[TestContext, int] {
return Of[TestContext](value)
})
value = 10
@@ -334,8 +334,8 @@ func TestSuspend(t *testing.T) {
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] {
eff := Of[TestContext](42)
tapped := Tap(func(x int) Effect[TestContext, any] {
sideEffectValue = x * 2
return Of[TestContext, any](nil)
})(eff)
@@ -350,7 +350,7 @@ func TestTap(t *testing.T) {
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] {
tapped := Tap(func(x int) Effect[TestContext, any] {
return Of[TestContext, any](nil)
})(eff)
@@ -362,8 +362,8 @@ func TestTap(t *testing.T) {
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] {
eff := Of[TestContext](42)
tapped := Tap(func(x int) Effect[TestContext, any] {
return Fail[TestContext, any](expectedErr)
})(eff)
@@ -375,11 +375,11 @@ func TestTap(t *testing.T) {
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] {
eff := Of[TestContext](10)
result := Tap(func(x int) Effect[TestContext, any] {
values = append(values, x+2)
return Of[TestContext, any](nil)
})(Tap[TestContext](func(x int) Effect[TestContext, any] {
})(Tap(func(x int) Effect[TestContext, any] {
values = append(values, x+1)
return Of[TestContext, any](nil)
})(eff))
@@ -394,13 +394,13 @@ func TestTap(t *testing.T) {
func TestTernary(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary[TestContext, int, string](
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
return Of[TestContext]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
return Of[TestContext]("less or equal")
},
)
@@ -411,13 +411,13 @@ func TestTernary(t *testing.T) {
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary[TestContext, int, string](
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
return Of[TestContext]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
return Of[TestContext]("less or equal")
},
)
@@ -429,13 +429,13 @@ func TestTernary(t *testing.T) {
t.Run("handles errors in onTrue branch", func(t *testing.T) {
expectedErr := errors.New("true branch error")
kleisli := Ternary[TestContext, int, string](
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
return Of[TestContext]("less or equal")
},
)
@@ -447,10 +447,10 @@ func TestTernary(t *testing.T) {
t.Run("handles errors in onFalse branch", func(t *testing.T) {
expectedErr := errors.New("false branch error")
kleisli := Ternary[TestContext, int, string](
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
return Of[TestContext]("greater")
},
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
@@ -466,9 +466,9 @@ func TestTernary(t *testing.T) {
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))
eff := Of[TestContext](5)
result := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext](fmt.Sprintf("result: %d", x))
})(Map[TestContext](func(x int) int {
return x * 2
})(eff))
@@ -481,12 +481,12 @@ func TestEffectComposition(t *testing.T) {
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] {
eff := Of[TestContext](10)
result := Tap(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)
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
@@ -499,7 +499,7 @@ func TestEffectComposition(t *testing.T) {
func TestEffectWithResult(t *testing.T) {
t.Run("converts result to effect", func(t *testing.T) {
res := result.Of[int](42)
res := result.Of(42)
// This demonstrates integration with result package
assert.True(t, result.IsRight(res))
})

View File

@@ -30,11 +30,11 @@ func TestApplicativeMonoid(t *testing.T) {
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext, string]("Hello")
eff2 := Of[TestContext, string](" ")
eff3 := Of[TestContext, string]("World")
eff1 := Of[TestContext]("Hello")
eff2 := Of[TestContext](" ")
eff3 := Of[TestContext]("World")
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
@@ -49,11 +49,11 @@ func TestApplicativeMonoid(t *testing.T) {
0,
)
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
eff1 := Of[TestContext, int](10)
eff2 := Of[TestContext, int](20)
eff3 := Of[TestContext, int](30)
eff1 := Of[TestContext](10)
eff2 := Of[TestContext](20)
eff3 := Of[TestContext](30)
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
@@ -68,7 +68,7 @@ func TestApplicativeMonoid(t *testing.T) {
"empty",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
@@ -83,10 +83,10 @@ func TestApplicativeMonoid(t *testing.T) {
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
eff1 := Fail[TestContext, string](expectedErr)
eff2 := Of[TestContext, string]("World")
eff2 := Of[TestContext]("World")
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
@@ -102,9 +102,9 @@ func TestApplicativeMonoid(t *testing.T) {
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
effectMonoid := ApplicativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext, string]("Hello")
eff1 := Of[TestContext]("Hello")
eff2 := Fail[TestContext, string](expectedErr)
combined := effectMonoid.Concat(eff1, eff2)
@@ -120,13 +120,13 @@ func TestApplicativeMonoid(t *testing.T) {
1,
)
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
effectMonoid := ApplicativeMonoid[TestContext](intMonoid)
effects := []Effect[TestContext, int]{
Of[TestContext, int](2),
Of[TestContext, int](3),
Of[TestContext, int](4),
Of[TestContext, int](5),
Of[TestContext](2),
Of[TestContext](3),
Of[TestContext](4),
Of[TestContext](5),
}
combined := effectMonoid.Empty()
@@ -152,11 +152,11 @@ func TestApplicativeMonoid(t *testing.T) {
Counter{Count: 0},
)
effectMonoid := ApplicativeMonoid[TestContext, Counter](counterMonoid)
effectMonoid := ApplicativeMonoid[TestContext](counterMonoid)
eff1 := Of[TestContext, Counter](Counter{Count: 5})
eff2 := Of[TestContext, Counter](Counter{Count: 10})
eff3 := Of[TestContext, Counter](Counter{Count: 15})
eff1 := Of[TestContext](Counter{Count: 5})
eff2 := Of[TestContext](Counter{Count: 10})
eff3 := Of[TestContext](Counter{Count: 15})
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
@@ -173,10 +173,10 @@ func TestAlternativeMonoid(t *testing.T) {
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext, string]("First")
eff2 := Of[TestContext, string]("Second")
eff1 := Of[TestContext]("First")
eff2 := Of[TestContext]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
@@ -191,10 +191,10 @@ func TestAlternativeMonoid(t *testing.T) {
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first failed"))
eff2 := Of[TestContext, string]("Second")
eff2 := Of[TestContext]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
@@ -210,7 +210,7 @@ func TestAlternativeMonoid(t *testing.T) {
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first error"))
eff2 := Fail[TestContext, string](expectedErr)
@@ -228,7 +228,7 @@ func TestAlternativeMonoid(t *testing.T) {
"default",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
effectMonoid := AlternativeMonoid[TestContext](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
@@ -242,12 +242,12 @@ func TestAlternativeMonoid(t *testing.T) {
0,
)
effectMonoid := AlternativeMonoid[TestContext, int](intMonoid)
effectMonoid := AlternativeMonoid[TestContext](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)
eff3 := Of[TestContext](42)
eff4 := Of[TestContext](100)
combined := effectMonoid.Concat(
effectMonoid.Concat(eff1, eff2),
@@ -273,11 +273,11 @@ func TestAlternativeMonoid(t *testing.T) {
Result{Value: "", Code: 0},
)
effectMonoid := AlternativeMonoid[TestContext, Result](resultMonoid)
effectMonoid := AlternativeMonoid[TestContext](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})
eff2 := Of[TestContext](Result{Value: "success", Code: 200})
eff3 := Of[TestContext](Result{Value: "backup", Code: 201})
combined := effectMonoid.Concat(effectMonoid.Concat(eff1, eff2), eff3)
result, err := runEffect(combined, TestContext{Value: "test"})
@@ -295,11 +295,11 @@ func TestMonoidComparison(t *testing.T) {
"",
)
applicativeMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
applicativeMonoid := ApplicativeMonoid[TestContext](stringMonoid)
alternativeMonoid := AlternativeMonoid[TestContext](stringMonoid)
eff1 := Of[TestContext, string]("A")
eff2 := Of[TestContext, string]("B")
eff1 := Of[TestContext]("A")
eff2 := Of[TestContext]("B")
// Applicative combines values
applicativeResult, err1 := runEffect(
@@ -325,11 +325,11 @@ func TestMonoidComparison(t *testing.T) {
0,
)
applicativeMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, int](intMonoid)
applicativeMonoid := ApplicativeMonoid[TestContext](intMonoid)
alternativeMonoid := AlternativeMonoid[TestContext](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Of[TestContext, int](42)
eff2 := Of[TestContext](42)
// Applicative fails on first error
_, err1 := runEffect(

View File

@@ -34,7 +34,7 @@ func TestRetrying(t *testing.T) {
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Of[TestContext, string]("success")
return Of[TestContext]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
@@ -59,7 +59,7 @@ func TestRetrying(t *testing.T) {
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("temporary error"))
}
return Of[TestContext, string]("success after retries")
return Of[TestContext]("success after retries")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
@@ -78,7 +78,7 @@ func TestRetrying(t *testing.T) {
maxRetries := uint(3)
policy := retry.LimitRetries(maxRetries)
eff := Retrying[TestContext, string](
eff := Retrying(
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
@@ -103,7 +103,7 @@ func TestRetrying(t *testing.T) {
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext, int](42)
return Of[TestContext](42)
},
func(res Result[int]) bool {
return result.IsLeft(res) // retry on error
@@ -125,7 +125,7 @@ func TestRetrying(t *testing.T) {
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext, int](attemptCount * 10)
return Of[TestContext](attemptCount * 10)
},
func(res Result[int]) bool {
// Retry if value is less than 30
@@ -155,7 +155,7 @@ func TestRetrying(t *testing.T) {
if len(statuses) < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("done")
return Of[TestContext]("done")
},
func(res Result[string]) bool {
return result.IsLeft(res)
@@ -188,7 +188,7 @@ func TestRetrying(t *testing.T) {
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success")
return Of[TestContext]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
@@ -218,7 +218,7 @@ func TestRetrying(t *testing.T) {
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success")
return Of[TestContext]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
@@ -250,7 +250,7 @@ func TestRetrying(t *testing.T) {
return Fail[TestContext, string](err)
}
attemptCount++
return Of[TestContext, string]("finally succeeded")
return Of[TestContext]("finally succeeded")
},
func(res Result[string]) bool {
return result.IsLeft(res)
@@ -268,7 +268,7 @@ func TestRetrying(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, string](
eff := Retrying(
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
@@ -297,7 +297,7 @@ func TestRetrying(t *testing.T) {
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success with context")
return Of[TestContext]("success with context")
},
func(res Result[string]) bool {
return result.IsLeft(res)
@@ -335,7 +335,7 @@ func TestRetryingWithComplexScenarios(t *testing.T) {
if status.IterNumber < 2 {
return Fail[TestContext, State](errors.New("retry"))
}
return Of[TestContext, State](state)
return Of[TestContext](state)
},
func(res Result[State]) bool {
return result.IsLeft(res)
@@ -353,8 +353,8 @@ func TestRetryingWithComplexScenarios(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)))
eff := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext]("final: " + string(rune('0'+x)))
})(Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
@@ -362,7 +362,7 @@ func TestRetryingWithComplexScenarios(t *testing.T) {
if attemptCount < 2 {
return Fail[TestContext, int](errors.New("retry"))
}
return Of[TestContext, int](attemptCount)
return Of[TestContext](attemptCount)
},
func(res Result[int]) bool {
return result.IsLeft(res)

View File

@@ -26,10 +26,10 @@ import (
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")
eff := Of[TestContext]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -43,10 +43,10 @@ func TestProvide(t *testing.T) {
}
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config, string]("connected")
eff := Of[Config]("connected")
ioResult := Provide[Config, string](cfg)(eff)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -59,7 +59,7 @@ func TestProvide(t *testing.T) {
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
@@ -72,10 +72,10 @@ func TestProvide(t *testing.T) {
}
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext, int](100)
eff := Of[SimpleContext](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -85,12 +85,12 @@ func TestProvide(t *testing.T) {
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))
eff := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext]("result")
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -102,10 +102,10 @@ func TestProvide(t *testing.T) {
eff := Map[TestContext](func(x int) string {
return "mapped"
})(Of[TestContext, int](42))
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -116,10 +116,10 @@ func TestProvide(t *testing.T) {
func TestRunSync(t *testing.T) {
t.Run("runs effect synchronously", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, int](42)
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -128,10 +128,10 @@ func TestRunSync(t *testing.T) {
t.Run("runs effect with context.Context", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, string]("hello")
eff := Of[TestContext]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
readerResult := RunSync(ioResult)
bgCtx := context.Background()
result, err := readerResult(bgCtx)
@@ -146,7 +146,7 @@ func TestRunSync(t *testing.T) {
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
@@ -156,14 +156,14 @@ func TestRunSync(t *testing.T) {
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)))
eff := Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x + 10)
})(Of[TestContext](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -172,10 +172,10 @@ func TestRunSync(t *testing.T) {
t.Run("handles multiple sequential runs", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, int](42)
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
readerResult := RunSync(ioResult)
// Run multiple times
result1, err1 := readerResult(context.Background())
@@ -198,10 +198,10 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext, User](user)
eff := Of[TestContext](user)
ioResult := Provide[TestContext, User](ctx)(eff)
readerResult := RunSync[User](ioResult)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
@@ -219,10 +219,10 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
cfg := AppConfig{APIKey: "secret", Timeout: 30}
// Create an effect that uses the config
eff := Of[AppConfig, string]("API call successful")
eff := Of[AppConfig]("API call successful")
// Provide config and run
result, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
@@ -238,7 +238,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
@@ -249,11 +249,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
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)))
})(Chain(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(Of[TestContext](21)))
result, err := RunSync[string](Provide[TestContext, string](ctx)(eff))(context.Background())
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
@@ -267,7 +267,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Bind[TestContext](
eff := Bind(
func(y int) func(State) State {
return func(s State) State {
s.Y = y
@@ -275,13 +275,13 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
}
},
func(s State) Effect[TestContext, int] {
return Of[TestContext, int](s.X * 2)
return Of[TestContext](s.X * 2)
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext, int](10)))
})(Of[TestContext](10)))
result, err := RunSync[State](Provide[TestContext, State](ctx)(eff))(context.Background())
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
@@ -297,14 +297,14 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
}
outerCtx := OuterCtx{Value: "outer"}
innerEff := Of[InnerCtx, string]("inner result")
innerEff := Of[InnerCtx]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync[string](Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
@@ -314,11 +314,11 @@ func TestProvideAndRunSyncIntegration(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)
eff := TraverseArray(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})(input)
result, err := RunSync[[]int](Provide[TestContext, []int](ctx)(eff))(context.Background())
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)

View File

@@ -27,8 +27,8 @@ import (
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))
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -39,8 +39,8 @@ func TestTraverseArray(t *testing.T) {
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))
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -51,8 +51,8 @@ func TestTraverseArray(t *testing.T) {
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))
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -63,8 +63,8 @@ func TestTraverseArray(t *testing.T) {
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))
kleisli := TraverseArray(func(s string) Effect[TestContext, int] {
return Of[TestContext](len(s))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -76,11 +76,11 @@ func TestTraverseArray(t *testing.T) {
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] {
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
if x == 3 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
return Of[TestContext](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -96,8 +96,8 @@ func TestTraverseArray(t *testing.T) {
}
input := []int{1, 2, 3}
kleisli := TraverseArray[TestContext](func(id int) Effect[TestContext, User] {
return Of[TestContext, User](User{
kleisli := TraverseArray(func(id int) Effect[TestContext, User] {
return Of[TestContext](User{
ID: id,
Name: fmt.Sprintf("User%d", id),
})
@@ -118,15 +118,15 @@ func TestTraverseArray(t *testing.T) {
t.Run("chains with other operations", func(t *testing.T) {
input := []int{1, 2, 3}
eff := Chain[TestContext](func(strings []string) Effect[TestContext, int] {
eff := Chain(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))
return Of[TestContext](total)
})(TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x * 2))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
@@ -137,10 +137,10 @@ func TestTraverseArray(t *testing.T) {
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"}))
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Chain(func(ctx TestContext) Effect[TestContext, string] {
return Of[TestContext](fmt.Sprintf("%s-%d", ctx.Value, x))
})(Of[TestContext](TestContext{Value: "prefix"}))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -151,8 +151,8 @@ func TestTraverseArray(t *testing.T) {
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)
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 10)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -168,8 +168,8 @@ func TestTraverseArray(t *testing.T) {
input[i] = i
}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
kleisli := TraverseArray(func(x int) Effect[TestContext, int] {
return Of[TestContext](x * 2)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -184,16 +184,16 @@ func TestTraverseArray(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))
kleisli1 := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
// Second traversal: string -> int (length)
kleisli2 := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
return Of[TestContext, int](len(s))
kleisli2 := TraverseArray(func(s string) Effect[TestContext, int] {
return Of[TestContext](len(s))
})
eff := Chain[TestContext](kleisli2)(kleisli1(input))
eff := Chain(kleisli2)(kleisli1(input))
result, err := runEffect(eff, TestContext{Value: "test"})
@@ -203,8 +203,8 @@ func TestTraverseArray(t *testing.T) {
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))
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -222,8 +222,8 @@ func TestTraverseArray(t *testing.T) {
result += s + ","
}
return result
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})(TraverseArray(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
@@ -235,11 +235,11 @@ func TestTraverseArray(t *testing.T) {
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] {
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
return Of[TestContext](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
@@ -251,11 +251,11 @@ func TestTraverseArray(t *testing.T) {
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] {
kleisli := TraverseArray(func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
return Of[TestContext](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})

View File

@@ -188,6 +188,81 @@ func MonadChain[E, A, B any](fa Either[E, A], f Kleisli[E, A, B]) Either[E, B] {
return f(fa.r)
}
// MonadChainLeft sequences a computation on the Left (error) value, allowing error recovery or transformation.
// If the Either is Left, applies the provided function to the error value, which returns a new Either.
// If the Either is Right, returns the Right value unchanged with the new error type.
//
// This is the dual of [MonadChain] - while MonadChain operates on Right values (success),
// MonadChainLeft operates on Left values (errors). It's useful for error recovery, error transformation,
// or chaining alternative computations when an error occurs.
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// The error type can be transformed from EA to EB, allowing flexible error type conversions.
//
// Example:
//
// // Error recovery: convert specific errors to success
// result := either.MonadChainLeft(
// either.Left[int](errors.New("not found")),
// func(err error) either.Either[string, int] {
// if err.Error() == "not found" {
// return either.Right[string](0) // default value
// }
// return either.Left[int](err.Error()) // transform error
// },
// ) // Right(0)
//
// // Error transformation: change error type
// result := either.MonadChainLeft(
// either.Left[int](404),
// func(code int) either.Either[string, int] {
// return either.Left[int](fmt.Sprintf("Error code: %d", code))
// },
// ) // Left("Error code: 404")
//
// // Right values pass through unchanged
// result := either.MonadChainLeft(
// either.Right[error](42),
// func(err error) either.Either[string, int] {
// return either.Left[int]("error")
// },
// ) // Right(42)
//
//go:inline
func MonadChainLeft[EA, EB, A any](fa Either[EA, A], f Kleisli[EB, EA, A]) Either[EB, A] {
return MonadFold(fa, f, Of[EB])
}
// ChainLeft is the curried version of [MonadChainLeft].
// Returns a function that sequences a computation on the Left (error) value.
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is useful for creating reusable error handlers or transformers that can be
// composed with other Either operations using pipes or function composition.
//
// Example:
//
// // Create a reusable error handler
// handleNotFound := either.ChainLeft[error, string](func(err error) either.Either[string, int] {
// if err.Error() == "not found" {
// return either.Right[string](0)
// }
// return either.Left[int](err.Error())
// })
//
// // Use in a pipeline
// result := F.Pipe1(
// either.Left[int](errors.New("not found")),
// handleNotFound,
// ) // Right(0)
//
//go:inline
func ChainLeft[EA, EB, A any](f Kleisli[EB, EA, A]) Kleisli[EB, Either[EA, A], A] {
return Fold(f, Of[EB])
}
// MonadChainFirst executes a side-effect computation but returns the original value.
// Useful for performing actions (like logging) without changing the value.
//
@@ -471,6 +546,8 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
// If the Either is Left, it applies the provided function to the error value,
// which returns a new Either that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative computations.
// The error type can be widened from E1 to E2, allowing transformation of error types.
//

View File

@@ -124,79 +124,52 @@ func TestStringer(t *testing.T) {
func TestZeroWithIntegers(t *testing.T) {
e := Zero[error, int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, 0, value, "Right value should be zero for int")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](0), e, "Zero should create a Right value with zero for int")
}
// TestZeroWithStrings tests Zero function with string types
func TestZeroWithStrings(t *testing.T) {
e := Zero[error, string]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, "", value, "Right value should be empty string")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](""), e, "Zero should create a Right value with empty string")
}
// TestZeroWithBooleans tests Zero function with boolean types
func TestZeroWithBooleans(t *testing.T) {
e := Zero[error, bool]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Equal(t, false, value, "Right value should be false for bool")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](false), e, "Zero should create a Right value with false for bool")
}
// TestZeroWithFloats tests Zero function with float types
func TestZeroWithFloats(t *testing.T) {
e := Zero[error, float64]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](0.0), e, "Zero should create a Right value with 0.0 for float64")
}
// TestZeroWithPointers tests Zero function with pointer types
func TestZeroWithPointers(t *testing.T) {
e := Zero[error, *int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for pointer type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilPtr *int
assert.Equal(t, Of[error](nilPtr), e, "Zero should create a Right value with nil pointer")
}
// TestZeroWithSlices tests Zero function with slice types
func TestZeroWithSlices(t *testing.T) {
e := Zero[error, []int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for slice type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilSlice []int
assert.Equal(t, Of[error](nilSlice), e, "Zero should create a Right value with nil slice")
}
// TestZeroWithMaps tests Zero function with map types
func TestZeroWithMaps(t *testing.T) {
e := Zero[error, map[string]int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for map type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilMap map[string]int
assert.Equal(t, Of[error](nilMap), e, "Zero should create a Right value with nil map")
}
// TestZeroWithStructs tests Zero function with struct types
@@ -208,23 +181,16 @@ func TestZeroWithStructs(t *testing.T) {
e := Zero[error, TestStruct]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
expected := TestStruct{Field1: 0, Field2: ""}
assert.Equal(t, expected, value, "Right value should be zero value for struct")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for struct")
}
// TestZeroWithInterfaces tests Zero function with interface types
func TestZeroWithInterfaces(t *testing.T) {
e := Zero[error, interface{}]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for interface type")
assert.Nil(t, err, "Error should be nil for Right value")
var nilInterface interface{}
assert.Equal(t, Of[error](nilInterface), e, "Zero should create a Right value with nil interface")
}
// TestZeroWithCustomErrorType tests Zero function with custom error types
@@ -236,12 +202,7 @@ func TestZeroWithCustomErrorType(t *testing.T) {
e := Zero[CustomError, string]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, "", value, "Right value should be empty string")
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
assert.Equal(t, Of[CustomError](""), e, "Zero should create a Right value with empty string")
}
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
@@ -252,17 +213,13 @@ func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
mapped := MonadMap(e, func(n int) string {
return fmt.Sprintf("%d", n)
})
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
value, _ := Unwrap(mapped)
assert.Equal(t, "0", value, "Mapped value should be '0'")
assert.Equal(t, Of[error]("0"), mapped, "Mapped Zero should be Right with '0'")
// Test with Chain
chained := MonadChain(e, func(n int) Either[error, string] {
return Right[error](fmt.Sprintf("value: %d", n))
})
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
chainedValue, _ := Unwrap(chained)
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
assert.Equal(t, Of[error]("value: 0"), chained, "Chained Zero should be Right with 'value: 0'")
// Test with Fold
folded := MonadFold(e,
@@ -295,23 +252,15 @@ func TestZeroWithComplexTypes(t *testing.T) {
e := Zero[error, ComplexType]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
expected := ComplexType{Nested: nil, Ptr: nil}
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](expected), e, "Zero should create a Right value with zero value for complex struct")
}
// TestZeroWithOption tests Zero with Option type
func TestZeroWithOption(t *testing.T) {
e := Zero[error, O.Option[int]]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
assert.Nil(t, err, "Error should be nil for Right value")
assert.Equal(t, Of[error](O.None[int]()), e, "Zero should create a Right value with None option")
}
// TestZeroIsNotLeft tests that Zero never creates a Left value
@@ -343,3 +292,211 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
}
// TestMonadChainLeft tests the MonadChainLeft function with various scenarios
func TestMonadChainLeft(t *testing.T) {
t.Run("Left value is transformed by function", func(t *testing.T) {
// Transform error to success
result := MonadChainLeft(
Left[int](errors.New("not found")),
func(err error) Either[string, int] {
if err.Error() == "not found" {
return Right[string](0) // default value
}
return Left[int](err.Error())
},
)
assert.Equal(t, Of[string](0), result)
})
t.Run("Left value error type is transformed", func(t *testing.T) {
// Transform error type from int to string
result := MonadChainLeft(
Left[int](404),
func(code int) Either[string, int] {
return Left[int](fmt.Sprintf("Error code: %d", code))
},
)
assert.Equal(t, Left[int]("Error code: 404"), result)
})
t.Run("Right value passes through unchanged", func(t *testing.T) {
// Right value should not be affected
result := MonadChainLeft(
Right[error](42),
func(err error) Either[string, int] {
return Left[int]("should not be called")
},
)
assert.Equal(t, Of[string](42), result)
})
t.Run("Chain multiple error transformations", func(t *testing.T) {
// First transformation
step1 := MonadChainLeft(
Left[int](errors.New("error1")),
func(err error) Either[error, int] {
return Left[int](errors.New("error2"))
},
)
// Second transformation
step2 := MonadChainLeft(
step1,
func(err error) Either[string, int] {
return Left[int](err.Error())
},
)
assert.Equal(t, Left[int]("error2"), step2)
})
t.Run("Error recovery with fallback", func(t *testing.T) {
// Recover from specific errors
result := MonadChainLeft(
Left[int](errors.New("timeout")),
func(err error) Either[error, int] {
if err.Error() == "timeout" {
return Right[error](999) // fallback value
}
return Left[int](err)
},
)
assert.Equal(t, Of[error](999), result)
})
t.Run("Transform error to different Left", func(t *testing.T) {
// Transform one error to another
result := MonadChainLeft(
Left[string]("original error"),
func(s string) Either[int, string] {
return Left[string](len(s))
},
)
assert.Equal(t, Left[string](14), result) // length of "original error"
})
}
// TestChainLeft tests the curried ChainLeft function
func TestChainLeft(t *testing.T) {
t.Run("Curried function transforms Left value", func(t *testing.T) {
// Create a reusable error handler
handleNotFound := ChainLeft[error, string](func(err error) Either[string, int] {
if err.Error() == "not found" {
return Right[string](0)
}
return Left[int](err.Error())
})
result := handleNotFound(Left[int](errors.New("not found")))
assert.Equal(t, Of[string](0), result)
})
t.Run("Curried function with Right value", func(t *testing.T) {
handler := ChainLeft[error, string](func(err error) Either[string, int] {
return Left[int]("should not be called")
})
result := handler(Right[error](42))
assert.Equal(t, Of[string](42), result)
})
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
// Create error transformer
toStringError := ChainLeft[int, string](func(code int) Either[string, string] {
return Left[string](fmt.Sprintf("Error: %d", code))
})
result := F.Pipe1(
Left[string](404),
toStringError,
)
assert.Equal(t, Left[string]("Error: 404"), result)
})
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
// First handler: convert error to string
handler1 := ChainLeft[error, string](func(err error) Either[string, int] {
return Left[int](err.Error())
})
// Second handler: add prefix to string error
handler2 := ChainLeft[string, string](func(s string) Either[string, int] {
return Left[int]("Handled: " + s)
})
result := F.Pipe2(
Left[int](errors.New("original")),
handler1,
handler2,
)
assert.Equal(t, Left[int]("Handled: original"), result)
})
t.Run("Error recovery in pipeline", func(t *testing.T) {
// Handler that recovers from specific errors
recoverFromTimeout := ChainLeft(func(err error) Either[error, int] {
if err.Error() == "timeout" {
return Right[error](0) // recovered value
}
return Left[int](err) // propagate other errors
})
// Test with timeout error
result1 := F.Pipe1(
Left[int](errors.New("timeout")),
recoverFromTimeout,
)
assert.Equal(t, Of[error](0), result1)
// Test with other error
result2 := F.Pipe1(
Left[int](errors.New("other error")),
recoverFromTimeout,
)
assert.True(t, IsLeft(result2))
})
t.Run("Transform error type in pipeline", func(t *testing.T) {
// Convert numeric error codes to descriptive strings
codeToMessage := ChainLeft(func(code int) Either[string, string] {
messages := map[int]string{
404: "Not Found",
500: "Internal Server Error",
}
if msg, ok := messages[code]; ok {
return Left[string](msg)
}
return Left[string](fmt.Sprintf("Unknown error: %d", code))
})
result := F.Pipe1(
Left[string](404),
codeToMessage,
)
assert.Equal(t, Left[string]("Not Found"), result)
})
t.Run("ChainLeft with Map combination", func(t *testing.T) {
// Combine ChainLeft with Map to handle both channels
errorHandler := ChainLeft(func(err error) Either[string, int] {
return Left[int]("Error: " + err.Error())
})
valueMapper := Map[string](S.Format[int]("Value: %d"))
// Test with Left
result1 := F.Pipe2(
Left[int](errors.New("fail")),
errorHandler,
valueMapper,
)
assert.Equal(t, Left[string]("Error: fail"), result1)
// Test with Right
result2 := F.Pipe2(
Right[error](42),
errorHandler,
valueMapper,
)
assert.Equal(t, Of[string]("Value: 42"), result2)
})
}

351
v2/either/filterable.go Normal file
View File

@@ -0,0 +1,351 @@
// 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 either provides implementations of the Either type and related operations.
//
// This package implements several Fantasy Land algebraic structures:
// - Filterable: https://github.com/fantasyland/fantasy-land#filterable
//
// The Filterable specification defines operations for filtering and partitioning
// data structures based on predicates and mapping functions.
package either
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
)
// Partition separates an [Either] value into a [Pair] based on a predicate function.
// It returns a function that takes an Either and produces a Pair of Either values,
// where the first element contains values that fail the predicate and the second
// contains values that pass the predicate.
//
// This function implements the Filterable specification's partition operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, both elements of the resulting Pair will be the same Left value
// - If the input is Right and the predicate returns true, the result is (Left(empty), Right(value))
// - If the input is Right and the predicate returns false, the result is (Right(value), Left(empty))
//
// This function is useful for separating Either values into two categories based on
// a condition, commonly used in filtering operations where you want to keep track of
// both the values that pass and fail a test.
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default Left value to use when creating Left instances for partitioning
//
// Returns:
//
// A function that takes an Either[E, A] and returns a Pair where:
// - First element: Either values that fail the predicate (or original Left)
// - Second element: Either values that pass the predicate (or original Left)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// N "github.com/IBM/fp-go/v2/number"
// P "github.com/IBM/fp-go/v2/pair"
// )
//
// // Partition positive and non-positive numbers
// isPositive := N.MoreThan(0)
// partition := E.Partition(isPositive, "not positive")
//
// // Right value that passes predicate
// result1 := partition(E.Right[string](5))
// // result1 = Pair(Left("not positive"), Right(5))
// left1, right1 := P.Unpack(result1)
// // left1 = Left("not positive"), right1 = Right(5)
//
// // Right value that fails predicate
// result2 := partition(E.Right[string](-3))
// // result2 = Pair(Right(-3), Left("not positive"))
// left2, right2 := P.Unpack(result2)
// // left2 = Right(-3), right2 = Left("not positive")
//
// // Left value passes through unchanged in both positions
// result3 := partition(E.Left[int]("error"))
// // result3 = Pair(Left("error"), Left("error"))
// left3, right3 := P.Unpack(result3)
// // left3 = Left("error"), right3 = Left("error")
func Partition[E, A any](p Predicate[A], empty E) func(Either[E, A]) Pair[Either[E, A], Either[E, A]] {
l := Left[A](empty)
return func(e Either[E, A]) Pair[Either[E, A], Either[E, A]] {
if e.isLeft {
return pair.Of(e)
}
if p(e.r) {
return pair.MakePair(l, e)
}
return pair.MakePair(e, l)
}
}
// Filter creates a filtering operation for [Either] values based on a predicate function.
// It returns a function that takes an Either and produces an Either, where Right values
// that fail the predicate are converted to Left values with the provided empty value.
//
// This function implements the Filterable specification's filter operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, it passes through unchanged
// - If the input is Right and the predicate returns true, the Right value passes through unchanged
// - If the input is Right and the predicate returns false, it's converted to Left(empty)
//
// This function is useful for conditional validation or filtering of Either values,
// where you want to reject Right values that don't meet certain criteria by converting
// them to Left values with a default error.
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default Left value to use when filtering out Right values that fail the predicate
//
// Returns:
//
// An Operator function that takes an Either[E, A] and returns an Either[E, A] where:
// - Left values pass through unchanged
// - Right values that pass the predicate remain as Right
// - Right values that fail the predicate become Left(empty)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// N "github.com/IBM/fp-go/v2/number"
// )
//
// // Filter to keep only positive numbers
// isPositive := N.MoreThan(0)
// filterPositive := E.Filter(isPositive, "not positive")
//
// // Right value that passes predicate - remains Right
// result1 := filterPositive(E.Right[string](5))
// // result1 = Right(5)
//
// // Right value that fails predicate - becomes Left
// result2 := filterPositive(E.Right[string](-3))
// // result2 = Left("not positive")
//
// // Left value passes through unchanged
// result3 := filterPositive(E.Left[int]("original error"))
// // result3 = Left("original error")
//
// // Chaining filters
// isEven := func(n int) bool { return n%2 == 0 }
// filterEven := E.Filter(isEven, "not even")
//
// // Apply multiple filters in sequence
// result4 := filterEven(filterPositive(E.Right[string](4)))
// // result4 = Right(4) - passes both filters
//
// result5 := filterEven(filterPositive(E.Right[string](3)))
// // result5 = Left("not even") - passes first, fails second
func Filter[E, A any](p Predicate[A], empty E) Operator[E, A, A] {
l := Left[A](empty)
return func(e Either[E, A]) Either[E, A] {
if e.isLeft || p(e.r) {
return e
}
return l
}
}
// FilterMap combines filtering and mapping operations for [Either] values using an [Option]-returning function.
// It returns a function that takes an Either[E, A] and produces an Either[E, B], where Right values
// are transformed by applying the function f. If f returns Some(B), the result is Right(B). If f returns
// None, the result is Left(empty).
//
// This function implements the Filterable specification's filterMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, it passes through with its error value preserved as Left[B]
// - If the input is Right and f returns Some(B), the result is Right(B)
// - If the input is Right and f returns None, the result is Left(empty)
//
// This function is useful for operations that combine validation/filtering with transformation,
// such as parsing strings to numbers (where invalid strings result in None), or extracting
// optional fields from structures.
//
// Parameters:
// - f: An Option Kleisli function that transforms values of type A to Option[B]
// - empty: The default Left value to use when f returns None
//
// Returns:
//
// An Operator function that takes an Either[E, A] and returns an Either[E, B] where:
// - Left values pass through with error preserved
// - Right values are transformed by f: Some(B) becomes Right(B), None becomes Left(empty)
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// O "github.com/IBM/fp-go/v2/option"
// "strconv"
// )
//
// // Parse string to int, filtering out invalid values
// parseInt := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
// filterMapInt := E.FilterMap(parseInt, "invalid number")
//
// // Valid number string - transforms to Right(int)
// result1 := filterMapInt(E.Right[string]("42"))
// // result1 = Right(42)
//
// // Invalid number string - becomes Left
// result2 := filterMapInt(E.Right[string]("abc"))
// // result2 = Left("invalid number")
//
// // Left value passes through with error preserved
// result3 := filterMapInt(E.Left[string]("original error"))
// // result3 = Left("original error")
//
// // Extract optional field from struct
// type Person struct {
// Name string
// Email O.Option[string]
// }
// extractEmail := func(p Person) O.Option[string] { return p.Email }
// filterMapEmail := E.FilterMap(extractEmail, "no email")
//
// result4 := filterMapEmail(E.Right[string](Person{Name: "Alice", Email: O.Some("alice@example.com")}))
// // result4 = Right("alice@example.com")
//
// result5 := filterMapEmail(E.Right[string](Person{Name: "Bob", Email: O.None[string]()}))
// // result5 = Left("no email")
func FilterMap[E, A, B any](f option.Kleisli[A, B], empty E) Operator[E, A, B] {
l := Left[B](empty)
return func(e Either[E, A]) Either[E, B] {
if e.isLeft {
return Left[B](e.l)
}
if b, ok := option.Unwrap(f(e.r)); ok {
return Right[E](b)
}
return l
}
}
// PartitionMap separates and transforms an [Either] value into a [Pair] of Either values using a mapping function.
// It returns a function that takes an Either[E, A] and produces a Pair of Either values, where the mapping
// function f transforms the Right value into Either[B, C]. The result is partitioned based on whether f
// produces a Left or Right value.
//
// This function implements the Filterable specification's partitionMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is Left, both elements of the resulting Pair will be Left with the original error
// - If the input is Right and f returns Left(B), the result is (Right(B), Left(empty))
// - If the input is Right and f returns Right(C), the result is (Left(empty), Right(C))
//
// This function is useful for operations that need to categorize and transform values simultaneously,
// such as separating valid and invalid data while applying different transformations to each category.
//
// Parameters:
// - f: A Kleisli function that transforms values of type A to Either[B, C]
// - empty: The default error value to use when creating Left instances for partitioning
//
// Returns:
//
// A function that takes an Either[E, A] and returns a Pair[Either[E, B], Either[E, C]] where:
// - If input is Left: (Left(original_error), Left(original_error))
// - If f returns Left(B): (Right(B), Left(empty))
// - If f returns Right(C): (Left(empty), Right(C))
//
// Example:
//
// import (
// E "github.com/IBM/fp-go/v2/either"
// P "github.com/IBM/fp-go/v2/pair"
// )
//
// // Classify and transform numbers: negative -> error message, positive -> squared value
// classifyNumber := func(n int) E.Either[string, int] {
// if n < 0 {
// return E.Left[int]("negative: " + strconv.Itoa(n))
// }
// return E.Right[string](n * n)
// }
// partitionMap := E.PartitionMap(classifyNumber, "not classified")
//
// // Positive number - goes to right side as squared value
// result1 := partitionMap(E.Right[string](5))
// // result1 = Pair(Left("not classified"), Right(25))
// left1, right1 := P.Unpack(result1)
// // left1 = Left("not classified"), right1 = Right(25)
//
// // Negative number - goes to left side with error message
// result2 := partitionMap(E.Right[string](-3))
// // result2 = Pair(Right("negative: -3"), Left("not classified"))
// left2, right2 := P.Unpack(result2)
// // left2 = Right("negative: -3"), right2 = Left("not classified")
//
// // Original Left value - appears in both positions
// result3 := partitionMap(E.Left[int]("original error"))
// // result3 = Pair(Left("original error"), Left("original error"))
// left3, right3 := P.Unpack(result3)
// // left3 = Left("original error"), right3 = Left("original error")
//
// // Validate and transform user input
// type ValidationError struct{ Field, Message string }
// type User struct{ Name string; Age int }
//
// validateUser := func(input map[string]string) E.Either[ValidationError, User] {
// name, hasName := input["name"]
// ageStr, hasAge := input["age"]
// if !hasName {
// return E.Left[User](ValidationError{"name", "missing"})
// }
// if !hasAge {
// return E.Left[User](ValidationError{"age", "missing"})
// }
// age, err := strconv.Atoi(ageStr)
// if err != nil {
// return E.Left[User](ValidationError{"age", "invalid"})
// }
// return E.Right[ValidationError](User{name, age})
// }
// partitionUsers := E.PartitionMap(validateUser, ValidationError{"", "not processed"})
//
// validInput := map[string]string{"name": "Alice", "age": "30"}
// result4 := partitionUsers(E.Right[string](validInput))
// // result4 = Pair(Left(ValidationError{"", "not processed"}), Right(User{"Alice", 30}))
//
// invalidInput := map[string]string{"name": "Bob"}
// result5 := partitionUsers(E.Right[string](invalidInput))
// // result5 = Pair(Right(ValidationError{"age", "missing"}), Left(ValidationError{"", "not processed"}))
func PartitionMap[E, A, B, C any](f Kleisli[B, A, C], empty E) func(Either[E, A]) Pair[Either[E, B], Either[E, C]] {
return func(e Either[E, A]) Pair[Either[E, B], Either[E, C]] {
if e.isLeft {
return pair.MakePair(Left[B](e.l), Left[C](e.l))
}
res := f(e.r)
if res.isLeft {
return pair.MakePair(Right[E](res.l), Left[C](empty))
}
return pair.MakePair(Left[B](empty), Right[E](res.r))
}
}

1433
v2/either/filterable_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ import (
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -53,4 +54,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]
Pair[L, R any] = pair.Pair[L, R]
)

View File

@@ -489,6 +489,8 @@ func After[E, A any](timestamp time.Time) Operator[E, A, A] {
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is useful for error recovery or error transformation scenarios where you want to handle
// errors by performing another computation that may also fail.
//
@@ -523,6 +525,8 @@ func MonadChainLeft[EA, EB, A any](fa IOEither[EA, A], f Kleisli[EB, EA, A]) IOE
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of an [IOEither].
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This is particularly useful in functional composition pipelines where you want to handle
// errors by performing another computation that may also fail.
//
@@ -644,6 +648,8 @@ func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
// If the IOEither is Left, it applies the provided function to the error value,
// which returns a new IOEither that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
// The error type can be widened from E1 to E2, allowing transformation of error types.
//

View File

@@ -490,3 +490,148 @@ func TestOrElseW(t *testing.T) {
preserved := preserveRecover(preservedRight)()
assert.Equal(t, E.Right[AppError](42), preserved)
}
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
// This test verifies that both functions produce the same results for all scenarios:
// - Left values with error recovery
// - Left values with error transformation
// - Right values passing through unchanged
func TestChainLeftIdenticalToOrElse(t *testing.T) {
// Test 1: Left value with error recovery - both should recover to Right
t.Run("Left value recovery - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) IOEither[string, int] {
if e == "recoverable" {
return Right[string](42)
}
return Left[int](e)
}
input := Left[int]("recoverable")
// Using ChainLeft
resultChainLeft := ChainLeft(recoveryFn)(input)()
// Using OrElse
resultOrElse := OrElse(recoveryFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](42), resultChainLeft)
})
// Test 2: Left value with error transformation - both should transform error
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
transformFn := func(e string) IOEither[string, int] {
return Left[int]("transformed: " + e)
}
input := Left[int]("original error")
// Using ChainLeft
resultChainLeft := ChainLeft(transformFn)(input)()
// Using OrElse
resultOrElse := OrElse(transformFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
})
// Test 3: Right value - both should pass through unchanged
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
handlerFn := func(e string) IOEither[string, int] {
return Left[int]("should not be called")
}
input := Right[string](100)
// Using ChainLeft
resultChainLeft := ChainLeft(handlerFn)(input)()
// Using OrElse
resultOrElse := OrElse(handlerFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](100), resultChainLeft)
})
// Test 4: Error type widening - both should handle type transformation
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
widenFn := func(e string) IOEither[int, int] {
return Left[int](404)
}
input := Left[int]("not found")
// Using ChainLeft
resultChainLeft := ChainLeft(widenFn)(input)()
// Using OrElse
resultOrElse := OrElse(widenFn)(input)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int](404), resultChainLeft)
})
// Test 5: Composition in pipeline - both should work identically in F.Pipe
t.Run("Pipeline composition - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) IOEither[string, int] {
if e == "network error" {
return Right[string](0)
}
return Left[int](e)
}
input := Left[int]("network error")
// Using ChainLeft in pipeline
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))()
// Using OrElse in pipeline
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](0), resultChainLeft)
})
// Test 6: Multiple chained operations - both should behave identically
t.Run("Multiple operations - ChainLeft equals OrElse", func(t *testing.T) {
handler1 := func(e string) IOEither[string, int] {
if e == "error1" {
return Right[string](1)
}
return Left[int](e)
}
handler2 := func(e string) IOEither[string, int] {
if e == "error2" {
return Right[string](2)
}
return Left[int](e)
}
input := Left[int]("error2")
// Using ChainLeft
resultChainLeft := F.Pipe2(
input,
ChainLeft(handler1),
ChainLeft(handler2),
)()
// Using OrElse
resultOrElse := F.Pipe2(
input,
OrElse(handler1),
OrElse(handler2),
)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](2), resultChainLeft)
})
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/readerio"
)
// MakeIORef creates a new IORef containing the given initial value.
@@ -50,6 +51,32 @@ func MakeIORef[A any](a A) IO[IORef[A]] {
}
}
// Write atomically writes a new value to an IORef and returns the written value.
//
// This function returns a Kleisli arrow that takes an IORef and produces an IO
// computation that writes the given value to the reference. The write operation
// is atomic and thread-safe, using a write lock to ensure exclusive access.
//
// Parameters:
// - a: The new value to write to the IORef
//
// Returns:
// - A Kleisli arrow from IORef[A] to IO[A] that writes the value and returns it
//
// Example:
//
// ref := ioref.MakeIORef(42)()
//
// // Write a new value
// newValue := ioref.Write(100)(ref)() // Returns 100, ref now contains 100
//
// // Chain writes
// pipe.Pipe2(
// ref,
// ioref.Write(50),
// io.Chain(ioref.Write(75)),
// )() // ref now contains 75
//
//go:inline
func Write[A any](a A) io.Kleisli[IORef[A], A] {
return func(ref IORef[A]) IO[A] {
@@ -180,6 +207,57 @@ func ModifyIOK[A any](f io.Kleisli[A, A]) io.Kleisli[IORef[A], A] {
}
}
// ModifyReaderIOK atomically modifies the value in an IORef using a ReaderIO-based transformation.
//
// This is a variant of ModifyIOK that works with ReaderIO computations, allowing the
// transformation function to access an environment of type R while performing IO effects.
// This is useful when the modification logic needs access to configuration, context,
// or other shared resources.
//
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
// access during the read-modify-write cycle. The ReaderIO effect in the transformation
// function is executed while holding the lock.
//
// Parameters:
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, A]) that takes the current value
// and an environment R, and returns an IO computation producing the new value
//
// Returns:
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, A] that returns the new value
//
// Example:
//
// type Config struct {
// multiplier int
// }
//
// ref := ioref.MakeIORef(10)()
//
// // Modify using environment
// modifyWithConfig := ioref.ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
// return func(cfg Config) io.IO[int] {
// return func() int {
// return x * cfg.multiplier
// }
// }
// })
//
// config := Config{multiplier: 5}
// newValue := modifyWithConfig(ref)(config)() // Returns 50, ref now contains 50
func ModifyReaderIOK[R, A any](f readerio.Kleisli[R, A, A]) readerio.Kleisli[R, IORef[A], A] {
return func(ref IORef[A]) ReaderIO[R, A] {
return func(r R) readerio.IO[A] {
return func() A {
ref.mu.Lock()
defer ref.mu.Unlock()
ref.a = f(ref.a)(r)()
return ref.a
}
}
}
}
// ModifyWithResult atomically modifies the value in an IORef and returns both
// the new value and an additional result computed from the old value.
//
@@ -269,3 +347,62 @@ func ModifyIOKWithResult[A, B any](f io.Kleisli[A, Pair[A, B]]) io.Kleisli[IORef
}
}
}
// ModifyReaderIOKWithResult atomically modifies the value in an IORef and returns a result,
// using a ReaderIO-based transformation function.
//
// This combines the capabilities of ModifyIOKWithResult and ModifyReaderIOK, allowing the
// transformation function to:
// - Access an environment of type R (like configuration or context)
// - Perform IO effects during the transformation
// - Both update the stored value and compute a result based on the old value
// - Ensure atomicity of the entire read-transform-write-compute cycle
//
// The modification is atomic and thread-safe, using a write lock to ensure exclusive
// access. The ReaderIO effect in the transformation function is executed while holding the lock.
//
// Parameters:
// - f: A ReaderIO Kleisli arrow (readerio.Kleisli[R, A, Pair[A, B]]) that takes the old value
// and an environment R, and returns an IO computation producing a Pair of (new value, result)
//
// Returns:
// - A ReaderIO Kleisli arrow from IORef[A] to ReaderIO[R, B] that produces the result
//
// Example:
//
// type Config struct {
// logEnabled bool
// }
//
// ref := ioref.MakeIORef(42)()
//
// // Increment with conditional logging, return old value
// incrementWithLog := ioref.ModifyReaderIOKWithResult(
// func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
// return func(cfg Config) io.IO[pair.Pair[int, int]] {
// return func() pair.Pair[int, int] {
// if cfg.logEnabled {
// fmt.Printf("Incrementing from %d\n", x)
// }
// return pair.MakePair(x+1, x)
// }
// }
// },
// )
//
// config := Config{logEnabled: true}
// oldValue := incrementWithLog(ref)(config)() // Logs and returns 42, ref now contains 43
func ModifyReaderIOKWithResult[R, A, B any](f readerio.Kleisli[R, A, Pair[A, B]]) readerio.Kleisli[R, IORef[A], B] {
return func(ref IORef[A]) ReaderIO[R, B] {
return func(r R) readerio.IO[B] {
return func() B {
ref.mu.Lock()
defer ref.mu.Unlock()
result := f(ref.a)(r)()
ref.a = pair.Head(result)
return pair.Tail(result)
}
}
}
}

View File

@@ -24,9 +24,588 @@ import (
"github.com/IBM/fp-go/v2/io"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
)
func TestMakeIORef(t *testing.T) {
t.Run("creates IORef with integer value", func(t *testing.T) {
ref := MakeIORef(42)()
assert.NotNil(t, ref)
assert.Equal(t, 42, Read(ref)())
})
t.Run("creates IORef with string value", func(t *testing.T) {
ref := MakeIORef("hello")()
assert.NotNil(t, ref)
assert.Equal(t, "hello", Read(ref)())
})
t.Run("creates IORef with slice value", func(t *testing.T) {
slice := []int{1, 2, 3}
ref := MakeIORef(slice)()
assert.NotNil(t, ref)
assert.Equal(t, slice, Read(ref)())
})
t.Run("creates IORef with struct value", func(t *testing.T) {
type Person struct {
Name string
Age int
}
person := Person{Name: "Alice", Age: 30}
ref := MakeIORef(person)()
assert.NotNil(t, ref)
assert.Equal(t, person, Read(ref)())
})
t.Run("creates IORef with zero value", func(t *testing.T) {
ref := MakeIORef(0)()
assert.NotNil(t, ref)
assert.Equal(t, 0, Read(ref)())
})
t.Run("creates IORef with nil pointer", func(t *testing.T) {
var ptr *int
ref := MakeIORef(ptr)()
assert.NotNil(t, ref)
assert.Nil(t, Read(ref)())
})
t.Run("multiple IORefs are independent", func(t *testing.T) {
ref1 := MakeIORef(10)()
ref2 := MakeIORef(20)()
assert.Equal(t, 10, Read(ref1)())
assert.Equal(t, 20, Read(ref2)())
Write(30)(ref1)()
assert.Equal(t, 30, Read(ref1)())
assert.Equal(t, 20, Read(ref2)()) // ref2 unchanged
})
}
func TestRead(t *testing.T) {
t.Run("reads initial value", func(t *testing.T) {
ref := MakeIORef(42)()
value := Read(ref)()
assert.Equal(t, 42, value)
})
t.Run("reads updated value", func(t *testing.T) {
ref := MakeIORef(10)()
Write(20)(ref)()
value := Read(ref)()
assert.Equal(t, 20, value)
})
t.Run("multiple reads return same value", func(t *testing.T) {
ref := MakeIORef(100)()
value1 := Read(ref)()
value2 := Read(ref)()
value3 := Read(ref)()
assert.Equal(t, 100, value1)
assert.Equal(t, 100, value2)
assert.Equal(t, 100, value3)
})
t.Run("concurrent reads are thread-safe", func(t *testing.T) {
ref := MakeIORef(42)()
var wg sync.WaitGroup
iterations := 100
results := make([]int, iterations)
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
results[idx] = Read(ref)()
}(i)
}
wg.Wait()
// All reads should return the same value
for _, v := range results {
assert.Equal(t, 42, v)
}
})
t.Run("reads during concurrent writes", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 50
// Start concurrent writes
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
Write(val)(ref)()
}(i)
}
// Start concurrent reads
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
value := Read(ref)()
// Value should be valid (between 0 and iterations-1)
assert.GreaterOrEqual(t, value, 0)
assert.Less(t, value, iterations)
}()
}
wg.Wait()
})
}
func TestWrite(t *testing.T) {
t.Run("writes new value", func(t *testing.T) {
ref := MakeIORef(42)()
result := Write(100)(ref)()
assert.Equal(t, 100, result)
assert.Equal(t, 100, Read(ref)())
})
t.Run("overwrites existing value", func(t *testing.T) {
ref := MakeIORef(10)()
Write(20)(ref)()
Write(30)(ref)()
assert.Equal(t, 30, Read(ref)())
})
t.Run("returns written value", func(t *testing.T) {
ref := MakeIORef(0)()
result := Write(42)(ref)()
assert.Equal(t, 42, result)
})
t.Run("writes string value", func(t *testing.T) {
ref := MakeIORef("hello")()
result := Write("world")(ref)()
assert.Equal(t, "world", result)
assert.Equal(t, "world", Read(ref)())
})
t.Run("chained writes", func(t *testing.T) {
ref := MakeIORef(1)()
Write(2)(ref)()
Write(3)(ref)()
result := Write(4)(ref)()
assert.Equal(t, 4, result)
assert.Equal(t, 4, Read(ref)())
})
t.Run("concurrent writes are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
Write(val)(ref)()
}(i)
}
wg.Wait()
// Final value should be one of the written values
finalValue := Read(ref)()
assert.GreaterOrEqual(t, finalValue, 0)
assert.Less(t, finalValue, iterations)
})
t.Run("write with zero value", func(t *testing.T) {
ref := MakeIORef(42)()
Write(0)(ref)()
assert.Equal(t, 0, Read(ref)())
})
}
func TestModify(t *testing.T) {
t.Run("modifies value with simple function", func(t *testing.T) {
ref := MakeIORef(10)()
result := Modify(func(x int) int { return x * 2 })(ref)()
assert.Equal(t, 20, result)
assert.Equal(t, 20, Read(ref)())
})
t.Run("modifies with addition", func(t *testing.T) {
ref := MakeIORef(5)()
Modify(func(x int) int { return x + 10 })(ref)()
assert.Equal(t, 15, Read(ref)())
})
t.Run("modifies string value", func(t *testing.T) {
ref := MakeIORef("hello")()
result := Modify(func(s string) string { return s + " world" })(ref)()
assert.Equal(t, "hello world", result)
assert.Equal(t, "hello world", Read(ref)())
})
t.Run("chained modifications", func(t *testing.T) {
ref := MakeIORef(2)()
Modify(func(x int) int { return x * 3 })(ref)() // 6
Modify(func(x int) int { return x + 4 })(ref)() // 10
result := Modify(func(x int) int { return x / 2 })(ref)()
assert.Equal(t, 5, result)
assert.Equal(t, 5, Read(ref)())
})
t.Run("concurrent modifications are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Modify(func(x int) int { return x + 1 })(ref)()
}()
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
})
t.Run("modify with identity function", func(t *testing.T) {
ref := MakeIORef(42)()
result := Modify(func(x int) int { return x })(ref)()
assert.Equal(t, 42, result)
assert.Equal(t, 42, Read(ref)())
})
t.Run("modify returns new value", func(t *testing.T) {
ref := MakeIORef(100)()
result := Modify(func(x int) int { return x - 50 })(ref)()
assert.Equal(t, 50, result)
})
}
func TestModifyWithResult(t *testing.T) {
t.Run("modifies and returns old value", func(t *testing.T) {
ref := MakeIORef(42)()
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)()
assert.Equal(t, 42, oldValue)
assert.Equal(t, 43, Read(ref)())
})
t.Run("swaps value and returns old", func(t *testing.T) {
ref := MakeIORef(100)()
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(200, x)
})(ref)()
assert.Equal(t, 100, oldValue)
assert.Equal(t, 200, Read(ref)())
})
t.Run("returns different type", func(t *testing.T) {
ref := MakeIORef(42)()
message := ModifyWithResult(func(x int) pair.Pair[int, string] {
return pair.MakePair(x*2, fmt.Sprintf("doubled from %d", x))
})(ref)()
assert.Equal(t, "doubled from 42", message)
assert.Equal(t, 84, Read(ref)())
})
t.Run("computes result based on old value", func(t *testing.T) {
ref := MakeIORef(10)()
wasPositive := ModifyWithResult(func(x int) pair.Pair[int, bool] {
return pair.MakePair(x+5, x > 0)
})(ref)()
assert.True(t, wasPositive)
assert.Equal(t, 15, Read(ref)())
})
t.Run("chained modifications with results", func(t *testing.T) {
ref := MakeIORef(5)()
result1 := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x*2, x)
})(ref)()
result2 := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+10, x)
})(ref)()
assert.Equal(t, 5, result1)
assert.Equal(t, 10, result2)
assert.Equal(t, 20, Read(ref)())
})
t.Run("concurrent modifications with results are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
var wg sync.WaitGroup
iterations := 100
results := make([]int, iterations)
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
oldValue := ModifyWithResult(func(x int) pair.Pair[int, int] {
return pair.MakePair(x+1, x)
})(ref)()
results[idx] = oldValue
}(i)
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
// All old values should be unique
seen := make(map[int]bool)
for _, v := range results {
assert.False(t, seen[v])
seen[v] = true
}
})
t.Run("extract and replace pattern", func(t *testing.T) {
ref := MakeIORef([]int{1, 2, 3})()
first := ModifyWithResult(func(xs []int) pair.Pair[[]int, int] {
if len(xs) == 0 {
return pair.MakePair(xs, 0)
}
return pair.MakePair(xs[1:], xs[0])
})(ref)()
assert.Equal(t, 1, first)
assert.Equal(t, []int{2, 3}, Read(ref)())
})
}
func TestModifyReaderIOK(t *testing.T) {
type Config struct {
multiplier int
}
t.Run("modifies with environment", func(t *testing.T) {
ref := MakeIORef(10)()
config := Config{multiplier: 5}
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x * cfg.multiplier)
}
})(ref)(config)()
assert.Equal(t, 50, result)
assert.Equal(t, 50, Read(ref)())
})
t.Run("uses environment for computation", func(t *testing.T) {
ref := MakeIORef(100)()
config := Config{multiplier: 2}
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return func() int {
return x / cfg.multiplier
}
}
})(ref)(config)()
assert.Equal(t, 50, result)
assert.Equal(t, 50, Read(ref)())
})
t.Run("chained modifications with different configs", func(t *testing.T) {
ref := MakeIORef(10)()
config1 := Config{multiplier: 2}
config2 := Config{multiplier: 3}
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x * cfg.multiplier)
}
})(ref)(config1)()
result := ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x + cfg.multiplier)
}
})(ref)(config2)()
assert.Equal(t, 23, result) // (10 * 2) + 3
assert.Equal(t, 23, Read(ref)())
})
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
config := Config{multiplier: 1}
var wg sync.WaitGroup
iterations := 100
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ModifyReaderIOK(func(x int) readerio.ReaderIO[Config, int] {
return func(cfg Config) io.IO[int] {
return io.Of(x + cfg.multiplier)
}
})(ref)(config)()
}()
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
})
t.Run("environment provides configuration", func(t *testing.T) {
type Settings struct {
prefix string
}
ref := MakeIORef("world")()
settings := Settings{prefix: "hello "}
result := ModifyReaderIOK(func(s string) readerio.ReaderIO[Settings, string] {
return func(cfg Settings) io.IO[string] {
return io.Of(cfg.prefix + s)
}
})(ref)(settings)()
assert.Equal(t, "hello world", result)
assert.Equal(t, "hello world", Read(ref)())
})
}
func TestModifyReaderIOKWithResult(t *testing.T) {
type Config struct {
logEnabled bool
multiplier int
}
t.Run("modifies with environment and returns result", func(t *testing.T) {
ref := MakeIORef(42)()
config := Config{logEnabled: false, multiplier: 2}
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x*cfg.multiplier, x))
}
})(ref)(config)()
assert.Equal(t, 42, oldValue)
assert.Equal(t, 84, Read(ref)())
})
t.Run("returns different type based on environment", func(t *testing.T) {
ref := MakeIORef(10)()
config := Config{logEnabled: true, multiplier: 3}
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
return func(cfg Config) io.IO[pair.Pair[int, string]] {
return func() pair.Pair[int, string] {
newVal := x * cfg.multiplier
msg := fmt.Sprintf("multiplied %d by %d", x, cfg.multiplier)
return pair.MakePair(newVal, msg)
}
}
})(ref)(config)()
assert.Equal(t, "multiplied 10 by 3", message)
assert.Equal(t, 30, Read(ref)())
})
t.Run("conditional logic based on environment", func(t *testing.T) {
ref := MakeIORef(-10)()
config := Config{logEnabled: true, multiplier: 2}
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, string]] {
return func(cfg Config) io.IO[pair.Pair[int, string]] {
return func() pair.Pair[int, string] {
if x < 0 {
return pair.MakePair(0, "reset negative value")
}
return pair.MakePair(x*cfg.multiplier, "multiplied positive value")
}
}
})(ref)(config)()
assert.Equal(t, "reset negative value", message)
assert.Equal(t, 0, Read(ref)())
})
t.Run("chained modifications with results", func(t *testing.T) {
ref := MakeIORef(5)()
config := Config{logEnabled: false, multiplier: 2}
result1 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x*cfg.multiplier, x))
}
})(ref)(config)()
result2 := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x+cfg.multiplier, x))
}
})(ref)(config)()
assert.Equal(t, 5, result1)
assert.Equal(t, 10, result2)
assert.Equal(t, 12, Read(ref)())
})
t.Run("concurrent modifications with environment are thread-safe", func(t *testing.T) {
ref := MakeIORef(0)()
config := Config{logEnabled: false, multiplier: 1}
var wg sync.WaitGroup
iterations := 100
results := make([]int, iterations)
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
oldValue := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[Config, pair.Pair[int, int]] {
return func(cfg Config) io.IO[pair.Pair[int, int]] {
return io.Of(pair.MakePair(x+cfg.multiplier, x))
}
})(ref)(config)()
results[idx] = oldValue
}(i)
}
wg.Wait()
assert.Equal(t, iterations, Read(ref)())
// All old values should be unique
seen := make(map[int]bool)
for _, v := range results {
assert.False(t, seen[v])
seen[v] = true
}
})
t.Run("environment provides validation rules", func(t *testing.T) {
type ValidationConfig struct {
maxValue int
}
ref := MakeIORef(100)()
config := ValidationConfig{maxValue: 50}
message := ModifyReaderIOKWithResult(func(x int) readerio.ReaderIO[ValidationConfig, pair.Pair[int, string]] {
return func(cfg ValidationConfig) io.IO[pair.Pair[int, string]] {
return func() pair.Pair[int, string] {
if x > cfg.maxValue {
return pair.MakePair(cfg.maxValue, fmt.Sprintf("capped at %d", cfg.maxValue))
}
return pair.MakePair(x, "value within limits")
}
}
})(ref)(config)()
assert.Equal(t, "capped at 50", message)
assert.Equal(t, 50, Read(ref)())
})
}
func TestModifyIOK(t *testing.T) {
t.Run("basic modification with IO effect", func(t *testing.T) {
ref := MakeIORef(42)()

View File

@@ -45,29 +45,110 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/readerio"
)
type (
// ioRef is the internal implementation of a mutable reference.
// It uses a read-write mutex to ensure thread-safe access.
// It uses a read-write mutex to ensure thread-safe access to the stored value.
//
// The mutex allows multiple concurrent readers (using RLock) but ensures
// exclusive access for writers (using Lock), preventing race conditions
// when reading or modifying the stored value.
//
// This type is not exported; users interact with it through the IORef type alias.
ioRef[A any] struct {
mu sync.RWMutex
a A
mu sync.RWMutex // Protects concurrent access to the stored value
a A // The stored value
}
// IO represents a synchronous computation that may have side effects.
// It's a function that takes no arguments and returns a value of type A.
//
// IO computations are lazy - they don't execute until explicitly invoked
// by calling the function. This allows for composing and chaining effects
// before execution.
//
// Example:
//
// // Define an IO computation
// computation := func() int {
// fmt.Println("Computing...")
// return 42
// }
//
// // Nothing happens yet - the computation is lazy
// result := computation() // Now it executes and prints "Computing..."
IO[A any] = io.IO[A]
// ReaderIO represents a computation that requires an environment of type R
// and produces an IO effect that yields a value of type A.
//
// This combines the Reader pattern (dependency injection) with IO effects,
// allowing computations to access shared configuration or context while
// performing side effects.
//
// Example:
//
// type Config struct {
// multiplier int
// }
//
// // A ReaderIO that uses config to compute a value
// computation := func(cfg Config) io.IO[int] {
// return func() int {
// return 42 * cfg.multiplier
// }
// }
//
// // Execute with specific config
// result := computation(Config{multiplier: 2})() // Returns 84
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// IORef represents a mutable reference to a value of type A.
// Operations on IORef are thread-safe and performed within the IO monad.
//
// IORef provides a way to work with mutable state in a functional style,
// where mutations are explicit and contained within IO computations.
// This makes side effects visible in the type system and allows for
// better reasoning about code that uses mutable state.
//
// All operations on IORef (Read, Write, Modify, etc.) are atomic and
// thread-safe, making it safe to share IORefs across goroutines.
//
// Example:
//
// // Create a new IORef
// ref := ioref.MakeIORef(42)()
//
// // Read the current value
// value := ioref.Read(ref)() // 42
//
// // Write a new value
// ioref.Write(100)(ref)()
//
// // Modify the value atomically
// ioref.Modify(func(x int) int { return x * 2 })(ref)()
IORef[A any] = *ioRef[A]
// Endomorphism represents a function from A to A.
// It's commonly used with Modify to transform the value in an IORef.
//
// An endomorphism is a morphism (structure-preserving map) from a
// mathematical object to itself. In programming terms, it's simply
// a function that takes a value and returns a value of the same type.
//
// Example:
//
// // An endomorphism that doubles an integer
// double := func(x int) int { return x * 2 }
//
// // An endomorphism that uppercases a string
// upper := func(s string) string { return strings.ToUpper(s) }
//
// // Use with IORef
// ref := ioref.MakeIORef(21)()
// ioref.Modify(double)(ref)() // ref now contains 42
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Pair represents a tuple of two values of types A and B.
@@ -76,6 +157,8 @@ type (
//
// The head of the pair contains the new value to store in the IORef,
// while the tail contains the result to return from the operation.
// This allows atomic operations that both update the reference and
// compute a result based on the old value.
//
// Example:
//
@@ -85,5 +168,11 @@ type (
// // Extract values
// newVal := pair.Head(p) // Gets the head (new value)
// oldVal := pair.Tail(p) // Gets the tail (old value)
//
// // Use with ModifyWithResult to swap and return old value
// ref := ioref.MakeIORef(42)()
// oldValue := ioref.ModifyWithResult(func(x int) pair.Pair[int, int] {
// return pair.MakePair(100, x) // Store 100, return old value
// })(ref)() // oldValue is 42, ref now contains 100
Pair[A, B any] = pair.Pair[A, B]
)

View File

@@ -466,6 +466,11 @@ func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
return F.Bind2nd(MonadChain[A, B], f)
}
//go:inline
func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
return Chain(f)
}
// Flatten flattens a sequence of sequences into a single sequence.
//
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll

158
v2/iterator/iter/option.go Normal file
View File

@@ -0,0 +1,158 @@
// 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 iter
import (
"github.com/IBM/fp-go/v2/option"
)
// MonadChainOptionK chains a function that returns an Option into a sequence,
// filtering out None values and unwrapping Some values.
//
// This is useful for operations that may or may not produce a value for each element
// in the sequence. Only the successful (Some) results are included in the output sequence,
// while None values are filtered out.
//
// This is the monadic form that takes the sequence as the first parameter.
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - as: The input sequence to transform
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// A new sequence containing only the unwrapped Some values
//
// Example:
//
// import (
// "strconv"
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Parse strings to integers, filtering out invalid ones
// parseNum := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
//
// seq := I.From("1", "invalid", "2", "3", "bad")
// result := I.MonadChainOptionK(seq, parseNum)
// // yields: 1, 2, 3 (invalid strings are filtered out)
func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
return MonadFilterMap(as, f)
}
// ChainOptionK returns an operator that chains a function returning an Option into a sequence,
// filtering out None values and unwrapping Some values.
//
// This is the curried version of [MonadChainOptionK], useful for function composition
// and creating reusable transformations.
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
//
// Example:
//
// import (
// "strconv"
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Create a reusable parser operator
// parsePositive := I.ChainOptionK(func(x int) O.Option[int] {
// if x > 0 {
// return O.Some(x)
// }
// return O.None[int]()
// })
//
// result := F.Pipe1(
// I.From(-1, 2, -3, 4, 5),
// parsePositive,
// )
// // yields: 2, 4, 5 (negative numbers are filtered out)
//
//go:inline
func ChainOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return FilterMap(f)
}
// FlatMapOptionK is an alias for [ChainOptionK].
//
// This provides a more familiar name for developers coming from other functional
// programming languages or libraries where "flatMap" is the standard terminology
// for the monadic bind operation.
//
// Type parameters:
// - A: The element type of the input sequence
// - B: The element type of the output sequence (wrapped in Option by the function)
//
// Parameters:
// - f: A function that takes an element and returns an Option[B]
//
// Returns:
//
// An Operator that transforms Seq[A] to Seq[B], filtering out None values
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// O "github.com/IBM/fp-go/v2/option"
// I "github.com/IBM/fp-go/v2/iterator/iter"
// )
//
// // Validate and transform data
// validateAge := I.FlatMapOptionK(func(age int) O.Option[string] {
// if age >= 18 && age <= 120 {
// return O.Some(fmt.Sprintf("Valid age: %d", age))
// }
// return O.None[string]()
// })
//
// result := F.Pipe1(
// I.From(15, 25, 150, 30),
// validateAge,
// )
// // yields: "Valid age: 25", "Valid age: 30"
//
//go:inline
func FlatMapOptionK[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return ChainOptionK(f)
}

View File

@@ -0,0 +1,387 @@
// 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 iter
import (
"fmt"
"slices"
"strconv"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestMonadChainOptionK_AllSome tests MonadChainOptionK when all values produce Some
func TestMonadChainOptionK_AllSome(t *testing.T) {
// Function that always returns Some
double := func(x int) O.Option[int] {
return O.Some(x * 2)
}
seq := From(1, 2, 3, 4, 5)
result := MonadChainOptionK(seq, double)
values := slices.Collect(result)
expected := A.From(2, 4, 6, 8, 10)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_AllNone tests MonadChainOptionK when all values produce None
func TestMonadChainOptionK_AllNone(t *testing.T) {
// Function that always returns None
alwaysNone := func(x int) O.Option[int] {
return O.None[int]()
}
seq := From(1, 2, 3, 4, 5)
result := MonadChainOptionK(seq, alwaysNone)
values := slices.Collect(result)
assert.Empty(t, values)
}
// TestMonadChainOptionK_MixedSomeNone tests MonadChainOptionK with mixed Some and None
func TestMonadChainOptionK_MixedSomeNone(t *testing.T) {
// Function that returns Some for even numbers, None for odd
evenOnly := func(x int) O.Option[int] {
if x%2 == 0 {
return O.Some(x)
}
return O.None[int]()
}
seq := From(1, 2, 3, 4, 5, 6)
result := MonadChainOptionK(seq, evenOnly)
values := slices.Collect(result)
expected := A.From(2, 4, 6)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_ParseStrings tests parsing strings to integers
func TestMonadChainOptionK_ParseStrings(t *testing.T) {
// Parse strings to integers, returning None for invalid strings
parseNum := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
seq := From("1", "invalid", "2", "3", "bad", "4")
result := MonadChainOptionK(seq, parseNum)
values := slices.Collect(result)
expected := A.From(1, 2, 3, 4)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_EmptySequence tests MonadChainOptionK with empty sequence
func TestMonadChainOptionK_EmptySequence(t *testing.T) {
double := func(x int) O.Option[int] {
return O.Some(x * 2)
}
seq := From[int]()
result := MonadChainOptionK(seq, double)
values := slices.Collect(result)
assert.Empty(t, values)
}
// TestMonadChainOptionK_TypeTransformation tests transforming types
func TestMonadChainOptionK_TypeTransformation(t *testing.T) {
// Convert integers to strings, only for positive numbers
positiveToString := func(x int) O.Option[string] {
if x > 0 {
return O.Some(fmt.Sprintf("num_%d", x))
}
return O.None[string]()
}
seq := From(-2, -1, 0, 1, 2, 3)
result := MonadChainOptionK(seq, positiveToString)
values := slices.Collect(result)
expected := A.From("num_1", "num_2", "num_3")
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_ComplexType tests with complex types
func TestMonadChainOptionK_ComplexType(t *testing.T) {
type Person struct {
Name string
Age int
}
// Extract age only for adults
getAdultAge := func(p Person) O.Option[int] {
if p.Age >= 18 {
return O.Some(p.Age)
}
return O.None[int]()
}
seq := From(
Person{"Alice", 25},
Person{"Bob", 15},
Person{"Charlie", 30},
Person{"David", 12},
)
result := MonadChainOptionK(seq, getAdultAge)
values := slices.Collect(result)
expected := A.From(25, 30)
assert.Equal(t, expected, values)
}
// TestChainOptionK_BasicUsage tests ChainOptionK basic functionality
func TestChainOptionK_BasicUsage(t *testing.T) {
// Create a reusable operator
parsePositive := ChainOptionK(func(x int) O.Option[int] {
if x > 0 {
return O.Some(x)
}
return O.None[int]()
})
seq := From(-1, 2, -3, 4, 5, -6)
result := parsePositive(seq)
values := slices.Collect(result)
expected := A.From(2, 4, 5)
assert.Equal(t, expected, values)
}
// TestChainOptionK_WithPipe tests ChainOptionK in a pipeline
func TestChainOptionK_WithPipe(t *testing.T) {
// Validate and transform in a pipeline
validateRange := ChainOptionK(func(x int) O.Option[int] {
if x >= 0 && x <= 100 {
return O.Some(x)
}
return O.None[int]()
})
result := F.Pipe2(
From(-10, 20, 150, 50, 200, 75),
validateRange,
Map(func(x int) int { return x * 2 }),
)
values := slices.Collect(result)
expected := A.From(40, 100, 150)
assert.Equal(t, expected, values)
}
// TestChainOptionK_Composition tests composing multiple ChainOptionK operations
func TestChainOptionK_Composition(t *testing.T) {
// First filter: only positive
onlyPositive := ChainOptionK(func(x int) O.Option[int] {
if x > 0 {
return O.Some(x)
}
return O.None[int]()
})
// Second filter: only even
onlyEven := ChainOptionK(func(x int) O.Option[int] {
if x%2 == 0 {
return O.Some(x)
}
return O.None[int]()
})
result := F.Pipe2(
From(-2, -1, 0, 1, 2, 3, 4, 5, 6),
onlyPositive,
onlyEven,
)
values := slices.Collect(result)
expected := A.From(2, 4, 6)
assert.Equal(t, expected, values)
}
// TestChainOptionK_StringParsing tests parsing with ChainOptionK
func TestChainOptionK_StringParsing(t *testing.T) {
// Create a reusable string parser
parseInt := ChainOptionK(func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
})
result := F.Pipe1(
From("10", "abc", "20", "xyz", "30"),
parseInt,
)
values := slices.Collect(result)
expected := A.From(10, 20, 30)
assert.Equal(t, expected, values)
}
// TestFlatMapOptionK_Equivalence tests that FlatMapOptionK is equivalent to ChainOptionK
func TestFlatMapOptionK_Equivalence(t *testing.T) {
validate := func(x int) O.Option[int] {
if x >= 0 && x <= 10 {
return O.Some(x)
}
return O.None[int]()
}
seq := From(-5, 0, 5, 10, 15)
// Using ChainOptionK
result1 := ChainOptionK(validate)(seq)
values1 := slices.Collect(result1)
// Using FlatMapOptionK
result2 := FlatMapOptionK(validate)(seq)
values2 := slices.Collect(result2)
// Both should produce the same result
assert.Equal(t, values1, values2)
assert.Equal(t, A.From(0, 5, 10), values1)
}
// TestFlatMapOptionK_WithMap tests FlatMapOptionK combined with Map
func TestFlatMapOptionK_WithMap(t *testing.T) {
// Validate age and convert to category
validateAge := FlatMapOptionK(func(age int) O.Option[string] {
if age >= 18 && age <= 120 {
return O.Some(fmt.Sprintf("Valid age: %d", age))
}
return O.None[string]()
})
result := F.Pipe1(
From(15, 25, 150, 30, 200),
validateAge,
)
values := slices.Collect(result)
expected := A.From("Valid age: 25", "Valid age: 30")
assert.Equal(t, expected, values)
}
// TestChainOptionK_LookupOperation tests using ChainOptionK for lookup operations
func TestChainOptionK_LookupOperation(t *testing.T) {
// Simulate a lookup table
lookup := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
lookupValue := ChainOptionK(func(key string) O.Option[int] {
if val, ok := lookup[key]; ok {
return O.Some(val)
}
return O.None[int]()
})
result := F.Pipe1(
From("one", "invalid", "two", "missing", "three"),
lookupValue,
)
values := slices.Collect(result)
expected := A.From(1, 2, 3)
assert.Equal(t, expected, values)
}
// TestMonadChainOptionK_EarlyTermination tests that iteration stops when yield returns false
func TestMonadChainOptionK_EarlyTermination(t *testing.T) {
callCount := 0
countCalls := func(x int) O.Option[int] {
callCount++
return O.Some(x)
}
seq := From(1, 2, 3, 4, 5)
result := MonadChainOptionK(seq, countCalls)
// Collect only first 3 elements
collected := make([]int, 0)
for v := range result {
collected = append(collected, v)
if len(collected) >= 3 {
break
}
}
// Should have called the function only 3 times due to early termination
assert.Equal(t, 3, callCount)
assert.Equal(t, A.From(1, 2, 3), collected)
}
// TestChainOptionK_WithReduce tests ChainOptionK with reduction
func TestChainOptionK_WithReduce(t *testing.T) {
// Parse and sum valid numbers
parseInt := ChainOptionK(func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
})
result := F.Pipe1(
From("10", "invalid", "20", "bad", "30"),
parseInt,
)
sum := MonadReduce(result, func(acc, x int) int {
return acc + x
}, 0)
assert.Equal(t, 60, sum)
}
// TestFlatMapOptionK_NestedOptions tests FlatMapOptionK with nested option handling
func TestFlatMapOptionK_NestedOptions(t *testing.T) {
type Result struct {
Value int
Valid bool
}
// Extract value only if valid
extractValid := FlatMapOptionK(func(r Result) O.Option[int] {
if r.Valid {
return O.Some(r.Value)
}
return O.None[int]()
})
seq := From(
Result{10, true},
Result{20, false},
Result{30, true},
Result{40, false},
Result{50, true},
)
result := F.Pipe1(seq, extractValid)
values := slices.Collect(result)
expected := A.From(10, 30, 50)
assert.Equal(t, expected, values)
}

99
v2/llms.txt Normal file
View File

@@ -0,0 +1,99 @@
# fp-go
> A comprehensive functional programming library for Go, bringing type-safe monads, functors, applicatives, optics, and composable abstractions inspired by fp-ts and Haskell to the Go ecosystem. Created by IBM, licensed under Apache-2.0.
fp-go v2 requires Go 1.24+ and leverages generic type aliases for a cleaner API.
Key concepts: `Option` for nullable values, `Either`/`Result` for error handling, `IO` for lazy side effects, `Reader` for dependency injection, `IOResult` for effectful error handling, `ReaderIOResult` for the full monad stack, and `Optics` (lens, prism, traversal, iso) for immutable data manipulation.
## Core Documentation
- [API Reference (pkg.go.dev)](https://pkg.go.dev/github.com/IBM/fp-go/v2): Complete API documentation for all packages
- [README](https://github.com/IBM/fp-go/blob/main/v2/README.md): Overview, quick start, installation, and migration guide from v1 to v2
- [Design Decisions](https://github.com/IBM/fp-go/blob/main/v2/DESIGN.md): Key design principles and patterns
- [Functional I/O Guide](https://github.com/IBM/fp-go/blob/main/v2/FUNCTIONAL_IO.md): Understanding Context, errors, and the Reader pattern for I/O operations
- [Idiomatic vs Standard Comparison](https://github.com/IBM/fp-go/blob/main/v2/IDIOMATIC_COMPARISON.md): Performance comparison and when to use each approach
- [Optics README](https://github.com/IBM/fp-go/blob/main/v2/optics/README.md): Guide to lens, prism, optional, and traversal optics
## Standard Packages (struct-based)
- [option](https://pkg.go.dev/github.com/IBM/fp-go/v2/option): Option monad — represent optional values without nil
- [either](https://pkg.go.dev/github.com/IBM/fp-go/v2/either): Either monad — type-safe error handling with Left/Right values
- [result](https://pkg.go.dev/github.com/IBM/fp-go/v2/result): Result monad — simplified Either with `error` as Left type (recommended for error handling)
- [io](https://pkg.go.dev/github.com/IBM/fp-go/v2/io): IO monad — lazy evaluation and side effect management
- [iooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/iooption): IOOption — IO combined with Option
- [ioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioeither): IOEither — IO combined with Either for effectful error handling
- [ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/ioresult): IOResult — IO combined with Result (recommended over IOEither)
- [reader](https://pkg.go.dev/github.com/IBM/fp-go/v2/reader): Reader monad — dependency injection pattern
- [readeroption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeroption): ReaderOption — Reader combined with Option
- [readeriooption](https://pkg.go.dev/github.com/IBM/fp-go/v2/readeriooption): ReaderIOOption — Reader + IO + Option
- [readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioresult): ReaderIOResult — Reader + IO + Result for complex workflows
- [readerioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/readerioeither): ReaderIOEither — Reader + IO + Either
- [statereaderioeither](https://pkg.go.dev/github.com/IBM/fp-go/v2/statereaderioeither): StateReaderIOEither — State + Reader + IO + Either
## Idiomatic Packages (tuple-based, high performance)
- [idiomatic/option](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/option): Option using native Go `(value, bool)` tuples
- [idiomatic/result](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/result): Result using native Go `(value, error)` tuples
- [idiomatic/ioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/ioresult): IOResult using `func() (value, error)`
- [idiomatic/readerresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerresult): ReaderResult with tuple-based results
- [idiomatic/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/idiomatic/readerioresult): ReaderIOResult with tuple-based results
## Context Packages (context.Context specializations)
- [context/readerioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult): ReaderIOResult specialized for context.Context
- [context/readerioresult/http](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http): Functional HTTP client utilities
- [context/readerioresult/http/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/readerioresult/http/builder): Functional HTTP request builder
- [context/statereaderioresult](https://pkg.go.dev/github.com/IBM/fp-go/v2/context/statereaderioresult): State + Reader + IO + Result for context.Context
## Optics
- [optics](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics): Core optics package
- [optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens): Lenses for focusing on fields in product types
- [optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism): Prisms for focusing on variants in sum types
- [optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso): Isomorphisms for bidirectional transformations
- [optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional): Optionals for values that may not exist
- [optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal): Traversals for focusing on multiple values
- [optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec): Codecs for encoding/decoding with validation
## Utility Packages
- [array](https://pkg.go.dev/github.com/IBM/fp-go/v2/array): Functional array/slice operations (map, filter, fold, etc.)
- [record](https://pkg.go.dev/github.com/IBM/fp-go/v2/record): Functional operations for maps
- [function](https://pkg.go.dev/github.com/IBM/fp-go/v2/function): Function composition, pipe, flow, curry, identity
- [pair](https://pkg.go.dev/github.com/IBM/fp-go/v2/pair): Strongly-typed pair/tuple data structure
- [tuple](https://pkg.go.dev/github.com/IBM/fp-go/v2/tuple): Type-safe heterogeneous tuples
- [predicate](https://pkg.go.dev/github.com/IBM/fp-go/v2/predicate): Predicate combinators (and, or, not, etc.)
- [endomorphism](https://pkg.go.dev/github.com/IBM/fp-go/v2/endomorphism): Endomorphism operations (compose, chain)
- [eq](https://pkg.go.dev/github.com/IBM/fp-go/v2/eq): Type-safe equality comparisons
- [ord](https://pkg.go.dev/github.com/IBM/fp-go/v2/ord): Total ordering type class
- [semigroup](https://pkg.go.dev/github.com/IBM/fp-go/v2/semigroup): Semigroup algebraic structure
- [monoid](https://pkg.go.dev/github.com/IBM/fp-go/v2/monoid): Monoid algebraic structure
- [number](https://pkg.go.dev/github.com/IBM/fp-go/v2/number): Algebraic structures for numeric types
- [string](https://pkg.go.dev/github.com/IBM/fp-go/v2/string): Functional string utilities
- [boolean](https://pkg.go.dev/github.com/IBM/fp-go/v2/boolean): Functional boolean utilities
- [bytes](https://pkg.go.dev/github.com/IBM/fp-go/v2/bytes): Functional byte slice utilities
- [json](https://pkg.go.dev/github.com/IBM/fp-go/v2/json): Functional JSON encoding/decoding
- [lazy](https://pkg.go.dev/github.com/IBM/fp-go/v2/lazy): Lazy evaluation without side effects
- [identity](https://pkg.go.dev/github.com/IBM/fp-go/v2/identity): Identity monad
- [retry](https://pkg.go.dev/github.com/IBM/fp-go/v2/retry): Retry policies with configurable backoff
- [tailrec](https://pkg.go.dev/github.com/IBM/fp-go/v2/tailrec): Trampoline for tail-call optimization
- [di](https://pkg.go.dev/github.com/IBM/fp-go/v2/di): Dependency injection utilities
- [effect](https://pkg.go.dev/github.com/IBM/fp-go/v2/effect): Functional effect system
- [circuitbreaker](https://pkg.go.dev/github.com/IBM/fp-go/v2/circuitbreaker): Circuit breaker error types
- [builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/builder): Generic builder pattern with validation
## Code Samples
- [samples/builder](https://github.com/IBM/fp-go/tree/main/v2/samples/builder): Functional builder pattern example
- [samples/http](https://github.com/IBM/fp-go/tree/main/v2/samples/http): HTTP client examples
- [samples/lens](https://github.com/IBM/fp-go/tree/main/v2/samples/lens): Optics/lens examples
- [samples/mostly-adequate](https://github.com/IBM/fp-go/tree/main/v2/samples/mostly-adequate): Examples adapted from "Mostly Adequate Guide to Functional Programming"
- [samples/tuples](https://github.com/IBM/fp-go/tree/main/v2/samples/tuples): Tuple usage examples
## Optional
- [Source Code](https://github.com/IBM/fp-go): GitHub repository
- [Issues](https://github.com/IBM/fp-go/issues): Bug reports and feature requests
- [Go Report Card](https://goreportcard.com/report/github.com/IBM/fp-go/v2): Code quality report
- [Coverage](https://coveralls.io/github/IBM/fp-go?branch=main): Test coverage report

View File

@@ -13,6 +13,50 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package monoid provides an implementation of the Monoid algebraic structure.
//
// # Monoid
//
// A Monoid is an algebraic structure that extends [Semigroup] by adding an identity element.
// It consists of:
// - A type A
// - An associative binary operation Concat: (A, A) → A
// - An identity element Empty: () → A
//
// # Laws
//
// A Monoid must satisfy the following laws:
//
// 1. Associativity (from Semigroup):
// Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// 2. Left Identity:
// Concat(Empty(), x) = x
//
// 3. Right Identity:
// Concat(x, Empty()) = x
//
// # Common Examples
//
// - Integer addition: Concat = (+), Empty = 0
// - Integer multiplication: Concat = (*), Empty = 1
// - String concatenation: Concat = (++), Empty = ""
// - List concatenation: Concat = (++), Empty = []
// - Boolean AND: Concat = (&&), Empty = true
// - Boolean OR: Concat = (||), Empty = false
// - Function composition: Concat = (∘), Empty = id
//
// # References
//
// - Haskell Data.Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
// - Fantasy Land Monoid: https://github.com/fantasyland/fantasy-land#monoid
// - Semigroup: https://github.com/IBM/fp-go/v2/semigroup
//
// # Related Concepts
//
// - [Semigroup]: A Monoid without the identity element requirement
// - Magma: A set with a binary operation (no laws required)
// - Group: A Monoid where every element has an inverse
package monoid
import (
@@ -21,20 +65,31 @@ import (
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
//
// A Monoid extends Semigroup by adding an identity element (Empty) that satisfies:
// A Monoid extends [Semigroup] by adding an identity element (Empty) that satisfies:
// - Left identity: Concat(Empty(), x) = x
// - Right identity: Concat(x, Empty()) = x
//
// The Monoid must also satisfy the associativity law from Semigroup:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Common examples:
// # Methods
//
// - Concat(x, y A) A: Inherited from Semigroup, combines two values associatively
// - Empty() A: Returns the identity element for the monoid
//
// # Common Examples
//
// - Integer addition with 0 as identity
// - Integer multiplication with 1 as identity
// - String concatenation with "" as identity
// - List concatenation with [] as identity
// - Boolean AND with true as identity
// - Boolean OR with false as identity
//
// # References
//
// - Haskell Monoid typeclass: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
// - Fantasy Land Monoid specification: https://github.com/fantasyland/fantasy-land#monoid
type Monoid[A any] interface {
S.Semigroup[A]
Empty() A
@@ -58,16 +113,22 @@ func (m monoid[A]) Empty() A {
// The provided concat function must be associative, and the empty element must
// satisfy the identity laws (left and right identity).
//
// Parameters:
// - c: An associative binary operation func(A, A) A
// - e: The identity element of type A
// This is the primary constructor for creating custom monoid instances. It's the
// equivalent of defining a Monoid instance in Haskell or implementing the Fantasy Land
// Monoid specification.
//
// Returns:
// - A Monoid[A] instance
// # Parameters
//
// Example:
// - c: An associative binary operation func(A, A) A (equivalent to Haskell's mappend or <>)
// - e: The identity element of type A (equivalent to Haskell's mempty)
//
// // Integer addition monoid
// # Returns
//
// - A [Monoid][A] instance
//
// # Example
//
// // Integer addition monoid (Sum in Haskell)
// addMonoid := MakeMonoid(
// func(a, b int) int { return a + b },
// 0, // identity element
@@ -81,6 +142,11 @@ func (m monoid[A]) Empty() A {
// "", // identity element
// )
// result := stringMonoid.Concat("Hello", " World") // "Hello World"
//
// # References
//
// - Haskell Monoid instance: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Monoid
// - Fantasy Land Monoid.empty: https://github.com/fantasyland/fantasy-land#monoid
func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
return monoid[A]{c: c, e: e}
}
@@ -91,13 +157,18 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
// operation in the opposite order. This is useful for operations that are
// not commutative.
//
// Parameters:
// This corresponds to the Dual newtype wrapper in Haskell's Data.Monoid, which
// provides a Monoid instance with reversed operation order.
//
// # Parameters
//
// - m: The monoid to reverse
//
// Returns:
// - A new Monoid[A] with reversed operation order
// # Returns
//
// Example:
// - A new [Monoid][A] with reversed operation order
//
// # Example
//
// // Subtraction monoid (not commutative)
// subMonoid := MakeMonoid(
@@ -116,6 +187,10 @@ func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] {
// )
// reversed := Reverse(stringMonoid)
// result := reversed.Concat("Hello", "World") // "WorldHello"
//
// # References
//
// - Haskell Data.Monoid.Dual: https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:Dual
func Reverse[A any](m Monoid[A]) Monoid[A] {
return MakeMonoid(S.Reverse(m).Concat, m.Empty())
}
@@ -125,13 +200,19 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
// This is useful when you need to use a monoid in a context that only requires
// a semigroup (associative binary operation without identity).
//
// Parameters:
// Since every Monoid is also a Semigroup (Monoid extends Semigroup), this conversion
// is always safe. This reflects the mathematical relationship where monoids form a
// subset of semigroups.
//
// # Parameters
//
// - m: The monoid to convert
//
// Returns:
// - A Semigroup[A] that uses the same Concat operation
// # Returns
//
// Example:
// - A [Semigroup][A] that uses the same Concat operation
//
// # Example
//
// addMonoid := MakeMonoid(
// func(a, b int) int { return a + b },
@@ -139,6 +220,11 @@ func Reverse[A any](m Monoid[A]) Monoid[A] {
// )
// sg := ToSemigroup(addMonoid)
// result := sg.Concat(5, 3) // 8 (identity not available)
//
// # References
//
// - Haskell Semigroup: https://hackage.haskell.org/package/base/docs/Data-Semigroup.html
// - Fantasy Land Semigroup: https://github.com/fantasyland/fantasy-land#semigroup
func ToSemigroup[A any](m Monoid[A]) S.Semigroup[A] {
return S.Semigroup[A](m)
}

480
v2/optics/codec/alt.go Normal file
View File

@@ -0,0 +1,480 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codec
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/reader"
)
// validateAlt creates a validation function that tries the first codec's validation,
// and if it fails, tries the second codec's validation as a fallback.
//
// This is an internal helper function that implements the Alternative pattern for
// codec validation. It combines two codec validators using the validate.Alt operation,
// which provides error recovery and fallback logic.
//
// # Type Parameters
//
// - A: The target type that both codecs decode to
// - O: The output type that both codecs encode to
// - I: The input type that both codecs decode from
//
// # Parameters
//
// - first: The primary codec whose validation is tried first
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
// first validation fails.
//
// # Returns
//
// A Validate[I, A] function that tries the first codec's validation, falling back
// to the second if needed. If both fail, errors from both are aggregated.
//
// # Behavior
//
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
//
// # Notes
//
// - The second codec is lazily evaluated for efficiency
// - This function is used internally by MonadAlt and Alt
// - The validation context is threaded through both validators
// - Errors are accumulated using the validation error monoid
func validateAlt[A, O, I any](
first Type[A, O, I],
second Lazy[Type[A, O, I]],
) Validate[I, A] {
return F.Pipe1(
first.Validate,
validate.Alt(F.Pipe1(
second,
lazy.Map(F.Flip(reader.Curry(Type[A, O, I].Validate))),
)),
)
}
// MonadAlt creates a new codec that tries the first codec, and if it fails during
// validation, tries the second codec as a fallback.
//
// This function implements the Alternative typeclass pattern for codecs, enabling
// "try this codec, or else try that codec" logic. It's particularly useful for:
// - Handling multiple valid input formats
// - Providing backward compatibility with legacy formats
// - Implementing graceful degradation in parsing
// - Supporting union types or polymorphic data
//
// The resulting codec uses the first codec's encoder and combines both validators
// using the Alternative pattern. If both validations fail, errors from both are
// aggregated for comprehensive error reporting.
//
// # Type Parameters
//
// - A: The target type that both codecs decode to
// - O: The output type that both codecs encode to
// - I: The input type that both codecs decode from
//
// # Parameters
//
// - first: The primary codec to try first. Its encoder is used for the result.
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
// first validation fails.
//
// # Returns
//
// A new Type[A, O, I] that combines both codecs with Alternative semantics.
//
// # Behavior
//
// **Validation**:
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
//
// **Encoding**:
// - Always uses the first codec's encoder
// - This assumes both codecs encode to the same output format
//
// **Type Checking**:
// - Uses the generic Is[A]() type checker
// - Validates that values are of type A
//
// # Example: Multiple Input Formats
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// )
//
// // Accept integers as either strings or numbers
// intFromString := codec.IntFromString()
// intFromNumber := codec.Int()
//
// // Try parsing as string first, fall back to number
// flexibleInt := codec.MonadAlt(
// intFromString,
// func() codec.Type[int, any, any] { return intFromNumber },
// )
//
// // Can now decode both "42" and 42
// result1 := flexibleInt.Decode("42") // Success(42)
// result2 := flexibleInt.Decode(42) // Success(42)
//
// # Example: Backward Compatibility
//
// // Support both old and new configuration formats
// newConfigCodec := codec.Struct(/* new format */)
// oldConfigCodec := codec.Struct(/* old format */)
//
// // Try new format first, fall back to old format
// configCodec := codec.MonadAlt(
// newConfigCodec,
// func() codec.Type[Config, any, any] { return oldConfigCodec },
// )
//
// // Automatically handles both formats
// config := configCodec.Decode(input)
//
// # Example: Error Aggregation
//
// // Both validations will fail for invalid input
// result := flexibleInt.Decode("not a number")
// // Result contains errors from both string and number parsing attempts
//
// # Notes
//
// - The second codec is lazily evaluated for efficiency
// - First success short-circuits evaluation (second not called)
// - Errors are aggregated when both fail
// - The resulting codec's name is "Alt[<first codec name>]"
// - Both codecs must have compatible input and output types
// - The first codec's encoder is always used
//
// # See Also
//
// - Alt: The curried, point-free version
// - validate.MonadAlt: The underlying validation operation
// - Either: For codecs that decode to Either[L, R] types
func MonadAlt[A, O, I any](first Type[A, O, I], second Lazy[Type[A, O, I]]) Type[A, O, I] {
return MakeType(
fmt.Sprintf("Alt[%s]", first.Name()),
Is[A](),
validateAlt(first, second),
first.Encode,
)
}
// Alt creates an operator that adds alternative fallback logic to a codec.
//
// This is the curried, point-free version of MonadAlt. It returns a function that
// can be applied to codecs to add fallback behavior. This style is particularly
// useful for building codec transformation pipelines using function composition.
//
// Alt implements the Alternative typeclass pattern, enabling "try this codec, or
// else try that codec" logic in a composable way.
//
// # Type Parameters
//
// - A: The target type that both codecs decode to
// - O: The output type that both codecs encode to
// - I: The input type that both codecs decode from
//
// # Parameters
//
// - second: A lazy codec that serves as the fallback. It's only evaluated if the
// first codec's validation fails.
//
// # Returns
//
// An Operator[A, A, O, I] that transforms codecs by adding alternative fallback logic.
// This operator can be applied to any Type[A, O, I] to create a new codec with
// fallback behavior.
//
// # Behavior
//
// When the returned operator is applied to a codec:
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
//
// # Example: Point-Free Style
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/optics/codec"
// )
//
// // Create a reusable fallback operator
// withNumberFallback := codec.Alt(func() codec.Type[int, any, any] {
// return codec.Int()
// })
//
// // Apply it to different codecs
// flexibleInt1 := withNumberFallback(codec.IntFromString())
// flexibleInt2 := withNumberFallback(codec.IntFromHex())
//
// # Example: Pipeline Composition
//
// // Build a codec pipeline with multiple fallbacks
// flexibleCodec := F.Pipe2(
// primaryCodec,
// codec.Alt(func() codec.Type[T, O, I] { return fallback1 }),
// codec.Alt(func() codec.Type[T, O, I] { return fallback2 }),
// )
// // Tries primary, then fallback1, then fallback2
//
// # Example: Reusable Transformations
//
// // Create a transformation that adds JSON fallback
// withJSONFallback := codec.Alt(func() codec.Type[Config, string, string] {
// return codec.JSONCodec[Config]()
// })
//
// // Apply to multiple codecs
// yamlWithFallback := withJSONFallback(yamlCodec)
// tomlWithFallback := withJSONFallback(tomlCodec)
//
// # Notes
//
// - This is the point-free style version of MonadAlt
// - Useful for building transformation pipelines with F.Pipe
// - The second codec is lazily evaluated for efficiency
// - First success short-circuits evaluation
// - Errors are aggregated when both fail
// - Can be composed with other codec operators
//
// # See Also
//
// - MonadAlt: The direct application version
// - validate.Alt: The underlying validation operation
// - F.Pipe: For composing multiple operators
func Alt[A, O, I any](second Lazy[Type[A, O, I]]) Operator[A, A, O, I] {
return F.Bind2nd(MonadAlt, second)
}
// AltMonoid creates a Monoid instance for Type[A, O, I] using alternative semantics
// with a provided zero/default codec.
//
// This function creates a monoid where:
// 1. The first successful codec wins (no result combination)
// 2. If the first fails during validation, the second is tried as a fallback
// 3. If both fail, errors are aggregated
// 4. The provided zero codec serves as the identity element
//
// Unlike other monoid patterns, AltMonoid does NOT combine successful results - it always
// returns the first success. This makes it ideal for building fallback chains with default
// codecs, configuration loading from multiple sources, and parser combinators with alternatives.
//
// # Type Parameters
//
// - A: The target type that all codecs decode to
// - O: The output type that all codecs encode to
// - I: The input type that all codecs decode from
//
// # Parameters
//
// - zero: A lazy Type[A, O, I] that serves as the identity element. This is typically
// a codec that always succeeds with a default value, but can also be a failing
// codec if no default is appropriate.
//
// # Returns
//
// A Monoid[Type[A, O, I]] that combines codecs using alternative semantics where
// the first success wins.
//
// # Behavior Details
//
// The AltMonoid implements a "first success wins" strategy:
//
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
// - **Concat with Empty**: The zero codec is used as fallback
// - **Encoding**: Always uses the first codec's encoder
//
// # Example: Configuration Loading with Fallbacks
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/array"
// )
//
// // Create a monoid with a default configuration
// m := codec.AltMonoid(func() codec.Type[Config, string, string] {
// return codec.MakeType(
// "DefaultConfig",
// codec.Is[Config](),
// func(s string) codec.Decode[codec.Context, Config] {
// return func(c codec.Context) codec.Validation[Config] {
// return validation.Success(defaultConfig)
// }
// },
// encodeConfig,
// )
// })
//
// // Define codecs for different sources
// fileCodec := loadFromFile("config.json")
// envCodec := loadFromEnv()
// defaultCodec := m.Empty()
//
// // Try file, then env, then default
// configCodec := array.MonadFold(
// []codec.Type[Config, string, string]{fileCodec, envCodec, defaultCodec},
// m.Empty(),
// m.Concat,
// )
//
// // Load configuration - tries each source in order
// result := configCodec.Decode(input)
//
// # Example: Parser with Multiple Formats
//
// // Create a monoid for parsing dates in multiple formats
// m := codec.AltMonoid(func() codec.Type[time.Time, string, string] {
// return codec.Date(time.RFC3339) // default format
// })
//
// // Define parsers for different date formats
// iso8601 := codec.Date("2006-01-02")
// usFormat := codec.Date("01/02/2006")
// euroFormat := codec.Date("02/01/2006")
//
// // Combine: try ISO 8601, then US, then European, then RFC3339
// flexibleDate := m.Concat(
// m.Concat(
// m.Concat(iso8601, usFormat),
// euroFormat,
// ),
// m.Empty(),
// )
//
// // Can parse any of these formats
// result1 := flexibleDate.Decode("2024-03-15") // ISO 8601
// result2 := flexibleDate.Decode("03/15/2024") // US format
// result3 := flexibleDate.Decode("15/03/2024") // European format
//
// # Example: Integer Parsing with Default
//
// // Create a monoid with default value of 0
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
// return codec.MakeType(
// "DefaultZero",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.Success(0)
// }
// },
// strconv.Itoa,
// )
// })
//
// // Try parsing as int, fall back to 0
// intOrZero := m.Concat(codec.IntFromString(), m.Empty())
//
// result1 := intOrZero.Decode("42") // Success(42)
// result2 := intOrZero.Decode("invalid") // Success(0) - uses default
//
// # Example: Error Aggregation
//
// // Both codecs fail - errors are aggregated
// m := codec.AltMonoid(func() codec.Type[int, string, string] {
// return codec.MakeType(
// "NoDefault",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "no default available")(c)
// }
// },
// strconv.Itoa,
// )
// })
//
// failing1 := codec.MakeType(
// "Failing1",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "error 1")(c)
// }
// },
// strconv.Itoa,
// )
//
// failing2 := codec.MakeType(
// "Failing2",
// codec.Is[int](),
// func(s string) codec.Decode[codec.Context, int] {
// return func(c codec.Context) codec.Validation[int] {
// return validation.FailureWithMessage[int](s, "error 2")(c)
// }
// },
// strconv.Itoa,
// )
//
// combined := m.Concat(failing1, failing2)
// result := combined.Decode("input")
// // result contains errors: "error 1", "error 2", and "no default available"
//
// # Monoid Laws
//
// AltMonoid satisfies the monoid laws:
//
// 1. **Left Identity**: m.Concat(m.Empty(), codec) ≡ codec
// 2. **Right Identity**: m.Concat(codec, m.Empty()) ≡ codec (tries codec first, falls back to zero)
// 3. **Associativity**: m.Concat(m.Concat(a, b), c) ≡ m.Concat(a, m.Concat(b, c))
//
// Note: Due to the "first success wins" behavior, right identity means the zero is only
// used if the codec fails.
//
// # Use Cases
//
// - Configuration loading with multiple sources (file, env, default)
// - Parsing data in multiple formats with fallbacks
// - API versioning (try v2, fall back to v1, then default)
// - Content negotiation (try JSON, then XML, then plain text)
// - Validation with default values
// - Parser combinators with alternative branches
//
// # Notes
//
// - The zero codec is lazily evaluated, only when needed
// - First success short-circuits evaluation (subsequent codecs not tried)
// - Error aggregation ensures all validation failures are reported
// - Encoding always uses the first codec's encoder
// - This follows the alternative functor laws
//
// # See Also
//
// - MonadAlt: The underlying alternative operation for two codecs
// - Alt: The curried version for pipeline composition
// - validate.AltMonoid: The validation-level alternative monoid
// - decode.AltMonoid: The decode-level alternative monoid
func AltMonoid[A, O, I any](zero Lazy[Type[A, O, I]]) Monoid[Type[A, O, I]] {
return monoid.AltMonoid(
zero,
MonadAlt[A, O, I],
)
}

921
v2/optics/codec/alt_test.go Normal file
View File

@@ -0,0 +1,921 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codec
import (
"fmt"
"strconv"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMonadAltBasicFunctionality tests the basic behavior of MonadAlt
func TestMonadAltBasicFunctionality(t *testing.T) {
t.Run("uses first codec when it succeeds", func(t *testing.T) {
// Create two codecs that both work with strings
stringCodec := Id[string]()
// Create another string codec that only accepts uppercase
uppercaseOnly := MakeType(
"UppercaseOnly",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
for _, r := range s {
if r >= 'a' && r <= 'z' {
return validation.FailureWithMessage[string](s, "must be uppercase")(c)
}
}
return validation.Success(s)
}
},
F.Identity[string],
)
// Create alt codec that tries uppercase first, then any string
altCodec := MonadAlt(
uppercaseOnly,
func() Type[string, string, string] { return stringCodec },
)
// Test with uppercase string - should succeed with first codec
result := altCodec.Decode("HELLO")
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "HELLO", value)
})
t.Run("falls back to second codec when first fails", func(t *testing.T) {
// Create a codec that only accepts positive integers
positiveInt := MakeType(
"PositiveInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i <= 0 {
return validation.FailureWithMessage[int](i, "must be positive")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
// Create a codec that accepts any integer (with same input type)
anyInt := MakeType(
"AnyInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(i)
}
},
F.Identity[int],
)
// Create alt codec
altCodec := MonadAlt(
positiveInt,
func() Type[int, int, int] { return anyInt },
)
// Test with negative number - first fails, second succeeds
result := altCodec.Decode(-5)
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, -5, value)
})
t.Run("aggregates errors when both codecs fail", func(t *testing.T) {
// Create two codecs that will both fail
positiveInt := MakeType(
"PositiveInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i <= 0 {
return validation.FailureWithMessage[int](i, "must be positive")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
evenInt := MakeType(
"EvenInt",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i%2 != 0 {
return validation.FailureWithMessage[int](i, "must be even")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
// Create alt codec
altCodec := MonadAlt(
positiveInt,
func() Type[int, int, int] { return evenInt },
)
// Test with -3 (negative and odd) - both should fail
result := altCodec.Decode(-3)
assert.True(t, either.IsLeft(result), "should fail when both codecs fail")
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
// Should have errors from both validation attempts
assert.GreaterOrEqual(t, len(errors), 2, "should have errors from both codecs")
})
}
// TestMonadAltNaming tests that the codec name is correctly generated
func TestMonadAltNaming(t *testing.T) {
t.Run("generates correct name", func(t *testing.T) {
stringCodec := Id[string]()
anotherStringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] { return anotherStringCodec },
)
assert.Equal(t, "Alt[string]", altCodec.Name())
})
}
// TestMonadAltEncoding tests that encoding uses the first codec's encoder
func TestMonadAltEncoding(t *testing.T) {
t.Run("uses first codec's encoder", func(t *testing.T) {
// Create a codec that encodes ints as strings with prefix
prefixedInt := MakeType(
"PrefixedInt",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
var n int
_, err := fmt.Sscanf(s, "NUM:%d", &n)
if err != nil {
return validation.FailureWithError[int](s, "expected NUM:n format")(err)(c)
}
return validation.Success(n)
}
},
func(n int) string {
return fmt.Sprintf("NUM:%d", n)
},
)
// Create a standard int from string codec
standardInt := IntFromString()
// Create alt codec
altCodec := MonadAlt(
prefixedInt,
func() Type[int, string, string] { return standardInt },
)
// Encode should use first codec's encoder
encoded := altCodec.Encode(42)
assert.Equal(t, "NUM:42", encoded)
})
}
// TestAltOperator tests the curried Alt function
func TestAltOperator(t *testing.T) {
t.Run("creates reusable operator", func(t *testing.T) {
// Create a fallback operator that accepts any string
withStringFallback := Alt(func() Type[string, string, string] {
return Id[string]()
})
// Create a codec that only accepts "hello"
helloOnly := MakeType(
"HelloOnly",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s != "hello" {
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
// Apply fallback to the codec
altCodec := withStringFallback(helloOnly)
// Test that it works
result1 := altCodec.Decode("hello")
assert.True(t, either.IsRight(result1))
result2 := altCodec.Decode("world")
assert.True(t, either.IsRight(result2))
})
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
// Create a codec pipeline with multiple fallbacks
baseCodec := MakeType(
"StrictInt",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
if s != "42" {
return validation.FailureWithMessage[int](s, "must be exactly '42'")(c)
}
return validation.Success(42)
}
},
strconv.Itoa,
)
fallback1 := MakeType(
"Fallback1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
if s != "100" {
return validation.FailureWithMessage[int](s, "must be exactly '100'")(c)
}
return validation.Success(100)
}
},
strconv.Itoa,
)
fallback2 := MakeType(
"AnyInt",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
n, err := strconv.Atoi(s)
if err != nil {
return validation.FailureWithError[int](s, "not an integer")(err)(c)
}
return validation.Success(n)
}
},
strconv.Itoa,
)
// Build pipeline with multiple alternatives
pipeline := F.Pipe2(
baseCodec,
Alt(func() Type[int, string, string] { return fallback1 }),
Alt(func() Type[int, string, string] { return fallback2 }),
)
// Test with "42" - should use base codec
result1 := pipeline.Decode("42")
assert.True(t, either.IsRight(result1))
value1 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result1)
assert.Equal(t, 42, value1)
// Test with "100" - should use fallback1
result2 := pipeline.Decode("100")
assert.True(t, either.IsRight(result2))
value2 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result2)
assert.Equal(t, 100, value2)
// Test with "999" - should use fallback2
result3 := pipeline.Decode("999")
assert.True(t, either.IsRight(result3))
value3 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result3)
assert.Equal(t, 999, value3)
})
}
// TestAltLazyEvaluation tests that the second codec is only evaluated when needed
func TestAltLazyEvaluation(t *testing.T) {
t.Run("does not evaluate second codec when first succeeds", func(t *testing.T) {
evaluated := false
stringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] {
evaluated = true
return Id[string]()
},
)
// Decode with first codec succeeding
result := altCodec.Decode("hello")
assert.True(t, either.IsRight(result))
// Second codec should not have been evaluated
assert.False(t, evaluated, "second codec should not be evaluated when first succeeds")
})
t.Run("evaluates second codec when first fails", func(t *testing.T) {
evaluated := false
// Create a codec that always fails
failingCodec := MakeType(
"Failing",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.FailureWithMessage[string](s, "always fails")(c)
}
},
F.Identity[string],
)
altCodec := MonadAlt(
failingCodec,
func() Type[string, string, string] {
evaluated = true
return Id[string]()
},
)
// Decode with first codec failing
result := altCodec.Decode("hello")
assert.True(t, either.IsRight(result))
// Second codec should have been evaluated
assert.True(t, evaluated, "second codec should be evaluated when first fails")
})
}
// TestAltWithComplexTypes tests Alt with more complex codec scenarios
func TestAltWithComplexTypes(t *testing.T) {
t.Run("works with string length validation", func(t *testing.T) {
// Create codec that accepts strings of length 5
length5 := MakeType(
"Length5",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if len(s) != 5 {
return validation.FailureWithMessage[string](s, "must be length 5")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
// Create codec that accepts any string
anyString := Id[string]()
// Create alt codec
altCodec := MonadAlt(
length5,
func() Type[string, string, string] { return anyString },
)
// Test with length 5 - should use first codec
result1 := altCodec.Decode("hello")
assert.True(t, either.IsRight(result1))
// Test with different length - should fall back to second codec
result2 := altCodec.Decode("hi")
assert.True(t, either.IsRight(result2))
})
}
// TestAltTypeChecking tests that type checking works correctly
func TestAltTypeChecking(t *testing.T) {
t.Run("type checking uses generic Is", func(t *testing.T) {
stringCodec := Id[string]()
anotherStringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] { return anotherStringCodec },
)
// Test Is with valid type
result1 := altCodec.Is("hello")
assert.True(t, either.IsRight(result1))
// Test Is with invalid type
result2 := altCodec.Is(42)
assert.True(t, either.IsLeft(result2))
})
}
// TestAltRoundTrip tests encoding and decoding round trips
func TestAltRoundTrip(t *testing.T) {
t.Run("round-trip with first codec", func(t *testing.T) {
stringCodec := Id[string]()
anotherStringCodec := Id[string]()
altCodec := MonadAlt(
stringCodec,
func() Type[string, string, string] { return anotherStringCodec },
)
original := "hello"
// Decode
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
// Encode
encoded := altCodec.Encode(decoded)
// Verify
assert.Equal(t, original, encoded)
})
t.Run("round-trip with second codec", func(t *testing.T) {
// Create a codec that only accepts "hello"
helloOnly := MakeType(
"HelloOnly",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s != "hello" {
return validation.FailureWithMessage[string](s, "must be 'hello'")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
anyString := Id[string]()
altCodec := MonadAlt(
helloOnly,
func() Type[string, string, string] { return anyString },
)
original := "world"
// Decode (will use second codec)
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
// Encode (uses first codec's encoder, which is identity)
encoded := altCodec.Encode(decoded)
// Verify
assert.Equal(t, original, encoded)
})
}
// TestAltErrorMessages tests that error messages are informative
func TestAltErrorMessages(t *testing.T) {
t.Run("provides detailed error context", func(t *testing.T) {
// Create two codecs with specific error messages
codec1 := MakeType(
"Codec1",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](i, "codec1 error")(c)
}
},
F.Identity[int],
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](i, "codec2 error")(c)
}
},
F.Identity[int],
)
altCodec := MonadAlt(
codec1,
func() Type[int, int, int] { return codec2 },
)
result := altCodec.Decode(42)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
require.GreaterOrEqual(t, len(errors), 2)
// Check that both error messages are present
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
hasCodec1Error := false
hasCodec2Error := false
for _, msg := range messages {
if msg == "codec1 error" {
hasCodec1Error = true
}
if msg == "codec2 error" {
hasCodec2Error = true
}
}
assert.True(t, hasCodec1Error, "should have error from first codec")
assert.True(t, hasCodec2Error, "should have error from second codec")
})
}
// TestAltMonoid tests the AltMonoid function
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
// Create a monoid with a default value of 0
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"DefaultZero",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
strconv.Itoa,
)
})
// Create codecs
intFromString := IntFromString()
failing := MakeType(
"Failing",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "always fails")(c)
}
},
strconv.Itoa,
)
t.Run("first success wins", func(t *testing.T) {
// Combine two successful codecs - first should win
codec1 := MakeType(
"Returns10",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(10)
}
},
strconv.Itoa,
)
codec2 := MakeType(
"Returns20",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(20)
}
},
strconv.Itoa,
)
combined := m.Concat(codec1, codec2)
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, 10, value, "first success should win")
})
t.Run("falls back to second when first fails", func(t *testing.T) {
combined := m.Concat(failing, intFromString)
result := combined.Decode("42")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
assert.Equal(t, 42, value)
})
t.Run("uses zero when both fail", func(t *testing.T) {
combined := m.Concat(failing, m.Empty())
result := combined.Decode("invalid")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
assert.Equal(t, 0, value, "should use default zero value")
})
})
t.Run("with failing zero", func(t *testing.T) {
// Create a monoid with a failing zero
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"NoDefault",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "no default available")(c)
}
},
strconv.Itoa,
)
})
failing1 := MakeType(
"Failing1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "error 1")(c)
}
},
strconv.Itoa,
)
failing2 := MakeType(
"Failing2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.FailureWithMessage[int](s, "error 2")(c)
}
},
strconv.Itoa,
)
t.Run("aggregates all errors when all fail", func(t *testing.T) {
combined := m.Concat(m.Concat(failing1, failing2), m.Empty())
result := combined.Decode("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
// Should have errors from all three: failing1, failing2, and zero
assert.GreaterOrEqual(t, len(errors), 3)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
hasError1 := false
hasError2 := false
hasNoDefault := false
for _, msg := range messages {
if msg == "error 1" {
hasError1 = true
}
if msg == "error 2" {
hasError2 = true
}
if msg == "no default available" {
hasNoDefault = true
}
}
assert.True(t, hasError1, "should have error from failing1")
assert.True(t, hasError2, "should have error from failing2")
assert.True(t, hasNoDefault, "should have error from zero")
})
})
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Type[string, string, string] {
return MakeType(
"Default",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
return validation.Success("default")
}
},
F.Identity[string],
)
})
primary := MakeType(
"Primary",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s == "primary" {
return validation.Success("from primary")
}
return validation.FailureWithMessage[string](s, "not primary")(c)
}
},
F.Identity[string],
)
secondary := MakeType(
"Secondary",
Is[string](),
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if s == "secondary" {
return validation.Success("from secondary")
}
return validation.FailureWithMessage[string](s, "not secondary")(c)
}
},
F.Identity[string],
)
// Chain: try primary, then secondary, then default
combined := m.Concat(m.Concat(primary, secondary), m.Empty())
t.Run("uses primary when it succeeds", func(t *testing.T) {
result := combined.Decode("primary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "from primary", value)
})
t.Run("uses secondary when primary fails", func(t *testing.T) {
result := combined.Decode("secondary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "from secondary", value)
})
t.Run("uses default when both fail", func(t *testing.T) {
result := combined.Decode("other")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
assert.Equal(t, "default", value)
})
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"DefaultZero",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
strconv.Itoa,
)
})
codec1 := MakeType(
"Codec1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(10)
}
},
strconv.Itoa,
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(20)
}
},
strconv.Itoa,
)
codec3 := MakeType(
"Codec3",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(30)
}
},
strconv.Itoa,
)
t.Run("left identity", func(t *testing.T) {
// m.Concat(m.Empty(), codec) should behave like codec
// But with AltMonoid, if codec fails, it falls back to empty
combined := m.Concat(m.Empty(), codec1)
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
// Empty (0) comes first, so it wins
assert.Equal(t, 0, value)
})
t.Run("right identity", func(t *testing.T) {
// m.Concat(codec, m.Empty()) tries codec first, falls back to empty
combined := m.Concat(codec1, m.Empty())
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
assert.Equal(t, 10, value, "codec1 should win")
})
t.Run("associativity", func(t *testing.T) {
// For AltMonoid, first success wins
left := m.Concat(m.Concat(codec1, codec2), codec3)
right := m.Concat(codec1, m.Concat(codec2, codec3))
resultLeft := left.Decode("input")
resultRight := right.Decode("input")
assert.True(t, either.IsRight(resultLeft))
assert.True(t, either.IsRight(resultRight))
valueLeft := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultRight)
// Both should return 10 (first success)
assert.Equal(t, valueLeft, valueRight)
assert.Equal(t, 10, valueLeft)
})
})
t.Run("encoding uses first codec", func(t *testing.T) {
m := AltMonoid(func() Type[int, string, string] {
return MakeType(
"Default",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(0)
}
},
func(n int) string { return "DEFAULT" },
)
})
codec1 := MakeType(
"Codec1",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(42)
}
},
func(n int) string { return fmt.Sprintf("FIRST:%d", n) },
)
codec2 := MakeType(
"Codec2",
Is[int](),
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
return validation.Success(100)
}
},
func(n int) string { return fmt.Sprintf("SECOND:%d", n) },
)
combined := m.Concat(codec1, codec2)
// Encoding should use first codec's encoder
encoded := combined.Encode(42)
assert.Equal(t, "FIRST:42", encoded)
})
}

View File

@@ -710,6 +710,146 @@ func TestTranscodeEither(t *testing.T) {
})
}
func TestTranscodeEitherValidation(t *testing.T) {
t.Run("validates Left value with context", func(t *testing.T) {
eitherCodec := TranscodeEither(String(), Int())
result := eitherCodec.Decode(either.Left[any, any](123)) // Invalid: should be string
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(either.Either[string, int]) validation.Errors { return nil },
)
assert.NotEmpty(t, errors)
// Verify error contains type information
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "string")
})
t.Run("validates Right value with context", func(t *testing.T) {
eitherCodec := TranscodeEither(String(), Int())
result := eitherCodec.Decode(either.Right[any, any]("not a number"))
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(either.Either[string, int]) validation.Errors { return nil },
)
assert.NotEmpty(t, errors)
// Verify error contains type information
assert.Contains(t, fmt.Sprintf("%v", errors[0]), "int")
})
t.Run("preserves Either structure on validation failure", func(t *testing.T) {
eitherCodec := TranscodeEither(String(), Int())
// Left with wrong type
leftResult := eitherCodec.Decode(either.Left[any, any]([]int{1, 2, 3}))
assert.True(t, either.IsLeft(leftResult))
// Right with wrong type
rightResult := eitherCodec.Decode(either.Right[any, any](true))
assert.True(t, either.IsLeft(rightResult))
})
t.Run("validates with custom codec that can fail", func(t *testing.T) {
// Create a codec that only accepts positive integers
positiveInt := MakeType(
"PositiveInt",
func(u any) either.Either[error, int] {
i, ok := u.(int)
if !ok || i <= 0 {
return either.Left[int](fmt.Errorf("not a positive integer"))
}
return either.Of[error](i)
},
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i <= 0 {
return validation.FailureWithMessage[int](i, "must be positive")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
eitherCodec := TranscodeEither(String(), positiveInt)
// Valid positive integer
validResult := eitherCodec.Decode(either.Right[any](42))
assert.True(t, either.IsRight(validResult))
// Invalid: zero
zeroResult := eitherCodec.Decode(either.Right[any](0))
assert.True(t, either.IsLeft(zeroResult))
// Invalid: negative
negativeResult := eitherCodec.Decode(either.Right[any](-5))
assert.True(t, either.IsLeft(negativeResult))
})
t.Run("validates both branches independently", func(t *testing.T) {
// Create codecs with specific validation rules
nonEmptyString := MakeType(
"NonEmptyString",
func(u any) either.Either[error, string] {
s, ok := u.(string)
if !ok || len(s) == 0 {
return either.Left[string](fmt.Errorf("not a non-empty string"))
}
return either.Of[error](s)
},
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if len(s) == 0 {
return validation.FailureWithMessage[string](s, "must not be empty")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
evenInt := MakeType(
"EvenInt",
func(u any) either.Either[error, int] {
i, ok := u.(int)
if !ok || i%2 != 0 {
return either.Left[int](fmt.Errorf("not an even integer"))
}
return either.Of[error](i)
},
func(i int) Decode[Context, int] {
return func(c Context) Validation[int] {
if i%2 != 0 {
return validation.FailureWithMessage[int](i, "must be even")(c)
}
return validation.Success(i)
}
},
F.Identity[int],
)
eitherCodec := TranscodeEither(nonEmptyString, evenInt)
// Valid Left: non-empty string
validLeft := eitherCodec.Decode(either.Left[int]("hello"))
assert.True(t, either.IsRight(validLeft))
// Invalid Left: empty string
invalidLeft := eitherCodec.Decode(either.Left[int](""))
assert.True(t, either.IsLeft(invalidLeft))
// Valid Right: even integer
validRight := eitherCodec.Decode(either.Right[string](42))
assert.True(t, either.IsRight(validRight))
// Invalid Right: odd integer
invalidRight := eitherCodec.Decode(either.Right[string](43))
assert.True(t, either.IsLeft(invalidRight))
})
}
func TestTranscodeEitherWithTransformation(t *testing.T) {
// Create a codec that transforms strings to their lengths
stringToLength := MakeType(

View File

@@ -0,0 +1,321 @@
# ChainLeft and OrElse in the Decode Package
## Overview
In [`optics/codec/decode/monad.go`](monad.go:53-62), the [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) functions work with decoders that may fail during decoding operations.
```go
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return readert.Chain[Decode[I, A]](
validation.ChainLeft,
f,
)
}
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return ChainLeft(f)
}
```
## Key Insight: OrElse is ChainLeft
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**.
## Understanding the Types
### Decode[I, A]
A decoder that takes input of type `I` and produces a `Validation[A]`:
```go
type Decode[I, A any] = func(I) Validation[A]
```
### Kleisli[I, Errors, A]
A function that takes `Errors` and produces a `Decode[I, A]`:
```go
type Kleisli[I, Errors, A] = func(Errors) Decode[I, A]
```
This allows error handlers to:
1. Access the validation errors that occurred
2. Access the original input (via the returned Decode function)
3. Either recover with a success value or produce new errors
### Operator[I, A, A]
A function that transforms one decoder into another:
```go
type Operator[I, A, A] = func(Decode[I, A]) Decode[I, A]
```
## Core Behavior
Both [`ChainLeft`](monad.go:53) and [`OrElse`](monad.go:60) delegate to [`validation.ChainLeft`](../validation/monad.go:304), which provides:
### 1. Error Aggregation
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid:
```go
failingDecoder := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "original error"},
})
}
handler := ChainLeft(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "additional error"},
})
}
})
decoder := handler(failingDecoder)
result := decoder("input")
// Result contains BOTH errors: ["original error", "additional error"]
```
### 2. Success Pass-Through
Success values pass through unchanged - the handler is never called:
```go
successDecoder := Of[string](42)
handler := ChainLeft(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "never called"},
})
}
})
decoder := handler(successDecoder)
result := decoder("input")
// Result: Success(42) - unchanged
```
### 3. Error Recovery
The handler can recover from failures by returning a successful decoder:
```go
failingDecoder := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "not found"},
})
}
recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
for _, err := range errs {
if err.Messsage == "not found" {
return Of[string](0) // recover with default
}
}
return func(input string) Validation[int] {
return either.Left[int](errs)
}
})
decoder := recoverFromNotFound(failingDecoder)
result := decoder("input")
// Result: Success(0) - recovered from failure
```
### 4. Access to Original Input
The handler returns a `Decode[I, A]` function, giving it access to the original input:
```go
handler := ChainLeft(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
// Can access both errs and input here
if input == "special" {
return validation.Of(999)
}
return either.Left[int](errs)
}
})
```
## Use Cases
### 1. Fallback Decoding (OrElse reads better)
```go
// Primary decoder that may fail
primaryDecoder := func(input string) Validation[int] {
n, err := strconv.Atoi(input)
if err != nil {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "not a valid integer"},
})
}
return validation.Of(n)
}
// Use OrElse for semantic clarity - "try primary, or else use default"
withDefault := OrElse(func(errs Errors) Decode[string, int] {
return Of[string](0) // default to 0 if decoding fails
})
decoder := withDefault(primaryDecoder)
result1 := decoder("42") // Success(42)
result2 := decoder("abc") // Success(0) - fallback
```
### 2. Error Context Addition (ChainLeft reads better)
```go
decodeUserAge := func(data map[string]any) Validation[int] {
age, ok := data["age"].(int)
if !ok {
return either.Left[int](validation.Errors{
{Value: data["age"], Messsage: "invalid type"},
})
}
return validation.Of(age)
}
// Use ChainLeft when emphasizing error transformation
addContext := ChainLeft(func(errs Errors) Decode[map[string]any, int] {
return func(data map[string]any) Validation[int] {
return either.Left[int](validation.Errors{
{
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
Messsage: "failed to decode user age",
},
})
}
})
decoder := addContext(decodeUserAge)
// Errors will include both original error and context
```
### 3. Conditional Recovery Based on Input
```go
decodePort := func(input string) Validation[int] {
port, err := strconv.Atoi(input)
if err != nil {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "invalid port"},
})
}
return validation.Of(port)
}
// Recover with different defaults based on input
smartDefault := OrElse(func(errs Errors) Decode[string, int] {
return func(input string) Validation[int] {
// Check input to determine appropriate default
if strings.Contains(input, "http") {
return validation.Of(80)
}
if strings.Contains(input, "https") {
return validation.Of(443)
}
return validation.Of(8080)
}
})
decoder := smartDefault(decodePort)
result1 := decoder("http-server") // Success(80)
result2 := decoder("https-server") // Success(443)
result3 := decoder("other") // Success(8080)
```
### 4. Pipeline Composition
```go
type Config struct {
DatabaseURL string
}
decodeConfig := func(data map[string]any) Validation[Config] {
url, ok := data["db_url"].(string)
if !ok {
return either.Left[Config](validation.Errors{
{Messsage: "missing db_url"},
})
}
return validation.Of(Config{DatabaseURL: url})
}
// Build a pipeline with multiple error handlers
decoder := F.Pipe2(
decodeConfig,
OrElse(func(errs Errors) Decode[map[string]any, Config] {
// Try environment variable as fallback
return func(data map[string]any) Validation[Config] {
if url := os.Getenv("DATABASE_URL"); url != "" {
return validation.Of(Config{DatabaseURL: url})
}
return either.Left[Config](errs)
}
}),
OrElse(func(errs Errors) Decode[map[string]any, Config] {
// Final fallback to default
return Of[map[string]any](Config{
DatabaseURL: "localhost:5432",
})
}),
)
```
## Comparison with validation.ChainLeft
The decode package's [`ChainLeft`](monad.go:53) wraps [`validation.ChainLeft`](../validation/monad.go:304) using the Reader transformer pattern:
| Aspect | validation.ChainLeft | decode.ChainLeft |
|--------|---------------------|------------------|
| **Input** | `Validation[A]` | `Decode[I, A]` (function) |
| **Handler** | `func(Errors) Validation[A]` | `func(Errors) Decode[I, A]` |
| **Output** | `Validation[A]` | `Decode[I, A]` (function) |
| **Context** | No input access | Access to original input `I` |
| **Use Case** | Pure validation logic | Decoding with input-dependent recovery |
The key difference is that decode's version gives handlers access to the original input through the returned `Decode[I, A]` function.
## When to Use Which Name
### Use **OrElse** when:
- Emphasizing fallback/alternative decoding logic
- Providing default values on decode failure
- The intent is "try this, or else try that"
- Code reads more naturally with "or else"
### Use **ChainLeft** when:
- Emphasizing technical error channel transformation
- Adding context or enriching error information
- The focus is on error handling mechanics
- Working with other functional programming concepts
## Verification
The test suite in [`monad_test.go`](monad_test.go:385) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
- ✅ Identical behavior for Success values
- ✅ Identical behavior for error recovery
- ✅ Identical behavior for error aggregation
- ✅ Identical behavior in pipeline composition
- ✅ Identical behavior for multiple error scenarios
- ✅ Both provide access to original input
Run the tests:
```bash
go test -v -run "TestChainLeft|TestOrElse" ./optics/codec/decode
```
## Conclusion
**`OrElse` is exactly the same as `ChainLeft`** in the decode package - they are aliases with identical implementations and behavior. Both:
1. **Delegate to validation.ChainLeft** for error handling logic
2. **Aggregate errors** when transformations fail
3. **Preserve successes** unchanged
4. **Enable recovery** from decode failures
5. **Provide access** to the original input
The choice between them is purely about **code readability and semantic intent**:
- Use **`OrElse`** when emphasizing fallback/alternative decoding
- Use **`ChainLeft`** when emphasizing error transformation
Both maintain the critical property of **error aggregation**, ensuring all validation failures are preserved and reported together.

View File

@@ -0,0 +1,335 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package decode
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type S to be used with the Bind operation.
// This is the starting point for building up a context using do-notation style.
//
// Example:
//
// type Result struct {
// x int
// y string
// }
// result := Do(Result{})
func Do[I, S any](
empty S,
) Decode[I, S] {
return Of[I](empty)
}
// Bind attaches the result of a computation to a context S1 to produce a context S2.
// This is used in do-notation style to sequentially build up a context.
//
// Example:
//
// type State struct { x int; y int }
// decoder := F.Pipe2(
// Do[string](State{}),
// Bind(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, func(s State) Decode[string, int] {
// return Of[string](42)
// }),
// )
// result := decoder("input") // Returns validation.Success(State{x: 42})
func Bind[I, S1, S2, A any](
setter func(A) func(S1) S2,
f Kleisli[I, S1, A],
) Operator[I, S1, S2] {
return C.Bind(
Chain[I, S1, S2],
Map[I, A, S2],
setter,
f,
)
}
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Unlike Bind, the computation function returns a plain value, not wrapped in Decode.
//
// Example:
//
// type State struct { x int; computed int }
// decoder := F.Pipe2(
// Do[string](State{x: 5}),
// Let[string](func(c int) func(State) State {
// return func(s State) State { s.computed = c; return s }
// }, func(s State) int { return s.x * 2 }),
// )
// result := decoder("input") // Returns validation.Success(State{x: 5, computed: 10})
func Let[I, S1, S2, B any](
key func(B) func(S1) S2,
f func(S1) B,
) Operator[I, S1, S2] {
return F.Let(
Map[I, S1, S2],
key,
f,
)
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
//
// Example:
//
// type State struct { x int; name string }
// result := F.Pipe2(
// Do(State{x: 5}),
// LetTo(func(n string) func(State) State {
// return func(s State) State { s.name = n; return s }
// }, "example"),
// )
func LetTo[I, S1, S2, B any](
key func(B) func(S1) S2,
b B,
) Operator[I, S1, S2] {
return F.LetTo(
Map[I, S1, S2],
key,
b,
)
}
// BindTo initializes a new state S1 from a value T.
// This is typically used as the first operation after creating a Decode value.
//
// Example:
//
// type State struct { value int }
// decoder := F.Pipe1(
// Of[string](42),
// BindTo[string](func(x int) State { return State{value: x} }),
// )
// result := decoder("input") // Returns validation.Success(State{value: 42})
func BindTo[I, S1, T any](
setter func(T) S1,
) Operator[I, T, S1] {
return C.BindTo(
Map[I, T, S1],
setter,
)
}
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
// This uses the applicative functor pattern, allowing parallel composition.
//
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
// and the value. If both validations fail, all errors are collected and returned together.
// This is useful for validating multiple independent fields and reporting all errors at once.
//
// Example:
//
// type State struct { x int; y int }
// decoder := F.Pipe2(
// Do[string](State{}),
// ApS(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, Of[string](42)),
// )
// result := decoder("input") // Returns validation.Success(State{x: 42})
//
// Error aggregation example:
//
// // Both decoders fail - errors are aggregated
// decoder1 := func(input string) Validation[State] {
// return validation.Failures[State](/* errors */)
// }
// decoder2 := func(input string) Validation[int] {
// return validation.Failures[int](/* errors */)
// }
// combined := ApS(setter, decoder2)(decoder1)
// result := combined("input") // Contains BOTH sets of errors
func ApS[I, S1, S2, T any](
setter func(T) func(S1) S2,
fa Decode[I, T],
) Operator[I, S1, S2] {
return A.ApS(
Ap[S2, I, T],
Map[I, S1, func(T) S2],
setter,
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
// and the value fail validation, all errors are collected and returned together.
// This enables comprehensive error reporting for complex nested structures.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type Address struct {
// Street string
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Create a lens for the Address field
// addressLens := lens.MakeLens(
// func(p Person) Address { return p.Address },
// func(p Person, a Address) Person { p.Address = a; return p },
// )
//
// // Use ApSL to update the address
// decoder := F.Pipe2(
// Of[string](Person{Name: "Alice"}),
// ApSL(
// addressLens,
// Of[string](Address{Street: "Main St", City: "NYC"}),
// ),
// )
// result := decoder("input") // Returns validation.Success(Person{...})
func ApSL[I, S, T any](
lens L.Lens[S, T],
fa Decode[I, T],
) Operator[I, S, S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// a Validation that produces the new value.
//
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
// the current value of the focused field.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but fail if it would exceed 100
// increment := func(v int) Decode[string, int] {
// return func(input string) Validation[int] {
// if v >= 100 {
// return validation.Failures[int](/* errors */)
// }
// return validation.Success(v + 1)
// }
// }
//
// decoder := F.Pipe1(
// Of[string](Counter{Value: 42}),
// BindL(valueLens, increment),
// )
// result := decoder("input") // Returns validation.Success(Counter{Value: 43})
func BindL[I, S, T any](
lens L.Lens[S, T],
f Kleisli[I, T, T],
) Operator[I, S, S] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in Validation).
//
// This is useful for pure transformations that cannot fail, such as mathematical operations,
// string manipulations, or other deterministic updates.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// decoder := F.Pipe1(
// Of[string](Counter{Value: 21}),
// LetL(valueLens, double),
// )
// result := decoder("input") // Returns validation.Success(Counter{Value: 42})
func LetL[I, S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[I, S, S] {
return Let[I](lens.Set, function.Flow2(lens.Get, f))
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// This is useful for resetting fields, initializing values, or setting fields to
// predetermined constants.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// decoder := F.Pipe1(
// Of[string](Config{Debug: true, Timeout: 30}),
// LetToL(debugLens, false),
// )
// result := decoder("input") // Returns validation.Success(Config{Debug: false, Timeout: 30})
func LetToL[I, S, T any](
lens L.Lens[S, T],
b T,
) Operator[I, S, S] {
return LetTo[I](lens.Set, b)
}

View File

@@ -0,0 +1,665 @@
package decode
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
func TestDo(t *testing.T) {
t.Run("creates decoder with empty state", func(t *testing.T) {
type State struct {
x int
y string
}
decoder := Do[string](State{})
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{}, value)
})
t.Run("creates decoder with initialized state", func(t *testing.T) {
type State struct {
x int
y string
}
initial := State{x: 42, y: "hello"}
decoder := Do[string](initial)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, initial, value)
})
t.Run("works with different input types", func(t *testing.T) {
intDecoder := Do[int](0)
assert.True(t, either.IsRight(intDecoder(42)))
strDecoder := Do[string]("")
assert.True(t, either.IsRight(strDecoder("test")))
type Custom struct{ Value int }
customDecoder := Do[[]byte](Custom{Value: 100})
assert.True(t, either.IsRight(customDecoder([]byte("data"))))
})
}
func TestBind(t *testing.T) {
type State struct {
x int
y int
}
t.Run("binds successful decode to state", func(t *testing.T) {
decoder := F.Pipe2(
Do[string](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Decode[string, int] {
return Of[string](42)
}),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Decode[string, int] {
return Of[string](10)
}),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 42, y: 10}, value)
})
t.Run("propagates failure", func(t *testing.T) {
decoder := F.Pipe2(
Do[string](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Decode[string, int] {
return Of[string](42)
}),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Decode[string, int] {
return func(input string) validation.Validation[int] {
return validation.Failures[int](validation.Errors{
&validation.ValidationError{Messsage: "y failed"},
})
}
}),
)
result := decoder("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(State) validation.Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "y failed", errors[0].Messsage)
})
t.Run("can access previous state values", func(t *testing.T) {
decoder := F.Pipe2(
Do[string](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Decode[string, int] {
return Of[string](10)
}),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Decode[string, int] {
// y depends on x
return Of[string](s.x * 2)
}),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 10, y: 20}, value)
})
t.Run("can access input in decoder", func(t *testing.T) {
decoder := F.Pipe1(
Do[string](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Decode[string, int] {
return func(input string) validation.Validation[int] {
// Use input to determine value
if input == "large" {
return validation.Success(100)
}
return validation.Success(10)
}
}),
)
result1 := decoder("large")
value1 := either.MonadFold(result1,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, 100, value1.x)
result2 := decoder("small")
value2 := either.MonadFold(result2,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, 10, value2.x)
})
}
func TestLet(t *testing.T) {
type State struct {
x int
computed int
}
t.Run("attaches pure computation result to state", func(t *testing.T) {
decoder := F.Pipe1(
Do[string](State{x: 5}),
Let[string](func(c int) func(State) State {
return func(s State) State { s.computed = c; return s }
}, func(s State) int { return s.x * 2 }),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 5, computed: 10}, value)
})
t.Run("chains multiple Let operations", func(t *testing.T) {
type State struct {
x int
y int
z int
}
decoder := F.Pipe3(
Do[string](State{x: 5}),
Let[string](func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) int { return s.x * 2 }),
Let[string](func(z int) func(State) State {
return func(s State) State { s.z = z; return s }
}, func(s State) int { return s.y + 10 }),
Let[string](func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) int { return s.z * 3 }),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
})
}
func TestLetTo(t *testing.T) {
type State struct {
x int
name string
}
t.Run("attaches constant value to state", func(t *testing.T) {
decoder := F.Pipe1(
Do[string](State{x: 5}),
LetTo[string](func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "example"),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 5, name: "example"}, value)
})
t.Run("sets multiple constant values", func(t *testing.T) {
type State struct {
name string
version int
active bool
}
decoder := F.Pipe3(
Do[string](State{}),
LetTo[string](func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "app"),
LetTo[string](func(v int) func(State) State {
return func(s State) State { s.version = v; return s }
}, 2),
LetTo[string](func(a bool) func(State) State {
return func(s State) State { s.active = a; return s }
}, true),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{name: "app", version: 2, active: true}, value)
})
}
func TestBindTo(t *testing.T) {
type State struct {
value int
}
t.Run("initializes state from value", func(t *testing.T) {
decoder := F.Pipe1(
Of[string](42),
BindTo[string](func(x int) State { return State{value: x} }),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{value: 42}, value)
})
t.Run("works with different types", func(t *testing.T) {
type StringState struct {
text string
}
decoder := F.Pipe1(
Of[string]("hello"),
BindTo[string](func(s string) StringState { return StringState{text: s} }),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) StringState { return StringState{} },
F.Identity[StringState],
)
assert.Equal(t, StringState{text: "hello"}, value)
})
}
func TestApS(t *testing.T) {
type State struct {
x int
y int
}
t.Run("attaches value using applicative pattern", func(t *testing.T) {
decoder := F.Pipe1(
Do[string](State{}),
ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, Of[string](42)),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 42}, value)
})
t.Run("accumulates errors from both decoders", func(t *testing.T) {
stateDecoder := func(input string) validation.Validation[State] {
return validation.Failures[State](validation.Errors{
&validation.ValidationError{Messsage: "state error"},
})
}
valueDecoder := func(input string) validation.Validation[int] {
return validation.Failures[int](validation.Errors{
&validation.ValidationError{Messsage: "value error"},
})
}
decoder := ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, valueDecoder)(stateDecoder)
result := decoder("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(State) validation.Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "state error")
assert.Contains(t, messages, "value error")
})
t.Run("combines multiple ApS operations", func(t *testing.T) {
decoder := F.Pipe2(
Do[string](State{}),
ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, Of[string](10)),
ApS(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, Of[string](20)),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 10, y: 20}, value)
})
}
func TestApSL(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
t.Run("updates nested structure using lens", func(t *testing.T) {
addressLens := L.MakeLens(
func(p Person) Address { return p.Address },
func(p Person, a Address) Person { p.Address = a; return p },
)
decoder := F.Pipe1(
Of[string](Person{Name: "Alice"}),
ApSL(
addressLens,
Of[string](Address{Street: "Main St", City: "NYC"}),
),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Person { return Person{} },
F.Identity[Person],
)
assert.Equal(t, "Alice", value.Name)
assert.Equal(t, "Main St", value.Address.Street)
assert.Equal(t, "NYC", value.Address.City)
})
t.Run("accumulates errors", func(t *testing.T) {
addressLens := L.MakeLens(
func(p Person) Address { return p.Address },
func(p Person, a Address) Person { p.Address = a; return p },
)
personDecoder := func(input string) validation.Validation[Person] {
return validation.Failures[Person](validation.Errors{
&validation.ValidationError{Messsage: "person error"},
})
}
addressDecoder := func(input string) validation.Validation[Address] {
return validation.Failures[Address](validation.Errors{
&validation.ValidationError{Messsage: "address error"},
})
}
decoder := ApSL(addressLens, addressDecoder)(personDecoder)
result := decoder("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
assert.Len(t, errors, 2)
})
}
func TestBindL(t *testing.T) {
type Counter struct {
Value int
}
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("updates field based on current value", func(t *testing.T) {
increment := func(v int) Decode[string, int] {
return Of[string](v + 1)
}
decoder := F.Pipe1(
Of[string](Counter{Value: 42}),
BindL(valueLens, increment),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, Counter{Value: 43}, value)
})
t.Run("fails validation based on current value", func(t *testing.T) {
increment := func(v int) Decode[string, int] {
return func(input string) validation.Validation[int] {
if v >= 100 {
return validation.Failures[int](validation.Errors{
&validation.ValidationError{Messsage: "exceeds limit"},
})
}
return validation.Success(v + 1)
}
}
decoder := F.Pipe1(
Of[string](Counter{Value: 100}),
BindL(valueLens, increment),
)
result := decoder("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(Counter) validation.Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "exceeds limit", errors[0].Messsage)
})
}
func TestLetL(t *testing.T) {
type Counter struct {
Value int
}
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("transforms field with pure function", func(t *testing.T) {
double := func(v int) int { return v * 2 }
decoder := F.Pipe1(
Of[string](Counter{Value: 21}),
LetL[string](valueLens, double),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, Counter{Value: 42}, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
add10 := func(v int) int { return v + 10 }
double := func(v int) int { return v * 2 }
decoder := F.Pipe2(
Of[string](Counter{Value: 5}),
LetL[string](valueLens, add10),
LetL[string](valueLens, double),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, Counter{Value: 30}, value)
})
}
func TestLetToL(t *testing.T) {
type Config struct {
Debug bool
Timeout int
}
debugLens := L.MakeLens(
func(c Config) bool { return c.Debug },
func(c Config, d bool) Config { c.Debug = d; return c },
)
t.Run("sets field to constant value", func(t *testing.T) {
decoder := F.Pipe1(
Of[string](Config{Debug: true, Timeout: 30}),
LetToL[string](debugLens, false),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, Config{Debug: false, Timeout: 30}, value)
})
t.Run("sets multiple fields", func(t *testing.T) {
timeoutLens := L.MakeLens(
func(c Config) int { return c.Timeout },
func(c Config, t int) Config { c.Timeout = t; return c },
)
decoder := F.Pipe2(
Of[string](Config{Debug: true, Timeout: 30}),
LetToL[string](debugLens, false),
LetToL[string](timeoutLens, 60),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, Config{Debug: false, Timeout: 60}, value)
})
}
func TestBindOperationsComposition(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
decoder := F.Pipe4(
Do[string](User{}),
LetTo[string](func(n string) func(User) User {
return func(u User) User { u.Name = n; return u }
}, "Alice"),
Bind(func(a int) func(User) User {
return func(u User) User { u.Age = a; return u }
}, func(u User) Decode[string, int] {
// Age validation
if len(u.Name) > 0 {
return Of[string](25)
}
return func(input string) validation.Validation[int] {
return validation.Failures[int](validation.Errors{
&validation.ValidationError{Messsage: "name required"},
})
}
}),
Let[string](func(e string) func(User) User {
return func(u User) User { u.Email = e; return u }
}, func(u User) string {
// Derive email from name
return u.Name + "@example.com"
}),
Bind(func(a int) func(User) User {
return func(u User) User { u.Age = a; return u }
}, func(u User) Decode[string, int] {
// Validate age is positive
if u.Age > 0 {
return Of[string](u.Age)
}
return func(input string) validation.Validation[int] {
return validation.Failures[int](validation.Errors{
&validation.ValidationError{Messsage: "age must be positive"},
})
}
}),
)
result := decoder("input")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) User { return User{} },
F.Identity[User],
)
assert.Equal(t, "Alice", value.Name)
assert.Equal(t, 25, value.Age)
assert.Equal(t, "Alice@example.com", value.Email)
})
}

View File

@@ -1,9 +1,10 @@
package decode
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
)
// Of creates a Decode that always succeeds with the given value.
@@ -14,7 +15,82 @@ import (
// decoder := decode.Of[string](42)
// result := decoder("any input") // Always returns validation.Success(42)
func Of[I, A any](a A) Decode[I, A] {
return reader.Of[I](validation.Of(a))
return readereither.Of[I, Errors](a)
}
// Left creates a Decode that always fails with the given validation errors.
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
// into the Decode context.
//
// Left is useful for:
// - Creating decoders that represent known failure states
// - Short-circuiting decode pipelines with specific errors
// - Building custom validation error responses
// - Testing error handling paths
//
// The returned decoder ignores its input and always returns a validation failure
// containing the provided errors. This makes it the identity element for the
// Alt/OrElse operations when used as a fallback.
//
// Type signature: func(Errors) Decode[I, A]
// - Takes validation errors
// - Returns a decoder that always fails with those errors
// - The decoder ignores its input of type I
// - The failure type A can be any type (phantom type)
//
// Example - Creating a failing decoder:
//
// failDecoder := decode.Left[string, int](validation.Errors{
// &validation.ValidationError{
// Value: nil,
// Messsage: "operation not supported",
// },
// })
// result := failDecoder("any input") // Always fails with the error
//
// Example - Short-circuiting with specific errors:
//
// validateAge := func(age int) Decode[map[string]any, int] {
// if age < 0 {
// return decode.Left[map[string]any, int](validation.Errors{
// &validation.ValidationError{
// Value: age,
// Context: validation.Context{{Key: "age", Type: "int"}},
// Messsage: "age cannot be negative",
// },
// })
// }
// return decode.Of[map[string]any](age)
// }
//
// Example - Building error responses:
//
// notFoundError := decode.Left[string, User](validation.Errors{
// &validation.ValidationError{
// Messsage: "user not found",
// },
// })
//
// decoder := decode.MonadAlt(
// tryFindUser,
// func() Decode[string, User] { return notFoundError },
// )
//
// Example - Testing error paths:
//
// // Create a decoder that always fails for testing
// alwaysFails := decode.Left[string, int](validation.Errors{
// &validation.ValidationError{Messsage: "test error"},
// })
//
// // Test error recovery logic
// recovered := decode.OrElse(func(errs Errors) Decode[string, int] {
// return decode.Of[string](0) // recover with default
// })(alwaysFails)
//
// result := recovered("input") // Success(0)
func Left[I, A any](err Errors) Decode[I, A] {
return readereither.Left[I, A](err)
}
// MonadChain sequences two decode operations, passing the result of the first to the second.
@@ -50,6 +126,212 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
)
}
// ChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
// This is the left-biased monadic chain operation that operates on validation failures.
//
// **Key behaviors**:
// - Success values pass through unchanged - the handler is never called
// - On failure, the handler receives the errors and can recover or add context
// - When the handler also fails, **both original and new errors are aggregated**
// - The handler returns a Decode[I, A], giving it access to the original input
//
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
// returns a failure, both the original errors AND the new errors are combined using the
// Errors monoid. This ensures no validation errors are lost.
//
// Use cases:
// - Adding contextual information to validation errors
// - Recovering from specific error conditions
// - Transforming error messages while preserving original errors
// - Implementing conditional recovery based on error types
//
// Example - Error recovery:
//
// failingDecoder := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not found"},
// })
// }
//
// recoverFromNotFound := ChainLeft(func(errs Errors) Decode[string, int] {
// for _, err := range errs {
// if err.Messsage == "not found" {
// return Of[string](0) // recover with default
// }
// }
// return func(input string) Validation[int] {
// return either.Left[int](errs)
// }
// })
//
// decoder := recoverFromNotFound(failingDecoder)
// result := decoder("input") // Success(0) - recovered from failure
//
// Example - Adding context:
//
// addContext := ChainLeft(func(errs Errors) Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to decode user age",
// },
// })
// }
// })
// // Result will contain BOTH original error and context error
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return readert.Chain[Decode[I, A]](
validation.ChainLeft,
f,
)
}
// MonadChainLeft transforms the error channel of a decoder, enabling error recovery and context addition.
// This is the uncurried version of ChainLeft, taking both the decoder and the transformation function directly.
//
// **Key behaviors**:
// - Success values pass through unchanged - the handler is never called
// - On failure, the handler receives the errors and can recover or add context
// - When the handler also fails, **both original and new errors are aggregated**
// - The handler returns a Decode[I, A], giving it access to the original input
//
// **Error Aggregation**: Unlike standard Either operations, when the transformation function
// returns a failure, both the original errors AND the new errors are combined using the
// Errors monoid. This ensures no validation errors are lost.
//
// This function is the direct, uncurried form of ChainLeft. Use ChainLeft when you need
// a curried operator for composition pipelines, and use MonadChainLeft when you have both
// the decoder and transformation function available at once.
//
// Use cases:
// - Adding contextual information to validation errors
// - Recovering from specific error conditions
// - Transforming error messages while preserving original errors
// - Implementing conditional recovery based on error types
//
// Example - Error recovery:
//
// failingDecoder := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not found"},
// })
// }
//
// recoverFromNotFound := func(errs Errors) Decode[string, int] {
// for _, err := range errs {
// if err.Messsage == "not found" {
// return Of[string](0) // recover with default
// }
// }
// return func(input string) Validation[int] {
// return either.Left[int](errs)
// }
// }
//
// decoder := MonadChainLeft(failingDecoder, recoverFromNotFound)
// result := decoder("input") // Success(0) - recovered from failure
//
// Example - Adding context:
//
// addContext := func(errs Errors) Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to decode user age",
// },
// })
// }
// }
//
// decoder := MonadChainLeft(failingDecoder, addContext)
// result := decoder("abc")
// // Result will contain BOTH original error and context error
//
// Example - Comparison with ChainLeft:
//
// // MonadChainLeft - direct application
// result1 := MonadChainLeft(decoder, handler)("input")
//
// // ChainLeft - curried for pipelines
// result2 := ChainLeft(handler)(decoder)("input")
//
// // Both produce identical results
func MonadChainLeft[I, A any](fa Decode[I, A], f Kleisli[I, Errors, A]) Decode[I, A] {
return readert.MonadChain(
validation.MonadChainLeft,
fa,
f,
)
}
// OrElse provides fallback decoding logic when the primary decoder fails.
// This is an alias for ChainLeft with a more semantic name for fallback scenarios.
//
// **OrElse is exactly the same as ChainLeft** - they are aliases with identical implementations
// and behavior. The choice between them is purely about code readability and semantic intent:
// - Use **OrElse** when emphasizing fallback/alternative decoding logic
// - Use **ChainLeft** when emphasizing technical error channel transformation
//
// **Key behaviors** (identical to ChainLeft):
// - Success values pass through unchanged - the handler is never called
// - On failure, the handler receives the errors and can provide an alternative
// - When the handler also fails, **both original and new errors are aggregated**
// - The handler returns a Decode[I, A], giving it access to the original input
//
// The name "OrElse" reads naturally in code: "try this decoder, or else try this alternative."
// This makes it ideal for expressing fallback logic and default values.
//
// Use cases:
// - Providing default values when decoding fails
// - Trying alternative decoding strategies
// - Implementing fallback chains with multiple alternatives
// - Input-dependent recovery (using access to original input)
//
// Example - Simple fallback:
//
// primaryDecoder := func(input string) Validation[int] {
// n, err := strconv.Atoi(input)
// if err != nil {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not a valid integer"},
// })
// }
// return validation.Of(n)
// }
//
// withDefault := OrElse(func(errs Errors) Decode[string, int] {
// return Of[string](0) // default to 0 if decoding fails
// })
//
// decoder := withDefault(primaryDecoder)
// result1 := decoder("42") // Success(42)
// result2 := decoder("abc") // Success(0) - fallback
//
// Example - Input-dependent fallback:
//
// smartDefault := OrElse(func(errs Errors) Decode[string, int] {
// return func(input string) Validation[int] {
// // Access original input to determine appropriate default
// if strings.Contains(input, "http") {
// return validation.Of(80)
// }
// if strings.Contains(input, "https") {
// return validation.Of(443)
// }
// return validation.Of(8080)
// }
// })
//
// decoder := smartDefault(decodePort)
// result1 := decoder("http-server") // Success(80)
// result2 := decoder("https-server") // Success(443)
// result3 := decoder("other") // Success(8080)
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return ChainLeft(f)
}
// MonadMap transforms the decoded value using the provided function.
// This is the functor map operation that applies a transformation to successful decode results.
//
@@ -127,3 +409,155 @@ func Ap[B, I, A any](fa Decode[I, A]) Operator[I, func(A) B, B] {
fa,
)
}
// MonadAlt provides alternative/fallback decoding with error aggregation.
// This is the Alternative pattern's core operation that tries the first decoder,
// and if it fails, tries the second decoder as a fallback.
//
// **Key behaviors**:
// - If first succeeds: returns the first result (second is never evaluated)
// - If first fails and second succeeds: returns the second result
// - If both fail: **aggregates errors from both decoders**
//
// **Error Aggregation**: Unlike simple fallback patterns, when both decoders fail,
// MonadAlt combines ALL errors from both attempts using the Errors monoid. This ensures
// complete visibility into why all alternatives failed, which is crucial for debugging
// and providing comprehensive error messages to users.
//
// The name "Alt" comes from the Alternative type class in functional programming,
// which represents computations with a notion of choice and failure.
//
// Use cases:
// - Trying multiple decoding strategies for the same input
// - Providing fallback decoders when primary decoder fails
// - Building validation pipelines with multiple alternatives
// - Implementing "try this, or else try that" logic
//
// Example - Simple fallback:
//
// primaryDecoder := func(input string) Validation[int] {
// n, err := strconv.Atoi(input)
// if err != nil {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not a valid integer"},
// })
// }
// return validation.Of(n)
// }
//
// fallbackDecoder := func() Decode[string, int] {
// return func(input string) Validation[int] {
// // Try parsing as float and converting to int
// f, err := strconv.ParseFloat(input, 64)
// if err != nil {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "not a valid number"},
// })
// }
// return validation.Of(int(f))
// }
// }
//
// decoder := MonadAlt(primaryDecoder, fallbackDecoder)
// result1 := decoder("42") // Success(42) - primary succeeds
// result2 := decoder("42.5") // Success(42) - fallback succeeds
// result3 := decoder("abc") // Failures with both errors aggregated
//
// Example - Multiple alternatives:
//
// decoder1 := parseAsJSON
// decoder2 := func() Decode[string, Config] { return parseAsYAML }
// decoder3 := func() Decode[string, Config] { return parseAsINI }
//
// // Try JSON, then YAML, then INI
// decoder := MonadAlt(MonadAlt(decoder1, decoder2), decoder3)
// // If all fail, errors from all three attempts are aggregated
//
// Example - Error aggregation:
//
// failing1 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Messsage: "primary decoder failed"},
// })
// }
// failing2 := func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Messsage: "fallback decoder failed"},
// })
// }
// }
//
// decoder := MonadAlt(failing1, failing2)
// result := decoder("input")
// // Result contains BOTH errors: ["primary decoder failed", "fallback decoder failed"]
func MonadAlt[I, A any](first Decode[I, A], second Lazy[Decode[I, A]]) Decode[I, A] {
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
}
// Alt creates an operator that provides alternative/fallback decoding with error aggregation.
// This is the curried version of MonadAlt, useful for composition pipelines.
//
// **Key behaviors** (identical to MonadAlt):
// - If first succeeds: returns the first result (second is never evaluated)
// - If first fails and second succeeds: returns the second result
// - If both fail: **aggregates errors from both decoders**
//
// The Alt operator enables building reusable fallback chains that can be applied
// to different decoders. It reads naturally in pipelines: "apply this decoder,
// with this alternative if it fails."
//
// Use cases:
// - Creating reusable fallback strategies
// - Building decoder combinators with alternatives
// - Composing multiple fallback layers
// - Implementing retry logic with different strategies
//
// Example - Creating a reusable fallback:
//
// // Create an operator that falls back to a default value
// withDefault := Alt(func() Decode[string, int] {
// return Of[string](0)
// })
//
// // Apply to any decoder
// decoder1 := withDefault(parseInteger)
// decoder2 := withDefault(parseFromJSON)
//
// result1 := decoder1("42") // Success(42)
// result2 := decoder1("abc") // Success(0) - fallback
//
// Example - Composing multiple alternatives:
//
// tryYAML := Alt(func() Decode[string, Config] { return parseAsYAML })
// tryINI := Alt(func() Decode[string, Config] { return parseAsINI })
// useDefault := Alt(func() Decode[string, Config] {
// return Of[string](defaultConfig)
// })
//
// // Build a pipeline: try JSON, then YAML, then INI, then default
// decoder := useDefault(tryINI(tryYAML(parseAsJSON)))
//
// Example - Error aggregation in pipeline:
//
// failing1 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{{Messsage: "error 1"}})
// }
// failing2 := func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{{Messsage: "error 2"}})
// }
// }
// failing3 := func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{{Messsage: "error 3"}})
// }
// }
//
// // Chain multiple alternatives
// decoder := Alt(failing3)(Alt(failing2)(failing1))
// result := decoder("input")
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
func Alt[I, A any](second Lazy[Decode[I, A]]) Operator[I, A, A] {
return ChainLeft(function.Ignore1of1[Errors](second))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,368 @@
package decode
import "github.com/IBM/fp-go/v2/monoid"
// ApplicativeMonoid creates a Monoid instance for Decode[I, A] given a Monoid for A.
// This allows combining decoders where both the decoded values and validation errors
// are combined according to their respective monoid operations.
//
// The resulting monoid enables:
// - Combining multiple decoders that produce monoidal values
// - Accumulating validation errors when any decoder fails
// - Building complex decoders from simpler ones through composition
//
// **Behavior**:
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
// - Concat: Combines two decoders:
// - Both succeed: Combines decoded values using the inner monoid
// - Any fails: Accumulates all validation errors using the Errors monoid
//
// This is particularly useful for:
// - Aggregating results from multiple independent decoders
// - Building decoders that combine partial results
// - Validating and combining configuration from multiple sources
// - Parallel validation with result accumulation
//
// Example - Combining string decoders:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// // Create a monoid for decoders that produce strings
// m := ApplicativeMonoid[map[string]any](S.Monoid)
//
// decoder1 := func(data map[string]any) Validation[string] {
// if name, ok := data["firstName"].(string); ok {
// return validation.Of(name)
// }
// return either.Left[string](validation.Errors{
// {Messsage: "missing firstName"},
// })
// }
//
// decoder2 := func(data map[string]any) Validation[string] {
// if name, ok := data["lastName"].(string); ok {
// return validation.Of(" " + name)
// }
// return either.Left[string](validation.Errors{
// {Messsage: "missing lastName"},
// })
// }
//
// // Combine decoders - will concatenate strings if both succeed
// combined := m.Concat(decoder1, decoder2)
// result := combined(map[string]any{
// "firstName": "John",
// "lastName": "Doe",
// }) // Success("John Doe")
//
// Example - Error accumulation:
//
// // If any decoder fails, errors are accumulated
// result := combined(map[string]any{}) // Failures with both error messages
//
// Example - Numeric aggregation:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// intMonoid := monoid.MakeMonoid(N.Add[int], 0)
// m := ApplicativeMonoid[string](intMonoid)
//
// decoder1 := func(input string) Validation[int] {
// return validation.Of(10)
// }
// decoder2 := func(input string) Validation[int] {
// return validation.Of(32)
// }
//
// combined := m.Concat(decoder1, decoder2)
// result := combined("input") // Success(42) - values are added
func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
return monoid.ApplicativeMonoid(
Of[I, A],
MonadMap[I, A, Endomorphism[A]],
MonadAp[A, I, A],
m,
)
}
// AlternativeMonoid creates a Monoid instance for Decode[I, A] using the Alternative pattern.
// This combines applicative error-accumulation behavior with alternative fallback behavior,
// allowing you to both accumulate errors and provide fallback alternatives when combining decoders.
//
// The Alternative pattern provides two key operations:
// - Applicative operations (Of, Map, Ap): accumulate errors when combining decoders
// - Alternative operation (Alt): provide fallback when a decoder fails
//
// This monoid is particularly useful when you want to:
// - Try multiple decoding strategies and fall back to alternatives
// - Combine successful values using the provided monoid
// - Accumulate all errors from failed attempts
// - Build decoding pipelines with fallback logic
//
// **Behavior**:
// - Empty: Returns a decoder that always succeeds with the empty value from the inner monoid
// - Concat: Combines two decoders using both applicative and alternative semantics:
// - If first succeeds and second succeeds: combines decoded values using inner monoid
// - If first fails: tries second as fallback (alternative behavior)
// - If both fail: **accumulates all errors from both decoders**
//
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
// are combined using the Errors monoid. This provides complete visibility into why all
// alternatives failed, which is essential for debugging and user feedback.
//
// Type Parameters:
// - I: The input type being decoded
// - A: The output type after successful decoding
//
// Parameters:
// - m: The monoid for combining successful decoded values of type A
//
// Returns:
//
// A Monoid[Decode[I, A]] that combines applicative and alternative behaviors
//
// Example - Combining successful decoders:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// m := AlternativeMonoid[string](S.Monoid)
//
// decoder1 := func(input string) Validation[string] {
// return validation.Of("Hello")
// }
// decoder2 := func(input string) Validation[string] {
// return validation.Of(" World")
// }
//
// combined := m.Concat(decoder1, decoder2)
// result := combined("input")
// // Result: Success("Hello World") - values combined using string monoid
//
// Example - Fallback behavior:
//
// m := AlternativeMonoid[string](S.Monoid)
//
// failing := func(input string) Validation[string] {
// return either.Left[string](validation.Errors{
// {Value: input, Messsage: "primary failed"},
// })
// }
// fallback := func(input string) Validation[string] {
// return validation.Of("fallback value")
// }
//
// combined := m.Concat(failing, fallback)
// result := combined("input")
// // Result: Success("fallback value") - second decoder used as fallback
//
// Example - Error accumulation when both fail:
//
// m := AlternativeMonoid[string](S.Monoid)
//
// failing1 := func(input string) Validation[string] {
// return either.Left[string](validation.Errors{
// {Value: input, Messsage: "error 1"},
// })
// }
// failing2 := func(input string) Validation[string] {
// return either.Left[string](validation.Errors{
// {Value: input, Messsage: "error 2"},
// })
// }
//
// combined := m.Concat(failing1, failing2)
// result := combined("input")
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
//
// Example - Building decoder with multiple fallbacks:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// m := AlternativeMonoid[string](N.MonoidSum[int]())
//
// // Try to parse from different formats
// parseJSON := func(input string) Validation[int] { /* ... */ }
// parseYAML := func(input string) Validation[int] { /* ... */ }
// parseINI := func(input string) Validation[int] { /* ... */ }
//
// // Combine with fallback chain
// decoder := m.Concat(m.Concat(parseJSON, parseYAML), parseINI)
// // Uses first successful parser, or accumulates all errors if all fail
//
// Example - Combining multiple configuration sources:
//
// type Config struct{ Port int }
// configMonoid := monoid.MakeMonoid(
// func(a, b Config) Config {
// if b.Port != 0 { return b }
// return a
// },
// Config{Port: 0},
// )
//
// m := AlternativeMonoid[map[string]any](configMonoid)
//
// fromEnv := func(data map[string]any) Validation[Config] { /* ... */ }
// fromFile := func(data map[string]any) Validation[Config] { /* ... */ }
// fromDefault := func(data map[string]any) Validation[Config] {
// return validation.Of(Config{Port: 8080})
// }
//
// // Try env, then file, then default
// decoder := m.Concat(m.Concat(fromEnv, fromFile), fromDefault)
// // Returns first successful config, or all errors if all fail
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Decode[I, A]] {
return monoid.AlternativeMonoid(
Of[I, A],
MonadMap[I, A, func(A) A],
MonadAp[A, I, A],
MonadAlt[I, A],
m,
)
}
// AltMonoid creates a Monoid instance for Decode[I, A] using the Alt (alternative) operation.
// This monoid provides a way to combine decoders with fallback behavior, where the second
// decoder is used as an alternative if the first one fails.
//
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
// for decoding scenarios where you want to attempt multiple decoding strategies in sequence
// and use the first one that succeeds.
//
// **Behavior**:
// - Empty: Returns the provided zero value (a lazy computation that produces a Decode[I, A])
// - Concat: Combines two decoders using Alt semantics:
// - If first succeeds: returns the first result (second is never evaluated)
// - If first fails: tries the second decoder as fallback
// - If both fail: **aggregates errors from both decoders**
//
// **Error Aggregation**: When both decoders fail, all validation errors from both attempts
// are combined using the Errors monoid. This ensures complete visibility into why all
// alternatives failed.
//
// This is different from [AlternativeMonoid] in that:
// - AltMonoid uses a custom zero value (provided by the user)
// - AlternativeMonoid derives the zero from an inner monoid
// - AltMonoid is simpler and only provides fallback behavior
// - AlternativeMonoid combines applicative and alternative behaviors
//
// Type Parameters:
// - I: The input type being decoded
// - A: The output type after successful decoding
//
// Parameters:
// - zero: A lazy computation that produces the identity/empty Decode[I, A].
// This is typically a decoder that always succeeds with a default value, or could be
// a decoder that always fails representing "no decoding attempted"
//
// Returns:
//
// A Monoid[Decode[I, A]] that combines decoders with fallback behavior
//
// Example - Using default value as zero:
//
// m := AltMonoid(func() Decode[string, int] {
// return Of[string](0)
// })
//
// failing := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "failed"},
// })
// }
// succeeding := func(input string) Validation[int] {
// return validation.Of(42)
// }
//
// combined := m.Concat(failing, succeeding)
// result := combined("input")
// // Result: Success(42) - falls back to second decoder
//
// empty := m.Empty()
// result2 := empty("input")
// // Result: Success(0) - the provided zero value
//
// Example - Chaining multiple fallbacks:
//
// m := AltMonoid(func() Decode[string, Config] {
// return Of[string](defaultConfig)
// })
//
// primary := parseFromPrimarySource // Fails
// secondary := parseFromSecondarySource // Fails
// tertiary := parseFromTertiarySource // Succeeds
//
// // Chain fallbacks
// decoder := m.Concat(m.Concat(primary, secondary), tertiary)
// result := decoder("input")
// // Result: Success from tertiary - uses first successful decoder
//
// Example - Error aggregation when all fail:
//
// m := AltMonoid(func() Decode[string, int] {
// return func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Messsage: "no default available"},
// })
// }
// })
//
// failing1 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "error 1"},
// })
// }
// failing2 := func(input string) Validation[int] {
// return either.Left[int](validation.Errors{
// {Value: input, Messsage: "error 2"},
// })
// }
//
// combined := m.Concat(failing1, failing2)
// result := combined("input")
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
//
// Example - Building a decoder pipeline with fallbacks:
//
// m := AltMonoid(func() Decode[string, Config] {
// return Of[string](defaultConfig)
// })
//
// // Try multiple decoding sources in order
// decoders := []Decode[string, Config]{
// loadFromFile("config.json"), // Try file first
// loadFromEnv, // Then environment
// loadFromRemote("api.example.com"), // Then remote API
// }
//
// // Fold using the monoid to get first successful config
// result := array.MonoidFold(m)(decoders)
// // Result: First successful config, or defaultConfig if all fail
//
// Example - Comparing with AlternativeMonoid:
//
// // AltMonoid - simple fallback with custom zero
// altM := AltMonoid(func() Decode[string, int] {
// return Of[string](0)
// })
//
// // AlternativeMonoid - combines values when both succeed
// import N "github.com/IBM/fp-go/v2/number"
// altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
//
// decoder1 := Of[string](10)
// decoder2 := Of[string](32)
//
// // AltMonoid: returns first success (10)
// result1 := altM.Concat(decoder1, decoder2)("input")
// // Result: Success(10)
//
// // AlternativeMonoid: combines both successes (10 + 32 = 42)
// result2 := altMonoid.Concat(decoder1, decoder2)("input")
// // Result: Success(42)
func AltMonoid[I, A any](zero Lazy[Decode[I, A]]) Monoid[Decode[I, A]] {
return monoid.AltMonoid(
zero,
MonadAlt[I, A],
)
}

View File

@@ -0,0 +1,970 @@
package decode
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
MO "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/optics/codec/validation"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestApplicativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
result := empty("any input")
assert.Equal(t, validation.Of(""), result)
})
t.Run("concat combines successful decoders", func(t *testing.T) {
decoder1 := Of[string]("Hello")
decoder2 := Of[string](" World")
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.Equal(t, validation.Of("Hello World"), result)
})
t.Run("concat with failure returns failure", func(t *testing.T) {
decoder1 := Of[string]("Hello")
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "decode failed"},
})
}
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "decode failed", errors[0].Messsage)
})
t.Run("concat accumulates all errors from both failures", func(t *testing.T) {
decoder1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("concat with empty preserves decoder", func(t *testing.T) {
decoder := Of[string]("test")
empty := m.Empty()
result1 := m.Concat(decoder, empty)("input")
result2 := m.Concat(empty, decoder)("input")
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
})
})
t.Run("with int addition monoid", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := ApplicativeMonoid[string](intMonoid)
t.Run("empty returns decoder with zero", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("concat adds decoded values", func(t *testing.T) {
decoder1 := Of[string](10)
decoder2 := Of[string](32)
combined := m.Concat(decoder1, decoder2)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("multiple concat operations", func(t *testing.T) {
decoder1 := Of[string](1)
decoder2 := Of[string](2)
decoder3 := Of[string](3)
decoder4 := Of[string](4)
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
})
})
t.Run("with map input type", func(t *testing.T) {
m := ApplicativeMonoid[map[string]any](S.Monoid)
t.Run("combines decoders with different inputs", func(t *testing.T) {
decoder1 := func(data map[string]any) Validation[string] {
if name, ok := data["firstName"].(string); ok {
return validation.Of(name)
}
return either.Left[string](validation.Errors{
{Messsage: "missing firstName"},
})
}
decoder2 := func(data map[string]any) Validation[string] {
if name, ok := data["lastName"].(string); ok {
return validation.Of(" " + name)
}
return either.Left[string](validation.Errors{
{Messsage: "missing lastName"},
})
}
combined := m.Concat(decoder1, decoder2)
// Test success case
result1 := combined(map[string]any{
"firstName": "John",
"lastName": "Doe",
})
value1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "John Doe", value1)
// Test failure case - both fields missing
result2 := combined(map[string]any{})
assert.True(t, either.IsLeft(result2))
errors := either.MonadFold(result2,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 2)
})
})
}
func TestMonoidLaws(t *testing.T) {
t.Run("ApplicativeMonoid satisfies monoid laws", func(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
decoder1 := Of[string]("a")
decoder2 := Of[string]("b")
t.Run("left identity", func(t *testing.T) {
// empty + a = a
result := m.Concat(m.Empty(), decoder1)("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("right identity", func(t *testing.T) {
// a + empty = a
result := m.Concat(decoder1, m.Empty())("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("associativity", func(t *testing.T) {
decoder3 := Of[string]("c")
// (a + b) + c = a + (b + c)
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
})
})
}
func TestApplicativeMonoidWithFailures(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
t.Run("failure propagates through concat", func(t *testing.T) {
decoder1 := Of[string]("a")
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error"},
})
}
decoder3 := Of[string]("c")
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 1)
})
t.Run("multiple failures accumulate", func(t *testing.T) {
decoder1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
decoder2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
decoder3 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 3"},
})
}
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
result := combined("input")
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 3)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
assert.Contains(t, messages, "error 3")
})
}
func TestApplicativeMonoidEdgeCases(t *testing.T) {
t.Run("with custom struct monoid", func(t *testing.T) {
type Counter struct{ Count int }
counterMonoid := MO.MakeMonoid(
func(a, b Counter) Counter { return Counter{Count: a.Count + b.Count} },
Counter{Count: 0},
)
m := ApplicativeMonoid[string](counterMonoid)
decoder1 := Of[string](Counter{Count: 5})
decoder2 := Of[string](Counter{Count: 10})
combined := m.Concat(decoder1, decoder2)
result := combined("input")
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, 15, value.Count)
})
t.Run("empty concat empty", func(t *testing.T) {
m := ApplicativeMonoid[string](S.Monoid)
combined := m.Concat(m.Empty(), m.Empty())
result := combined("input")
value := either.MonadFold(result,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
})
t.Run("with different input types", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := ApplicativeMonoid[int](intMonoid)
decoder1 := func(input int) Validation[int] {
return validation.Of(input * 2)
}
decoder2 := func(input int) Validation[int] {
return validation.Of(input + 10)
}
combined := m.Concat(decoder1, decoder2)
result := combined(5)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
// (5 * 2) + (5 + 10) = 10 + 15 = 25
assert.Equal(t, 25, value)
})
}
func TestApplicativeMonoidRealWorldScenarios(t *testing.T) {
t.Run("combining configuration from multiple sources", func(t *testing.T) {
type Config struct {
Host string
Port int
}
// Monoid that combines configs (last non-empty wins for strings, sum for ints)
configMonoid := MO.MakeMonoid(
func(a, b Config) Config {
host := a.Host
if b.Host != "" {
host = b.Host
}
return Config{
Host: host,
Port: a.Port + b.Port,
}
},
Config{Host: "", Port: 0},
)
m := ApplicativeMonoid[map[string]any](configMonoid)
decoder1 := func(data map[string]any) Validation[Config] {
if host, ok := data["host"].(string); ok {
return validation.Of(Config{Host: host, Port: 0})
}
return either.Left[Config](validation.Errors{
{Messsage: "missing host"},
})
}
decoder2 := func(data map[string]any) Validation[Config] {
if port, ok := data["port"].(int); ok {
return validation.Of(Config{Host: "", Port: port})
}
return either.Left[Config](validation.Errors{
{Messsage: "missing port"},
})
}
combined := m.Concat(decoder1, decoder2)
// Success case
result := combined(map[string]any{
"host": "localhost",
"port": 8080,
})
config := either.MonadFold(result,
func(Errors) Config { return Config{} },
F.Identity[Config],
)
assert.Equal(t, "localhost", config.Host)
assert.Equal(t, 8080, config.Port)
})
t.Run("aggregating validation results", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := ApplicativeMonoid[string](intMonoid)
// Decoder that extracts and validates a number
makeDecoder := func(value int, shouldFail bool) Decode[string, int] {
return func(input string) Validation[int] {
if shouldFail {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "validation failed"},
})
}
return validation.Of(value)
}
}
// All succeed - values are summed
decoder1 := makeDecoder(10, false)
decoder2 := makeDecoder(20, false)
decoder3 := makeDecoder(12, false)
combined := m.Concat(m.Concat(decoder1, decoder2), decoder3)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
// Some fail - errors are accumulated
decoder4 := makeDecoder(10, true)
decoder5 := makeDecoder(20, true)
combinedFail := m.Concat(decoder4, decoder5)
resultFail := combinedFail("input")
assert.True(t, either.IsLeft(resultFail))
errors := either.MonadFold(resultFail,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 2)
})
}
// TestAlternativeMonoid tests the AlternativeMonoid function
func TestAlternativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := AlternativeMonoid[string](S.Monoid)
t.Run("empty returns decoder that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
assert.Equal(t, validation.Of(""), result)
})
t.Run("concat combines successful decoders using monoid", func(t *testing.T) {
decoder1 := Of[string]("Hello")
decoder2 := Of[string](" World")
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.Equal(t, validation.Of("Hello World"), result)
})
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
succeeding := Of[string]("fallback")
combined := m.Concat(failing, succeeding)
result := combined("input")
assert.Equal(t, validation.Of("fallback"), result)
})
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(failing1, failing2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("concat with empty preserves decoder", func(t *testing.T) {
decoder := Of[string]("test")
empty := m.Empty()
result1 := m.Concat(decoder, empty)("input")
result2 := m.Concat(empty, decoder)("input")
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
})
})
t.Run("with int addition monoid", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := AlternativeMonoid[string](intMonoid)
t.Run("empty returns decoder with zero", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
decoder1 := Of[string](10)
decoder2 := Of[string](32)
combined := m.Concat(decoder1, decoder2)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("concat uses fallback when first fails", func(t *testing.T) {
failing := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("multiple concat operations", func(t *testing.T) {
decoder1 := Of[string](1)
decoder2 := Of[string](2)
decoder3 := Of[string](3)
decoder4 := Of[string](4)
combined := m.Concat(m.Concat(m.Concat(decoder1, decoder2), decoder3), decoder4)
result := combined("input")
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
})
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AlternativeMonoid[string](S.Monoid)
decoder1 := Of[string]("a")
decoder2 := Of[string]("b")
decoder3 := Of[string]("c")
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), decoder1)("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("right identity", func(t *testing.T) {
result := m.Concat(decoder1, m.Empty())("input")
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
})
})
t.Run("error aggregation with multiple failures", func(t *testing.T) {
m := AlternativeMonoid[string](S.Monoid)
failing1 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
failing3 := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 3"},
})
}
combined := m.Concat(m.Concat(failing1, failing2), failing3)
result := combined("input")
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 3, "Should aggregate errors from all decoders")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
assert.Contains(t, messages, "error 3")
})
}
// TestAltMonoid tests the AltMonoid function
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
t.Run("empty returns the provided zero decoder", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
assert.Equal(t, validation.Of(0), result)
})
t.Run("concat returns first decoder when it succeeds", func(t *testing.T) {
decoder1 := Of[string](42)
decoder2 := Of[string](100)
combined := m.Concat(decoder1, decoder2)
result := combined("input")
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(failing1, failing2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both decoders")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
})
t.Run("with failing zero", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "no default available"},
})
}
})
t.Run("empty returns the failing zero decoder", func(t *testing.T) {
empty := m.Empty()
result := empty("input")
assert.True(t, either.IsLeft(result))
})
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
failing1 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
failing2 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
combined := m.Concat(failing1, failing2)
result := combined("input")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
})
})
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Decode[string, string] {
return Of[string]("default")
})
primary := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "primary failed"},
})
}
secondary := func(input string) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "secondary failed"},
})
}
tertiary := Of[string]("tertiary value")
combined := m.Concat(m.Concat(primary, secondary), tertiary)
result := combined("input")
assert.Equal(t, validation.Of("tertiary value"), result)
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
decoder1 := Of[string](1)
decoder2 := Of[string](2)
decoder3 := Of[string](3)
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), decoder1)("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
// With AltMonoid, first success wins, so empty (0) is returned
assert.Equal(t, 0, value)
})
t.Run("right identity", func(t *testing.T) {
result := m.Concat(decoder1, m.Empty())("input")
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
// First decoder succeeds, so 1 is returned
assert.Equal(t, 1, value)
})
t.Run("associativity", func(t *testing.T) {
// For AltMonoid, first success wins
left := m.Concat(m.Concat(decoder1, decoder2), decoder3)("input")
right := m.Concat(decoder1, m.Concat(decoder2, decoder3))("input")
leftVal := either.MonadFold(left,
func(Errors) int { return -1 },
F.Identity[int],
)
rightVal := either.MonadFold(right,
func(Errors) int { return -1 },
F.Identity[int],
)
// Both should return 1 (first success)
assert.Equal(t, 1, leftVal)
assert.Equal(t, 1, rightVal)
})
})
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
// AltMonoid - first success wins
altM := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
// AlternativeMonoid - combines successes
altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
decoder1 := Of[string](10)
decoder2 := Of[string](32)
// AltMonoid: returns first success (10)
result1 := altM.Concat(decoder1, decoder2)("input")
value1 := either.MonadFold(result1,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value1, "AltMonoid returns first success")
// AlternativeMonoid: combines both successes (10 + 32 = 42)
result2 := altMonoid.Concat(decoder1, decoder2)("input")
value2 := either.MonadFold(result2,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
})
t.Run("error aggregation with context", func(t *testing.T) {
m := AltMonoid(func() Decode[string, int] {
return Of[string](0)
})
failing1 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "parse error",
Context: validation.Context{{Key: "field", Type: "int"}},
},
})
}
failing2 := func(input string) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "validation error",
Context: validation.Context{{Key: "value", Type: "int"}},
},
})
}
combined := m.Concat(failing1, failing2)
result := combined("abc")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both decoders")
// Verify that errors with context are present
hasParseError := false
hasValidationError := false
for _, err := range errors {
if err.Messsage == "parse error" {
hasParseError = true
assert.NotNil(t, err.Context)
}
if err.Messsage == "validation error" {
hasValidationError = true
assert.NotNil(t, err.Context)
}
}
assert.True(t, hasParseError, "Should have parse error")
assert.True(t, hasValidationError, "Should have validation error")
})
}

View File

@@ -1,30 +1,346 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package decode
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
)
type (
// Errors is a collection of validation errors that occurred during decoding.
// This is an alias for validation.Errors, which is []*ValidationError.
//
// Errors accumulates multiple validation failures, allowing decoders to report
// all problems at once rather than failing on the first error. This is particularly
// useful for form validation, API request validation, and configuration parsing
// where users benefit from seeing all issues simultaneously.
//
// The Errors type forms a Semigroup and Monoid, enabling:
// - Concatenation: Combining errors from multiple decoders
// - Accumulation: Collecting errors through applicative operations
// - Empty value: An empty slice representing no errors (success)
//
// Each error in the collection is a *ValidationError containing:
// - Value: The actual value that failed validation
// - Context: The path to the value in nested structures
// - Message: Human-readable error description
// - Cause: Optional underlying error
//
// Example:
//
// // Multiple validation failures
// errors := Errors{
// &validation.ValidationError{
// Value: "",
// Context: []validation.ContextEntry{{Key: "name"}},
// Messsage: "name is required",
// },
// &validation.ValidationError{
// Value: "invalid@",
// Context: []validation.ContextEntry{{Key: "email"}},
// Messsage: "invalid email format",
// },
// }
//
// // Create a failed validation with these errors
// result := validation.Failures[User](errors)
//
// // Errors can be combined using the monoid
// moreErrors := Errors{
// &validation.ValidationError{
// Value: -1,
// Context: []validation.ContextEntry{{Key: "age"}},
// Messsage: "age must be positive",
// },
// }
// allErrors := append(errors, moreErrors...)
Errors = validation.Errors
// Validation represents the result of a validation operation that may contain
// validation errors or a successfully validated value of type A.
// This is an alias for validation.Validation[A], which is Either[Errors, A].
//
// In the decode context:
// - Left(Errors): Decoding failed with one or more validation errors
// - Right(A): Successfully decoded value of type A
//
// Example:
//
// // Success case
// valid := validation.Success(42) // Right(42)
//
// // Failure case
// invalid := validation.Failures[int](validation.Errors{
// &validation.ValidationError{Messsage: "invalid format"},
// }) // Left([...])
Validation[A any] = validation.Validation[A]
// Reader represents a computation that depends on an environment R and produces a value A.
// This is an alias for reader.Reader[R, A], which is func(R) A.
//
// In the decode context, Reader is used to access the input data being decoded.
// The environment R is typically the raw input (e.g., JSON, string, bytes) that
// needs to be decoded into a structured type A.
//
// Example:
//
// // A reader that extracts a field from a map
// getField := func(data map[string]any) string {
// return data["name"].(string)
// } // Reader[map[string]any, string]
Reader[R, A any] = reader.Reader[R, A]
// Decode is a function that decodes input I to type A with validation.
// It returns a Validation result directly.
// It combines the Reader pattern (for accessing input) with Validation (for error handling).
//
// Type: func(I) Validation[A]
//
// A Decode function:
// 1. Takes raw input of type I (e.g., JSON, string, bytes)
// 2. Attempts to decode/parse it into type A
// 3. Returns a Validation[A] with either:
// - Success(A): Successfully decoded value
// - Failures(Errors): Validation errors describing what went wrong
//
// This type is the foundation of the decode package, enabling composable,
// type-safe decoding with comprehensive error reporting.
//
// Example:
//
// // Decode a string to an integer
// decodeInt := func(input string) Validation[int] {
// n, err := strconv.Atoi(input)
// if err != nil {
// return validation.Failures[int](validation.Errors{
// &validation.ValidationError{
// Value: input,
// Messsage: "not a valid integer",
// Cause: err,
// },
// })
// }
// return validation.Success(n)
// } // Decode[string, int]
//
// result := decodeInt("42") // Success(42)
// result := decodeInt("abc") // Failures([...])
Decode[I, A any] = Reader[I, Validation[A]]
// Kleisli represents a function from A to a decoded B given input type I.
// It's a Reader that takes an input A and produces a Decode[I, B] function.
// This enables composition of decoding operations in a functional style.
//
// Type: func(A) Decode[I, B]
// which expands to: func(A) func(I) Validation[B]
//
// Kleisli arrows are the fundamental building blocks for composing decoders.
// They allow you to chain decoding operations where each step can:
// 1. Depend on the result of the previous step (the A parameter)
// 2. Access the original input (the I parameter via Decode)
// 3. Fail with validation errors (via Validation[B])
//
// This is particularly useful for:
// - Conditional decoding based on previously decoded values
// - Multi-stage decoding pipelines
// - Dependent field validation
//
// Example:
//
// // Decode a user, then decode their age based on their type
// decodeAge := func(userType string) Decode[map[string]any, int] {
// return func(data map[string]any) Validation[int] {
// if userType == "admin" {
// // Admins must be 18+
// age := data["age"].(int)
// if age < 18 {
// return validation.Failures[int](/* error */)
// }
// return validation.Success(age)
// }
// // Regular users can be any age
// return validation.Success(data["age"].(int))
// }
// } // Kleisli[map[string]any, string, int]
//
// // Use with Chain to compose decoders
// decoder := F.Pipe2(
// decodeUserType, // Decode[map[string]any, string]
// Chain(decodeAge), // Chains with Kleisli
// Map(func(age int) User { // Transform to final type
// return User{Age: age}
// }),
// )
Kleisli[I, A, B any] = Reader[A, Decode[I, B]]
// Operator represents a decoding transformation that takes a decoded A and produces a decoded B.
// It's a specialized Kleisli arrow for composing decode operations where the input is already decoded.
// This allows chaining multiple decode transformations together.
//
// Type: func(Decode[I, A]) Decode[I, B]
//
// Operators are higher-order functions that transform one decoder into another.
// They are the result of partially applying functions like Map, Chain, and Ap,
// making them ideal for use in composition pipelines with F.Pipe.
//
// Key characteristics:
// - Takes a Decode[I, A] as input
// - Returns a Decode[I, B] as output
// - Preserves the input type I (the raw data being decoded)
// - Transforms the output type from A to B
//
// Common operators:
// - Map(f): Transforms successful decode results
// - Chain(f): Sequences dependent decode operations
// - Ap(fa): Applies function decoders to value decoders
//
// Example:
//
// // Create reusable operators
// toString := Map(func(n int) string {
// return strconv.Itoa(n)
// }) // Operator[string, int, string]
//
// validatePositive := Chain(func(n int) Decode[string, int] {
// return func(input string) Validation[int] {
// if n <= 0 {
// return validation.Failures[int](/* error */)
// }
// return validation.Success(n)
// }
// }) // Operator[string, int, int]
//
// // Compose operators in a pipeline
// decoder := F.Pipe2(
// decodeInt, // Decode[string, int]
// validatePositive, // Operator[string, int, int]
// toString, // Operator[string, int, string]
// ) // Decode[string, string]
//
// result := decoder("42") // Success("42")
// result := decoder("-5") // Failures([...])
Operator[I, A, B any] = Kleisli[I, Decode[I, A], B]
// Endomorphism represents a function from a type to itself: func(A) A.
// This is an alias for endomorphism.Endomorphism[A].
//
// In the decode context, endomorphisms are used with LetL to transform
// decoded values using pure functions that don't change the type.
//
// Endomorphisms are useful for:
// - Normalizing data (e.g., trimming strings, rounding numbers)
// - Applying business rules (e.g., clamping values to ranges)
// - Data sanitization (e.g., removing special characters)
//
// Example:
//
// // Normalize a string by trimming and lowercasing
// normalize := func(s string) string {
// return strings.ToLower(strings.TrimSpace(s))
// } // Endomorphism[string]
//
// // Clamp an integer to a range
// clamp := func(n int) int {
// if n < 0 { return 0 }
// if n > 100 { return 100 }
// return n
// } // Endomorphism[int]
//
// // Use with LetL to transform decoded values
// decoder := F.Pipe1(
// decodeString,
// LetL(nameLens, normalize),
// )
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Monoid represents an algebraic structure with an associative binary operation
// and an identity element. This is an alias for monoid.Monoid[A].
//
// A Monoid[A] consists of:
// - Concat: func(A, A) A - An associative binary operation
// - Empty: func() A - An identity element
//
// In the decode context, monoids are used to combine multiple decoders or
// validation results. The most common use case is combining validation errors
// from multiple decoders using the Errors monoid.
//
// Properties:
// - Associativity: Concat(Concat(a, b), c) == Concat(a, Concat(b, c))
// - Identity: Concat(Empty(), a) == a == Concat(a, Empty())
//
// Common monoid instances:
// - Errors: Combines validation errors from multiple sources
// - Array: Concatenates arrays of decoded values
// - String: Concatenates strings
//
// Example:
//
// // Combine validation errors from multiple decoders
// errorsMonoid := validation.GetMonoid[int]()
//
// // Decode multiple fields and combine errors
// result1 := decodeField1(data) // Validation[string]
// result2 := decodeField2(data) // Validation[int]
//
// // If both fail, errors are combined using the monoid
// combined := errorsMonoid.Concat(result1, result2)
//
// // The monoid's Empty() provides a successful validation with no errors
// empty := errorsMonoid.Empty() // Success with no value
Monoid[A any] = monoid.Monoid[A]
// Lazy represents a deferred computation that produces a value of type A.
// This is an alias for lazy.Lazy[A], which is func() A.
//
// In the decode context, Lazy is used to defer expensive computations or
// recursive decoder definitions until they are actually needed. This is
// particularly important for:
// - Recursive data structures (e.g., trees, linked lists)
// - Expensive default values
// - Breaking circular dependencies in decoder definitions
//
// A Lazy[A] is simply a function that takes no arguments and returns A.
// The computation is only executed when the function is called, allowing
// for lazy evaluation and recursive definitions.
//
// Example:
//
// // Define a recursive decoder for a tree structure
// type Tree struct {
// Value int
// Children []Tree
// }
//
// // Use Lazy to break the circular dependency
// var decodeTree Decode[map[string]any, Tree]
// decodeTree = func(data map[string]any) Validation[Tree] {
// // Lazy evaluation allows referencing decodeTree within itself
// childrenDecoder := Array(Lazy(func() Decode[map[string]any, Tree] {
// return decodeTree
// }))
// // ... rest of decoder implementation
// }
//
// // Lazy default value that's only computed if needed
// expensiveDefault := Lazy(func() Config {
// // This computation only runs if the decode fails
// return computeExpensiveDefaultConfig()
// })
Lazy[A any] = lazy.Lazy[A]
)

265
v2/optics/codec/either.go Normal file
View File

@@ -0,0 +1,265 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codec
import (
"fmt"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validate"
)
// encodeEither creates an encoder for Either[A, B] values.
//
// This function produces an encoder that handles both Left and Right cases of an Either value.
// It uses the provided codecs to encode the Left (A) and Right (B) values respectively.
//
// # Type Parameters
//
// - A: The type of the Left value
// - B: The type of the Right value
// - O: The output type after encoding
// - I: The input type for validation (not used in encoding)
//
// # Parameters
//
// - leftItem: The codec for encoding Left values of type A
// - rightItem: The codec for encoding Right values of type B
//
// # Returns
//
// An Encode function that takes an Either[A, B] and returns O by encoding
// either the Left or Right value using the appropriate codec.
//
// # Example
//
// stringCodec := String()
// intCodec := Int()
// encoder := encodeEither(stringCodec, intCodec)
//
// // Encode a Left value
// leftResult := encoder(either.Left[int]("error"))
// // leftResult contains the encoded string "error"
//
// // Encode a Right value
// rightResult := encoder(either.Right[string](42))
// // rightResult contains the encoded int 42
//
// # Notes
//
// - Uses either.Fold to pattern match on the Either value
// - Left values are encoded using leftItem.Encode
// - Right values are encoded using rightItem.Encode
func encodeEither[A, B, O, I any](
leftItem Type[A, O, I],
rightItem Type[B, O, I],
) Encode[either.Either[A, B], O] {
return either.Fold(
leftItem.Encode,
rightItem.Encode,
)
}
// validateEither creates a validator for Either[A, B] values.
//
// This function produces a validator that attempts to validate the input as both
// a Left (A) and Right (B) value. The validation strategy is:
// 1. First, try to validate as a Right value (B)
// 2. If Right validation succeeds, return Either.Right[A](B)
// 3. If Right validation fails, try to validate as a Left value (A)
// 4. If Left validation succeeds, return Either.Left[B](A)
// 5. If both validations fail, concatenate all errors from both attempts
//
// This approach ensures that the validator tries both branches and provides
// comprehensive error information when both fail.
//
// # Type Parameters
//
// - A: The type of the Left value
// - B: The type of the Right value
// - O: The output type after encoding (not used in validation)
// - I: The input type to validate
//
// # Parameters
//
// - leftItem: The codec for validating Left values of type A
// - rightItem: The codec for validating Right values of type B
//
// # Returns
//
// A Validate function that takes an input I and returns a Decode function.
// The Decode function takes a Context and returns a Validation[Either[A, B]].
//
// # Validation Logic
//
// The validator follows this decision tree:
//
// Input I
// |
// +--> Validate as Right (B)
// |
// +-- Success --> Return Either.Right[A](B)
// |
// +-- Failure --> Validate as Left (A)
// |
// +-- Success --> Return Either.Left[B](A)
// |
// +-- Failure --> Return all errors (Left + Right)
//
// # Example
//
// stringCodec := String()
// intCodec := Int()
// validator := validateEither(stringCodec, intCodec)
//
// // Validate a string (will succeed as Left)
// result1 := validator("hello")(validation.Context{})
// // result1 is Success(Either.Left[int]("hello"))
//
// // Validate an int (will succeed as Right)
// result2 := validator(42)(validation.Context{})
// // result2 is Success(Either.Right[string](42))
//
// // Validate something that's neither (will fail with both errors)
// result3 := validator([]int{1, 2, 3})(validation.Context{})
// // result3 is Failure with errors from both string and int validation
//
// # Notes
//
// - Prioritizes Right validation over Left validation
// - Accumulates errors from both branches when both fail
// - Uses the validation context to provide detailed error messages
// - The validator is lazy: it only evaluates Left if Right fails
func validateEither[A, B, O, I any](
leftItem Type[A, O, I],
rightItem Type[B, O, I],
) Validate[I, either.Either[A, B]] {
valRight := F.Pipe1(
rightItem.Validate,
validate.Map[I, B](either.Right[A]),
)
valLeft := F.Pipe1(
leftItem.Validate,
validate.Map[I, A](either.Left[B]),
)
return F.Pipe1(
valRight,
validate.Alt(lazy.Of(valLeft)),
)
}
// Either creates a codec for Either[A, B] values.
//
// This function constructs a complete codec that can encode, decode, and validate
// Either values. An Either represents a value that can be one of two types: Left (A)
// or Right (B). This is commonly used for error handling, where Left represents an
// error and Right represents a success value.
//
// The codec handles both branches of the Either type using the provided codecs for
// each branch. During validation, it attempts to validate the input as both types
// and succeeds if either validation passes.
//
// # Type Parameters
//
// - A: The type of the Left value
// - B: The type of the Right value
// - O: The output type after encoding
// - I: The input type for validation
//
// # Parameters
//
// - leftItem: The codec for handling Left values of type A
// - rightItem: The codec for handling Right values of type B
//
// # Returns
//
// A Type[either.Either[A, B], O, I] that can encode, decode, and validate Either values.
//
// # Codec Behavior
//
// Encoding:
// - Left values are encoded using leftItem.Encode
// - Right values are encoded using rightItem.Encode
//
// Validation:
// - First attempts to validate as Right (B)
// - If Right fails, attempts to validate as Left (A)
// - If both fail, returns all accumulated errors
// - If either succeeds, returns the corresponding Either value
//
// Type Checking:
// - Uses Is[either.Either[A, B]]() to verify the value is an Either
//
// Naming:
// - The codec name is "Either[<leftName>, <rightName>]"
// - Example: "Either[string, int]"
//
// # Example
//
// // Create a codec for Either[string, int]
// stringCodec := String()
// intCodec := Int()
// eitherCodec := Either(stringCodec, intCodec)
//
// // Encode a Left value
// leftEncoded := eitherCodec.Encode(either.Left[int]("error"))
// // leftEncoded contains the encoded string
//
// // Encode a Right value
// rightEncoded := eitherCodec.Encode(either.Right[string](42))
// // rightEncoded contains the encoded int
//
// // Decode/validate an input
// result := eitherCodec.Decode("hello")
// // result is Success(Either.Left[int]("hello"))
//
// result2 := eitherCodec.Decode(42)
// // result2 is Success(Either.Right[string](42))
//
// // Get the codec name
// name := eitherCodec.Name()
// // name is "Either[string, int]"
//
// # Use Cases
//
// - Error handling: Either[Error, Value]
// - Alternative values: Either[DefaultValue, CustomValue]
// - Union types: Either[TypeA, TypeB]
// - Validation results: Either[ValidationError, ValidatedValue]
//
// # Notes
//
// - The codec prioritizes Right validation over Left validation
// - Both branches must have compatible encoding output types (O)
// - Both branches must have compatible validation input types (I)
// - The codec name includes the names of both branch codecs
// - This is a building block for more complex sum types
func Either[A, B, O, I any](
leftItem Type[A, O, I],
rightItem Type[B, O, I],
) Type[either.Either[A, B], O, I] {
return MakeType(
fmt.Sprintf("Either[%s, %s]", leftItem.Name(), rightItem.Name()),
Is[either.Either[A, B]](),
validateEither(leftItem, rightItem),
encodeEither(leftItem, rightItem),
)
}

View File

@@ -0,0 +1,368 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package codec
import (
"fmt"
"strconv"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestEitherWithIdentityCodecs tests the Either function with identity codecs
// where both branches have the same output and input types
func TestEitherWithIdentityCodecs(t *testing.T) {
t.Run("creates codec with correct name", func(t *testing.T) {
// The Either function is designed for cases where both branches encode to the same type
// For example, both encode to string or both encode to JSON
// Create codecs that both encode to string
stringToString := Id[string]()
intToString := IntFromString()
eitherCodec := Either(stringToString, intToString)
assert.Equal(t, "Either[string, IntFromString]", eitherCodec.Name())
})
}
// TestEitherEncode tests encoding of Either values
func TestEitherEncode(t *testing.T) {
// Create codecs that both encode to string
stringToString := Id[string]()
intToString := IntFromString()
eitherCodec := Either(stringToString, intToString)
t.Run("encodes Left value", func(t *testing.T) {
leftValue := either.Left[int]("hello")
encoded := eitherCodec.Encode(leftValue)
assert.Equal(t, "hello", encoded)
})
t.Run("encodes Right value", func(t *testing.T) {
rightValue := either.Right[string](42)
encoded := eitherCodec.Encode(rightValue)
assert.Equal(t, "42", encoded)
})
}
// TestEitherDecode tests decoding/validation of Either values
func TestEitherDecode(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, either.Either[string, int]](either.Left[int]("")))
// Create codecs that both work with string input
stringCodec := Id[string]()
intFromString := IntFromString()
eitherCodec := Either(stringCodec, intFromString)
t.Run("decodes integer string as Right", func(t *testing.T) {
result := eitherCodec.Decode("42")
assert.True(t, either.IsRight(result), "should successfully decode integer string")
value := getOrElseNull(result)
assert.True(t, either.IsRight(value), "should be Right")
rightValue := either.MonadFold(value,
func(string) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, rightValue)
})
t.Run("decodes non-integer string as Left", func(t *testing.T) {
result := eitherCodec.Decode("hello")
assert.True(t, either.IsRight(result), "should successfully decode string")
value := getOrElseNull(result)
assert.True(t, either.IsLeft(value), "should be Left")
leftValue := either.MonadFold(value,
F.Identity[string],
func(int) string { return "" },
)
assert.Equal(t, "hello", leftValue)
})
}
// TestEitherValidation tests validation behavior
func TestEitherValidation(t *testing.T) {
t.Run("validates with custom codecs", func(t *testing.T) {
// Create a codec that only accepts non-empty strings
nonEmptyString := MakeType(
"NonEmptyString",
func(u any) either.Either[error, string] {
s, ok := u.(string)
if !ok || len(s) == 0 {
return either.Left[string](fmt.Errorf("not a non-empty string"))
}
return either.Of[error](s)
},
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if len(s) == 0 {
return validation.FailureWithMessage[string](s, "must not be empty")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
// Create a codec that only accepts positive integers from strings
positiveIntFromString := MakeType(
"PositiveInt",
func(u any) either.Either[error, int] {
i, ok := u.(int)
if !ok || i <= 0 {
return either.Left[int](fmt.Errorf("not a positive integer"))
}
return either.Of[error](i)
},
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
var n int
_, err := fmt.Sscanf(s, "%d", &n)
if err != nil {
return validation.FailureWithError[int](s, "expected integer string")(err)(c)
}
if n <= 0 {
return validation.FailureWithMessage[int](n, "must be positive")(c)
}
return validation.Success(n)
}
},
func(n int) string {
return fmt.Sprintf("%d", n)
},
)
eitherCodec := Either(nonEmptyString, positiveIntFromString)
// Valid non-empty string
validLeft := eitherCodec.Decode("hello")
assert.True(t, either.IsRight(validLeft))
// Valid positive integer
validRight := eitherCodec.Decode("42")
assert.True(t, either.IsRight(validRight))
// Invalid empty string - should fail both validations
invalidEmpty := eitherCodec.Decode("")
assert.True(t, either.IsLeft(invalidEmpty))
// Invalid zero - should fail Right validation, succeed as Left
zeroResult := eitherCodec.Decode("0")
// "0" is a valid non-empty string, so it should succeed as Left
assert.True(t, either.IsRight(zeroResult))
})
}
// TestEitherRoundTrip tests encoding and decoding round trips
func TestEitherRoundTrip(t *testing.T) {
stringCodec := Id[string]()
intFromString := IntFromString()
eitherCodec := Either(stringCodec, intFromString)
t.Run("round-trip Left value", func(t *testing.T) {
original := "hello"
// Decode
decodeResult := eitherCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.MonadFold(decodeResult,
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
F.Identity[either.Either[string, int]],
)
// Encode
encoded := eitherCodec.Encode(decoded)
// Verify
assert.Equal(t, original, encoded)
})
t.Run("round-trip Right value", func(t *testing.T) {
original := "42"
// Decode
decodeResult := eitherCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.MonadFold(decodeResult,
func(validation.Errors) either.Either[string, int] { return either.Right[string](0) },
F.Identity[either.Either[string, int]],
)
// Encode
encoded := eitherCodec.Encode(decoded)
// Verify
assert.Equal(t, original, encoded)
})
}
// TestEitherPrioritization tests that Right validation is prioritized over Left
func TestEitherPrioritization(t *testing.T) {
stringCodec := Id[string]()
intFromString := IntFromString()
eitherCodec := Either(stringCodec, intFromString)
t.Run("prioritizes Right over Left when both could succeed", func(t *testing.T) {
// "42" can be validated as both string (Left) and int (Right)
// The codec should prioritize Right
result := eitherCodec.Decode("42")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
F.Identity[either.Either[string, int]],
)
// Should be Right because int validation succeeds and is prioritized
assert.True(t, either.IsRight(value))
rightValue := either.MonadFold(value,
func(string) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, rightValue)
})
t.Run("falls back to Left when Right fails", func(t *testing.T) {
// "hello" can only be validated as string (Left), not as int (Right)
result := eitherCodec.Decode("hello")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) either.Either[string, int] { return either.Left[int]("") },
F.Identity[either.Either[string, int]],
)
// Should be Left because int validation failed
assert.True(t, either.IsLeft(value))
leftValue := either.MonadFold(value,
F.Identity[string],
func(int) string { return "" },
)
assert.Equal(t, "hello", leftValue)
})
}
// TestEitherErrorAccumulation tests that errors from both branches are accumulated
func TestEitherErrorAccumulation(t *testing.T) {
// Create codecs with specific validation rules that will both fail
nonEmptyString := MakeType(
"NonEmptyString",
func(u any) either.Either[error, string] {
s, ok := u.(string)
if !ok || len(s) == 0 {
return either.Left[string](fmt.Errorf("not a non-empty string"))
}
return either.Of[error](s)
},
func(s string) Decode[Context, string] {
return func(c Context) Validation[string] {
if len(s) == 0 {
return validation.FailureWithMessage[string](s, "must not be empty")(c)
}
return validation.Success(s)
}
},
F.Identity[string],
)
positiveIntFromString := MakeType(
"PositiveInt",
func(u any) either.Either[error, int] {
i, ok := u.(int)
if !ok || i <= 0 {
return either.Left[int](fmt.Errorf("not a positive integer"))
}
return either.Of[error](i)
},
func(s string) Decode[Context, int] {
return func(c Context) Validation[int] {
var n int
_, err := fmt.Sscanf(s, "%d", &n)
if err != nil {
return validation.FailureWithError[int](s, "expected integer string")(err)(c)
}
if n <= 0 {
return validation.FailureWithMessage[int](n, "must be positive")(c)
}
return validation.Success(n)
}
},
strconv.Itoa,
)
eitherCodec := Either(nonEmptyString, positiveIntFromString)
t.Run("accumulates errors from both branches when both fail", func(t *testing.T) {
// Empty string will fail both validations
result := eitherCodec.Decode("")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(either.Either[string, int]) validation.Errors { return nil },
)
require.NotNil(t, errors)
// Should have errors from both string and int validation attempts
assert.GreaterOrEqual(t, len(errors), 2, "Should have at least 2 errors (one from Right validation, one from Left validation)")
// Verify we have errors from both validation attempts
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
// Check that we have errors related to both validations
hasIntError := false
hasStringError := false
for _, msg := range messages {
if msg == "expected integer string" || msg == "must be positive" {
hasIntError = true
}
if msg == "must not be empty" {
hasStringError = true
}
}
assert.True(t, hasIntError, "Should have error from integer validation (Right branch)")
assert.True(t, hasStringError, "Should have error from string validation (Left branch)")
})
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/internal/formatting"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/decode"
"github.com/IBM/fp-go/v2/optics/codec/validate"
"github.com/IBM/fp-go/v2/optics/codec/validation"
@@ -40,6 +41,27 @@ type (
// Codec combines a Decoder and an Encoder for bidirectional transformations.
// It can decode input I to type A and encode type A to output O.
//
// This is a simple struct that pairs a decoder with an encoder, providing
// the basic building blocks for bidirectional data transformation. Unlike
// the Type interface, Codec is a concrete struct without validation context
// or type checking capabilities.
//
// Type Parameters:
// - I: The input type to decode from
// - O: The output type to encode to
// - A: The intermediate type (decoded to, encoded from)
//
// Fields:
// - Decode: A decoder that transforms I to A
// - Encode: An encoder that transforms A to O
//
// Example:
// A Codec[string, string, int] can decode strings to integers and
// encode integers back to strings.
//
// Note: For most use cases, prefer using the Type interface which provides
// additional validation and type checking capabilities.
Codec[I, O, A any] struct {
Decode decoder.Decoder[I, A]
Encode encoder.Encoder[O, A]
@@ -55,16 +77,82 @@ type (
// Validate is a function that validates input I to produce type A.
// It takes an input and returns a Reader that depends on the validation Context.
//
// The Validate type is the core validation abstraction, defined as:
// Reader[I, Decode[Context, A]]
//
// This means:
// 1. It takes an input of type I
// 2. Returns a Reader that depends on validation Context
// 3. That Reader produces a Validation[A] (Either[Errors, A])
//
// This layered structure allows validators to:
// - Access the input value
// - Track validation context (path in nested structures)
// - Accumulate multiple validation errors
// - Compose with other validators
//
// Example:
// A Validate[string, int] takes a string and returns a context-aware
// function that validates and converts it to an integer.
Validate[I, A any] = validate.Validate[I, A]
// Decode is a function that decodes input I to type A with validation.
// It returns a Validation result directly.
//
// The Decode type is defined as:
// Reader[I, Validation[A]]
//
// This is simpler than Validate as it doesn't require explicit context passing.
// The context is typically created automatically when the decoder is invoked.
//
// Decode is used when:
// - You don't need to manually manage validation context
// - You want a simpler API for basic validation
// - You're working at the top level of validation
//
// Example:
// A Decode[string, int] takes a string and returns a Validation[int]
// which is Either[Errors, int].
Decode[I, A any] = decode.Decode[I, A]
// Encode is a function that encodes type A to output O.
//
// Encode is simply a Reader[A, O], which is a function from A to O.
// Encoders are pure functions with no error handling - they assume
// the input is valid.
//
// Encoding is the inverse of decoding:
// - Decoding: I -> Validation[A] (may fail)
// - Encoding: A -> O (always succeeds)
//
// Example:
// An Encode[int, string] takes an integer and returns its string
// representation.
Encode[A, O any] = Reader[A, O]
// Decoder is an interface for types that can decode and validate input.
//
// A Decoder transforms input of type I into a validated value of type A,
// providing detailed error information when validation fails. It supports
// both context-aware validation (via Validate) and direct decoding (via Decode).
//
// Type Parameters:
// - I: The input type to decode from
// - A: The target type to decode to
//
// Methods:
// - Name(): Returns a descriptive name for this decoder (used in error messages)
// - Validate(I): Returns a context-aware validation function that can track
// the path through nested structures
// - Decode(I): Directly decodes input to a Validation result with a fresh context
//
// The Validate method is more flexible as it returns a Reader that can be called
// with different contexts, while Decode is a convenience method that creates a
// new context automatically.
//
// Example:
// A Decoder[string, int] can decode strings to integers with validation.
Decoder[I, A any] interface {
Name() string
Validate(I) Decode[Context, A]
@@ -72,13 +160,76 @@ type (
}
// Encoder is an interface for types that can encode values.
//
// An Encoder transforms values of type A into output format O. This is the
// inverse operation of decoding, allowing bidirectional transformations.
//
// Type Parameters:
// - A: The source type to encode from
// - O: The output type to encode to
//
// Methods:
// - Encode(A): Transforms a value of type A into output format O
//
// Encoders are pure functions with no validation or error handling - they
// assume the input is valid. Validation should be performed during decoding.
//
// Example:
// An Encoder[int, string] can encode integers to their string representation.
Encoder[A, O any] interface {
// Encode transforms a value of type A into output format O.
Encode(A) O
}
// Type is a bidirectional codec that combines encoding, decoding, validation,
// and type checking capabilities. It represents a complete specification of
// how to work with a particular type.
//
// Type is the central abstraction in the codec package, providing:
// - Decoding: Transform input I to validated type A
// - Encoding: Transform type A to output O
// - Validation: Context-aware validation with detailed error reporting
// - Type Checking: Runtime type verification via Is()
// - Formatting: Human-readable type descriptions via Name()
//
// Type Parameters:
// - A: The target type (what we decode to and encode from)
// - O: The output type (what we encode to)
// - I: The input type (what we decode from)
//
// Common patterns:
// - Type[A, A, A]: Identity codec (no transformation)
// - Type[A, string, string]: String-based serialization
// - Type[A, any, any]: Generic codec accepting any input/output
// - Type[A, JSON, JSON]: JSON codec
//
// Methods:
// - Name(): Returns the codec's descriptive name
// - Validate(I): Returns context-aware validation function
// - Decode(I): Decodes input with automatic context creation
// - Encode(A): Encodes value to output format
// - AsDecoder(): Returns this Type as a Decoder interface
// - AsEncoder(): Returns this Type as an Encoder interface
// - Is(any): Checks if a value can be converted to type A
//
// Example usage:
// intCodec := codec.Int() // Type[int, int, any]
// stringCodec := codec.String() // Type[string, string, any]
// intFromString := codec.IntFromString() // Type[int, string, string]
//
// // Decode
// result := intFromString.Decode("42") // Validation[int]
//
// // Encode
// str := intFromString.Encode(42) // "42"
//
// // Type check
// isInt := intCodec.Is(42) // Right(42)
// notInt := intCodec.Is("42") // Left(error)
//
// Composition:
// Types can be composed using operators like Alt, Map, Chain, and Pipe
// to build complex codecs from simpler ones.
Type[A, O, I any] interface {
Formattable
Decoder[I, A]
@@ -99,6 +250,92 @@ type (
// contain a value of type A. It provides a way to preview and review values.
Prism[S, A any] = prism.Prism[S, A]
// Refinement represents the concept that B is a specialized type of A
// Refinement represents the concept that B is a specialized type of A.
// It's an alias for Prism[A, B], providing a semantic name for type refinement operations.
//
// A refinement allows you to:
// - Preview: Try to extract a B from an A (may fail if A is not a B)
// - Review: Inject a B back into an A
//
// This is useful for working with subtypes, validated types, or constrained types.
//
// Example:
// - Refinement[int, PositiveInt] - refines int to positive integers only
// - Refinement[string, NonEmptyString] - refines string to non-empty strings
// - Refinement[any, User] - refines any to User type
Refinement[A, B any] = Prism[A, B]
// Kleisli represents a Kleisli arrow in the codec context.
// It's a function that takes a value of type A and returns a codec Type[B, O, I].
//
// This is the fundamental building block for codec transformations and compositions.
// Kleisli arrows allow you to:
// - Chain codec operations
// - Build dependent codecs (where the next codec depends on the previous result)
// - Create codec pipelines
//
// Type Parameters:
// - A: The input type to the function
// - B: The target type that the resulting codec decodes to
// - O: The output type that the resulting codec encodes to
// - I: The input type that the resulting codec decodes from
//
// Example:
// A Kleisli[string, int, string, string] takes a string and returns a codec
// that can decode strings to ints and encode ints to strings.
Kleisli[A, B, O, I any] = Reader[A, Type[B, O, I]]
// Operator is a specialized Kleisli arrow that transforms codecs.
// It takes a codec Type[A, O, I] and returns a new codec Type[B, O, I].
//
// Operators are the primary way to build codec transformation pipelines.
// They enable functional composition of codec transformations using F.Pipe.
//
// Type Parameters:
// - A: The source type that the input codec decodes to
// - B: The target type that the output codec decodes to
// - O: The output type (same for both input and output codecs)
// - I: The input type (same for both input and output codecs)
//
// Common operators include:
// - Map: Transforms the decoded value
// - Chain: Sequences dependent codec operations
// - Alt: Provides alternative fallback codecs
// - Refine: Adds validation constraints
//
// Example:
// An Operator[int, PositiveInt, int, any] transforms a codec that decodes
// to int into a codec that decodes to PositiveInt (with validation).
//
// Usage with F.Pipe:
// codec := F.Pipe2(
// baseCodec,
// operator1, // Operator[A, B, O, I]
// operator2, // Operator[B, C, O, I]
// )
Operator[A, B, O, I any] = Kleisli[Type[A, O, I], B, O, I]
// Monoid represents an algebraic structure with an associative binary operation
// and an identity element.
//
// A Monoid[A] provides:
// - Empty(): Returns the identity element
// - Concat(A, A): Combines two values associatively
//
// Monoid laws:
// 1. Left Identity: Concat(Empty(), a) = a
// 2. Right Identity: Concat(a, Empty()) = a
// 3. Associativity: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
//
// In the codec context, monoids are used to:
// - Combine multiple codecs with specific semantics
// - Build codec chains with fallback behavior (AltMonoid)
// - Aggregate validation results (ApplicativeMonoid)
// - Compose codec transformations
//
// Example monoids for codecs:
// - AltMonoid: First success wins (alternative semantics)
// - ApplicativeMonoid: Combines successful results using inner monoid
// - AlternativeMonoid: Combines applicative and alternative behaviors
Monoid[A any] = monoid.Monoid[A]
)

View File

@@ -0,0 +1,335 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validate
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type S to be used with the Bind operation.
// This is the starting point for building up a context using do-notation style.
//
// Example:
//
// type Result struct {
// x int
// y string
// }
// result := Do(Result{})
func Do[I, S any](
empty S,
) Validate[I, S] {
return Of[I](empty)
}
// Bind attaches the result of a computation to a context S1 to produce a context S2.
// This is used in do-notation style to sequentially build up a context.
//
// Example:
//
// type State struct { x int; y int }
// decoder := F.Pipe2(
// Do[string](State{}),
// Bind(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, func(s State) Validate[string, int] {
// return Of[string](42)
// }),
// )
// result := decoder("input") // Returns validation.Success(State{x: 42})
func Bind[I, S1, S2, A any](
setter func(A) func(S1) S2,
f Kleisli[I, S1, A],
) Operator[I, S1, S2] {
return C.Bind(
Chain[I, S1, S2],
Map[I, A, S2],
setter,
f,
)
}
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Unlike Bind, the computation function returns a plain value, not wrapped in Validate.
//
// Example:
//
// type State struct { x int; computed int }
// decoder := F.Pipe2(
// Do[string](State{x: 5}),
// Let[string](func(c int) func(State) State {
// return func(s State) State { s.computed = c; return s }
// }, func(s State) int { return s.x * 2 }),
// )
// result := decoder("input") // Returns validation.Success(State{x: 5, computed: 10})
func Let[I, S1, S2, B any](
key func(B) func(S1) S2,
f func(S1) B,
) Operator[I, S1, S2] {
return F.Let(
Map[I, S1, S2],
key,
f,
)
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
//
// Example:
//
// type State struct { x int; name string }
// result := F.Pipe2(
// Do(State{x: 5}),
// LetTo(func(n string) func(State) State {
// return func(s State) State { s.name = n; return s }
// }, "example"),
// )
func LetTo[I, S1, S2, B any](
key func(B) func(S1) S2,
b B,
) Operator[I, S1, S2] {
return F.LetTo(
Map[I, S1, S2],
key,
b,
)
}
// BindTo initializes a new state S1 from a value T.
// This is typically used as the first operation after creating a Validate value.
//
// Example:
//
// type State struct { value int }
// decoder := F.Pipe1(
// Of[string](42),
// BindTo[string](func(x int) State { return State{value: x} }),
// )
// result := decoder("input") // Returns validation.Success(State{value: 42})
func BindTo[I, S1, T any](
setter func(T) S1,
) Operator[I, T, S1] {
return C.BindTo(
Map[I, T, S1],
setter,
)
}
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
// This uses the applicative functor pattern, allowing parallel composition.
//
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
// and the value. If both validations fail, all errors are collected and returned together.
// This is useful for validating multiple independent fields and reporting all errors at once.
//
// Example:
//
// type State struct { x int; y int }
// decoder := F.Pipe2(
// Do[string](State{}),
// ApS(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, Of[string](42)),
// )
// result := decoder("input") // Returns validation.Success(State{x: 42})
//
// Error aggregation example:
//
// // Both decoders fail - errors are aggregated
// decoder1 := func(input string) Validation[State] {
// return validation.Failures[State](/* errors */)
// }
// decoder2 := func(input string) Validation[int] {
// return validation.Failures[int](/* errors */)
// }
// combined := ApS(setter, decoder2)(decoder1)
// result := combined("input") // Contains BOTH sets of errors
func ApS[I, S1, S2, T any](
setter func(T) func(S1) S2,
fa Validate[I, T],
) Operator[I, S1, S2] {
return A.ApS(
Ap[S2, I, T],
Map[I, S1, func(T) S2],
setter,
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
// and the value fail validation, all errors are collected and returned together.
// This enables comprehensive error reporting for complex nested structures.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type Address struct {
// Street string
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Create a lens for the Address field
// addressLens := lens.MakeLens(
// func(p Person) Address { return p.Address },
// func(p Person, a Address) Person { p.Address = a; return p },
// )
//
// // Use ApSL to update the address
// decoder := F.Pipe2(
// Of[string](Person{Name: "Alice"}),
// ApSL(
// addressLens,
// Of[string](Address{Street: "Main St", City: "NYC"}),
// ),
// )
// result := decoder("input") // Returns validation.Success(Person{...})
func ApSL[I, S, T any](
lens L.Lens[S, T],
fa Validate[I, T],
) Operator[I, S, S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// a Validation that produces the new value.
//
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
// the current value of the focused field.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but fail if it would exceed 100
// increment := func(v int) Validate[string, int] {
// return func(input string) Validation[int] {
// if v >= 100 {
// return validation.Failures[int](/* errors */)
// }
// return validation.Success(v + 1)
// }
// }
//
// decoder := F.Pipe1(
// Of[string](Counter{Value: 42}),
// BindL(valueLens, increment),
// )
// result := decoder("input") // Returns validation.Success(Counter{Value: 43})
func BindL[I, S, T any](
lens L.Lens[S, T],
f Kleisli[I, T, T],
) Operator[I, S, S] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in Validation).
//
// This is useful for pure transformations that cannot fail, such as mathematical operations,
// string manipulations, or other deterministic updates.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// decoder := F.Pipe1(
// Of[string](Counter{Value: 21}),
// LetL(valueLens, double),
// )
// result := decoder("input") // Returns validation.Success(Counter{Value: 42})
func LetL[I, S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[I, S, S] {
return Let[I](lens.Set, function.Flow2(lens.Get, f))
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// This is useful for resetting fields, initializing values, or setting fields to
// predetermined constants.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// decoder := F.Pipe1(
// Of[string](Config{Debug: true, Timeout: 30}),
// LetToL(debugLens, false),
// )
// result := decoder("input") // Returns validation.Success(Config{Debug: false, Timeout: 30})
func LetToL[I, S, T any](
lens L.Lens[S, T],
b T,
) Operator[I, S, S] {
return LetTo[I](lens.Set, b)
}

View File

@@ -0,0 +1,733 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validate
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
func TestDo(t *testing.T) {
t.Run("creates successful validation with empty state", func(t *testing.T) {
type State struct {
x int
y string
}
validator := Do[string](State{})
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](State{}), result)
})
t.Run("creates successful validation with initialized state", func(t *testing.T) {
type State struct {
x int
y string
}
initial := State{x: 42, y: "hello"}
validator := Do[string](initial)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](initial), result)
})
t.Run("works with different input types", func(t *testing.T) {
intValidator := Do[int](0)
assert.Equal(t, either.Of[Errors](0), intValidator(42)(nil))
strValidator := Do[string]("")
assert.Equal(t, either.Of[Errors](""), strValidator("test")(nil))
type Custom struct{ Value int }
customValidator := Do[[]byte](Custom{Value: 100})
assert.Equal(t, either.Of[Errors](Custom{Value: 100}), customValidator([]byte("data"))(nil))
})
}
func TestBind(t *testing.T) {
type State struct {
x int
y int
}
t.Run("binds successful validation to state", func(t *testing.T) {
validator := F.Pipe2(
Do[string](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Validate[string, int] {
return Of[string](42)
}),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Validate[string, int] {
return Of[string](10)
}),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](State{x: 42, y: 10}), result)
})
t.Run("propagates failure", func(t *testing.T) {
validator := F.Pipe2(
Do[string](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Validate[string, int] {
return Of[string](42)
}),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "y failed"}})
}
}
}),
)
result := validator("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "y failed", errors[0].Messsage)
})
t.Run("can access previous state values", func(t *testing.T) {
validator := F.Pipe2(
Do[string](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Validate[string, int] {
return Of[string](10)
}),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Validate[string, int] {
// y depends on x
return Of[string](s.x * 2)
}),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](State{x: 10, y: 20}), result)
})
t.Run("can access input value", func(t *testing.T) {
validator := F.Pipe1(
Do[int](State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Validate[int, int] {
return func(input int) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return validation.Success(input * 2)
}
}
}),
)
result := validator(21)(nil)
assert.Equal(t, either.Of[Errors](State{x: 42}), result)
})
}
func TestLet(t *testing.T) {
type State struct {
x int
computed int
}
t.Run("attaches pure computation result to state", func(t *testing.T) {
validator := F.Pipe1(
Do[string](State{x: 5}),
Let[string](func(c int) func(State) State {
return func(s State) State { s.computed = c; return s }
}, func(s State) int { return s.x * 2 }),
)
result := validator("input")(nil)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 5, computed: 10}, value)
})
t.Run("preserves failure", func(t *testing.T) {
failure := func(input string) Reader[Context, Validation[State]] {
return func(ctx Context) Validation[State] {
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "error"}})
}
}
validator := Let[string](func(c int) func(State) State {
return func(s State) State { s.computed = c; return s }
}, func(s State) int { return s.x * 2 })
result := validator(failure)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "error", errors[0].Messsage)
})
t.Run("chains multiple Let operations", func(t *testing.T) {
type State struct {
x int
y int
z int
}
validator := F.Pipe3(
Do[string](State{x: 5}),
Let[string](func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) int { return s.x * 2 }),
Let[string](func(z int) func(State) State {
return func(s State) State { s.z = z; return s }
}, func(s State) int { return s.y + 10 }),
Let[string](func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) int { return s.z * 3 }),
)
result := validator("input")(nil)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
})
}
func TestLetTo(t *testing.T) {
type State struct {
x int
name string
}
t.Run("attaches constant value to state", func(t *testing.T) {
validator := F.Pipe1(
Do[string](State{x: 5}),
LetTo[string](func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "example"),
)
result := validator("input")(nil)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{x: 5, name: "example"}, value)
})
t.Run("preserves failure", func(t *testing.T) {
failure := func(input string) Reader[Context, Validation[State]] {
return func(ctx Context) Validation[State] {
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "error"}})
}
}
validator := LetTo[string](func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "example")
result := validator(failure)("input")(nil)
assert.True(t, either.IsLeft(result))
})
t.Run("sets multiple constant values", func(t *testing.T) {
type State struct {
name string
version int
active bool
}
validator := F.Pipe3(
Do[string](State{}),
LetTo[string](func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "app"),
LetTo[string](func(v int) func(State) State {
return func(s State) State { s.version = v; return s }
}, 2),
LetTo[string](func(a bool) func(State) State {
return func(s State) State { s.active = a; return s }
}, true),
)
result := validator("input")(nil)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{name: "app", version: 2, active: true}, value)
})
}
func TestBindTo(t *testing.T) {
type State struct {
value int
}
t.Run("initializes state from value", func(t *testing.T) {
validator := F.Pipe1(
Of[string](42),
BindTo[string](func(x int) State { return State{value: x} }),
)
result := validator("input")(nil)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) State { return State{} },
F.Identity[State],
)
assert.Equal(t, State{value: 42}, value)
})
t.Run("preserves failure", func(t *testing.T) {
failure := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "error"}})
}
}
validator := BindTo[string](func(x int) State { return State{value: x} })
result := validator(failure)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "error", errors[0].Messsage)
})
t.Run("works with different types", func(t *testing.T) {
type StringState struct {
text string
}
validator := F.Pipe1(
Of[int]("hello"),
BindTo[int](func(s string) StringState { return StringState{text: s} }),
)
result := validator(42)(nil)
assert.Equal(t, either.Of[Errors](StringState{text: "hello"}), result)
})
}
func TestApS(t *testing.T) {
type State struct {
x int
y int
}
t.Run("attaches value using applicative pattern", func(t *testing.T) {
validator := F.Pipe1(
Do[string](State{}),
ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, Of[string](42)),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](State{x: 42}), result)
})
t.Run("accumulates errors from both validations", func(t *testing.T) {
stateFailure := func(input string) Reader[Context, Validation[State]] {
return func(ctx Context) Validation[State] {
return validation.Failures[State](Errors{&validation.ValidationError{Messsage: "state error"}})
}
}
valueFailure := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "value error"}})
}
}
validator := ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, valueFailure)
result := validator(stateFailure)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "state error")
assert.Contains(t, messages, "value error")
})
t.Run("combines multiple ApS operations", func(t *testing.T) {
validator := F.Pipe2(
Do[string](State{}),
ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, Of[string](10)),
ApS(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, Of[string](20)),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](State{x: 10, y: 20}), result)
})
}
func TestApSL(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
t.Run("updates nested structure using lens", func(t *testing.T) {
addressLens := L.MakeLens(
func(p Person) Address { return p.Address },
func(p Person, a Address) Person { p.Address = a; return p },
)
validator := F.Pipe1(
Of[string](Person{Name: "Alice"}),
ApSL(
addressLens,
Of[string](Address{Street: "Main St", City: "NYC"}),
),
)
result := validator("input")(nil)
expected := Person{
Name: "Alice",
Address: Address{Street: "Main St", City: "NYC"},
}
assert.Equal(t, either.Of[Errors](expected), result)
})
t.Run("accumulates errors", func(t *testing.T) {
addressLens := L.MakeLens(
func(p Person) Address { return p.Address },
func(p Person, a Address) Person { p.Address = a; return p },
)
personFailure := func(input string) Reader[Context, Validation[Person]] {
return func(ctx Context) Validation[Person] {
return validation.Failures[Person](Errors{&validation.ValidationError{Messsage: "person error"}})
}
}
addressFailure := func(input string) Reader[Context, Validation[Address]] {
return func(ctx Context) Validation[Address] {
return validation.Failures[Address](Errors{&validation.ValidationError{Messsage: "address error"}})
}
}
validator := ApSL(addressLens, addressFailure)
result := validator(personFailure)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(Person) Errors { return nil },
)
assert.Len(t, errors, 2)
})
}
func TestBindL(t *testing.T) {
type Counter struct {
Value int
}
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("updates field based on current value", func(t *testing.T) {
increment := func(v int) Validate[string, int] {
return Of[string](v + 1)
}
validator := F.Pipe1(
Of[string](Counter{Value: 42}),
BindL(valueLens, increment),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](Counter{Value: 43}), result)
})
t.Run("fails validation based on current value", func(t *testing.T) {
increment := func(v int) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
if v >= 100 {
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "exceeds limit"}})
}
return validation.Success(v + 1)
}
}
}
validator := F.Pipe1(
Of[string](Counter{Value: 100}),
BindL(valueLens, increment),
)
result := validator("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(Counter) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "exceeds limit", errors[0].Messsage)
})
t.Run("preserves failure", func(t *testing.T) {
increment := func(v int) Validate[string, int] {
return Of[string](v + 1)
}
failure := func(input string) Reader[Context, Validation[Counter]] {
return func(ctx Context) Validation[Counter] {
return validation.Failures[Counter](Errors{&validation.ValidationError{Messsage: "error"}})
}
}
validator := BindL(valueLens, increment)
result := validator(failure)("input")(nil)
assert.True(t, either.IsLeft(result))
})
}
func TestLetL(t *testing.T) {
type Counter struct {
Value int
}
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("transforms field with pure function", func(t *testing.T) {
double := func(v int) int { return v * 2 }
validator := F.Pipe1(
Of[string](Counter{Value: 21}),
LetL[string](valueLens, double),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](Counter{Value: 42}), result)
})
t.Run("preserves failure", func(t *testing.T) {
double := func(v int) int { return v * 2 }
failure := func(input string) Reader[Context, Validation[Counter]] {
return func(ctx Context) Validation[Counter] {
return validation.Failures[Counter](Errors{&validation.ValidationError{Messsage: "error"}})
}
}
validator := LetL[string](valueLens, double)
result := validator(failure)("input")(nil)
assert.True(t, either.IsLeft(result))
})
t.Run("chains multiple transformations", func(t *testing.T) {
add10 := func(v int) int { return v + 10 }
double := func(v int) int { return v * 2 }
validator := F.Pipe2(
Of[string](Counter{Value: 5}),
LetL[string](valueLens, add10),
LetL[string](valueLens, double),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](Counter{Value: 30}), result)
})
}
func TestLetToL(t *testing.T) {
type Config struct {
Debug bool
Timeout int
}
debugLens := L.MakeLens(
func(c Config) bool { return c.Debug },
func(c Config, d bool) Config { c.Debug = d; return c },
)
t.Run("sets field to constant value", func(t *testing.T) {
validator := F.Pipe1(
Of[string](Config{Debug: true, Timeout: 30}),
LetToL[string](debugLens, false),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](Config{Debug: false, Timeout: 30}), result)
})
t.Run("preserves failure", func(t *testing.T) {
failure := func(input string) Reader[Context, Validation[Config]] {
return func(ctx Context) Validation[Config] {
return validation.Failures[Config](Errors{&validation.ValidationError{Messsage: "error"}})
}
}
validator := LetToL[string](debugLens, false)
result := validator(failure)("input")(nil)
assert.True(t, either.IsLeft(result))
})
t.Run("sets multiple fields", func(t *testing.T) {
timeoutLens := L.MakeLens(
func(c Config) int { return c.Timeout },
func(c Config, t int) Config { c.Timeout = t; return c },
)
validator := F.Pipe2(
Of[string](Config{Debug: true, Timeout: 30}),
LetToL[string](debugLens, false),
LetToL[string](timeoutLens, 60),
)
result := validator("input")(nil)
assert.Equal(t, either.Of[Errors](Config{Debug: false, Timeout: 60}), result)
})
}
func TestBindOperationsComposition(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
validator := F.Pipe4(
Do[string](User{}),
LetTo[string](func(n string) func(User) User {
return func(u User) User { u.Name = n; return u }
}, "Alice"),
Bind(func(a int) func(User) User {
return func(u User) User { u.Age = a; return u }
}, func(u User) Validate[string, int] {
// Age validation
if len(u.Name) > 0 {
return Of[string](25)
}
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "name required"}})
}
}
}),
Let[string](func(e string) func(User) User {
return func(u User) User { u.Email = e; return u }
}, func(u User) string {
// Derive email from name
return u.Name + "@example.com"
}),
Bind(func(a int) func(User) User {
return func(u User) User { u.Age = a; return u }
}, func(u User) Validate[string, int] {
// Validate age is positive
if u.Age > 0 {
return Of[string](u.Age)
}
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "age must be positive"}})
}
}
}),
)
result := validator("input")(nil)
expected := User{
Name: "Alice",
Age: 25,
Email: "Alice@example.com",
}
assert.Equal(t, either.Of[Errors](expected), result)
})
t.Run("validates with input-dependent logic", func(t *testing.T) {
type Config struct {
MaxValue int
Value int
}
validator := F.Pipe2(
Do[int](Config{}),
Bind(func(max int) func(Config) Config {
return func(c Config) Config { c.MaxValue = max; return c }
}, func(c Config) Validate[int, int] {
// Extract max from input
return func(input int) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return validation.Success(input)
}
}
}),
Bind(func(val int) func(Config) Config {
return func(c Config) Config { c.Value = val; return c }
}, func(c Config) Validate[int, int] {
// Validate value against max
return func(input int) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
if input/2 <= c.MaxValue {
return validation.Success(input / 2)
}
return validation.Failures[int](Errors{&validation.ValidationError{Messsage: "value exceeds max"}})
}
}
}),
)
result := validator(100)(nil)
assert.Equal(t, either.Of[Errors](Config{MaxValue: 100, Value: 50}), result)
})
}

View File

@@ -0,0 +1,661 @@
package validate
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
// TestMonadChainLeft tests the MonadChainLeft function
func TestMonadChainLeft(t *testing.T) {
t.Run("transforms failures while preserving successes", func(t *testing.T) {
// Create a failing validator
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "validation failed"},
})
}
}
// Handler that recovers from specific errors
handler := func(errs Errors) Validate[string, int] {
for _, err := range errs {
if err.Messsage == "validation failed" {
return Of[string, int](0) // recover with default
}
}
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](errs)
}
}
}
validator := MonadChainLeft(failingValidator, handler)
res := validator("input")(nil)
assert.Equal(t, validation.Of(0), res, "Should recover from failure")
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[string, int](42)
handler := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "should not be called"},
})
}
}
}
validator := MonadChainLeft(successValidator, handler)
res := validator("input")(nil)
assert.Equal(t, validation.Of(42), res, "Success should pass through unchanged")
})
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "original error"},
})
}
}
handler := func(errs Errors) Validate[string, string] {
return func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Messsage: "additional error"},
})
}
}
}
validator := MonadChainLeft(failingValidator, handler)
res := validator("input")(nil)
assert.True(t, either.IsLeft(res))
errors := either.MonadFold(res,
reader.Ask[Errors](),
func(string) Errors { return nil },
)
assert.Len(t, errors, 2, "Should aggregate both errors")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "original error")
assert.Contains(t, messages, "additional error")
})
t.Run("adds context to errors", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "invalid format"},
})
}
}
addContext := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
Messsage: "failed to validate user age",
},
})
}
}
}
validator := MonadChainLeft(failingValidator, addContext)
res := validator("abc")(nil)
assert.True(t, either.IsLeft(res))
errors := either.MonadFold(res,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.Len(t, errors, 2, "Should have both original and context errors")
})
t.Run("works with different input types", func(t *testing.T) {
type Config struct {
Port int
}
failingValidator := func(cfg Config) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: cfg.Port, Messsage: "invalid port"},
})
}
}
handler := func(errs Errors) Validate[Config, string] {
return Of[Config, string]("default-value")
}
validator := MonadChainLeft(failingValidator, handler)
res := validator(Config{Port: 9999})(nil)
assert.Equal(t, validation.Of("default-value"), res)
})
t.Run("handler can access original input", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "parse failed"},
})
}
}
handler := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
// Handler can use the original input to make decisions
if input == "special" {
return validation.Of(999)
}
return validation.Of(0)
}
}
}
validator := MonadChainLeft(failingValidator, handler)
res1 := validator("special")(nil)
assert.Equal(t, validation.Of(999), res1)
res2 := validator("other")(nil)
assert.Equal(t, validation.Of(0), res2)
})
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error"},
})
}
}
handler := func(errs Errors) Validate[string, int] {
return Of[string, int](42)
}
// MonadChainLeft - direct application
result1 := MonadChainLeft(failingValidator, handler)("input")(nil)
// ChainLeft - curried for pipelines
result2 := ChainLeft(handler)(failingValidator)("input")(nil)
assert.Equal(t, result1, result2, "MonadChainLeft and ChainLeft should produce identical results")
})
t.Run("chains multiple error transformations", func(t *testing.T) {
failingValidator := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error1"},
})
}
}
handler1 := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "error2"},
})
}
}
}
handler2 := func(errs Errors) Validate[string, int] {
// Check if we can recover
for _, err := range errs {
if err.Messsage == "error1" {
return Of[string, int](100) // recover
}
}
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](errs)
}
}
}
// Chain handlers
validator := MonadChainLeft(MonadChainLeft(failingValidator, handler1), handler2)
res := validator("input")(nil)
// Should recover because error1 is present
assert.Equal(t, validation.Of(100), res)
})
t.Run("does not call handler on success", func(t *testing.T) {
successValidator := Of[string, int](42)
handlerCalled := false
handler := func(errs Errors) Validate[string, int] {
handlerCalled = true
return Of[string, int](0)
}
validator := MonadChainLeft(successValidator, handler)
res := validator("input")(nil)
assert.Equal(t, validation.Of(42), res)
assert.False(t, handlerCalled, "Handler should not be called on success")
})
}
// TestMonadAlt tests the MonadAlt function
func TestMonadAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("returns second validator when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
result := MonadAlt(failing, fallback)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
result := MonadAlt(failing1, failing2)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1", "Should contain error from first validator")
assert.Contains(t, messages, "error 2", "Should contain error from second validator")
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
assert.Equal(t, validation.Of(42), result)
assert.False(t, evaluated, "Second validator should not be evaluated")
})
t.Run("works with different types", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
fallback := func() Validate[string, string] {
return Of[string, string]("fallback")
}
result := MonadAlt(failing, fallback)("input")(nil)
assert.Equal(t, validation.Of("fallback"), result)
})
t.Run("chains multiple alternatives", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
}
// Chain: try failing1, then failing2, then succeeding
result := MonadAlt(MonadAlt(failing1, failing2), succeeding)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("works with complex input types", func(t *testing.T) {
type Config struct {
Port int
}
failing := func(cfg Config) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: cfg.Port, Messsage: "invalid port"},
})
}
}
fallback := func() Validate[Config, string] {
return Of[Config, string]("default")
}
result := MonadAlt(failing, fallback)(Config{Port: 9999})(nil)
assert.Equal(t, validation.Of("default"), result)
})
t.Run("preserves error context", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "parse error",
Context: validation.Context{{Key: "field", Type: "int"}},
},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{
Value: input,
Messsage: "validation error",
Context: validation.Context{{Key: "value", Type: "int"}},
},
})
}
}
}
result := MonadAlt(failing1, failing2)("abc")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should have errors from both validators")
// Verify that errors with context are present
hasParseError := false
hasValidationError := false
for _, err := range errors {
if err.Messsage == "parse error" {
hasParseError = true
assert.NotNil(t, err.Context)
}
if err.Messsage == "validation error" {
hasValidationError = true
assert.NotNil(t, err.Context)
}
}
assert.True(t, hasParseError, "Should have parse error")
assert.True(t, hasValidationError, "Should have validation error")
})
}
// TestAlt tests the Alt function
func TestAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
}
withAlt := Alt(validator2)
result := withAlt(validator1)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("returns second validator when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
withAlt := Alt(fallback)
result := withAlt(failing)("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
withAlt := Alt(failing2)
result := withAlt(failing1)("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
}
withAlt := Alt(validator2)
result := withAlt(validator1)("input")(nil)
assert.Equal(t, validation.Of(42), result)
assert.False(t, evaluated, "Second validator should not be evaluated")
})
t.Run("can be used in pipelines", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
}
// Use F.Pipe to chain alternatives
validator := F.Pipe2(
failing1,
Alt(failing2),
Alt(succeeding),
)
result := validator("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("is equivalent to MonadAlt", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
// Alt - curried for pipelines
result1 := Alt(fallback)(failing)("input")(nil)
// MonadAlt - direct application
result2 := MonadAlt(failing, fallback)("input")(nil)
assert.Equal(t, result1, result2, "Alt and MonadAlt should produce identical results")
})
}
// TestMonadAltAndAltEquivalence tests that MonadAlt and Alt are equivalent
func TestMonadAltAndAltEquivalence(t *testing.T) {
t.Run("both produce same results for success", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
}
resultMonadAlt := MonadAlt(validator1, validator2)("input")(nil)
resultAlt := Alt(validator2)(validator1)("input")(nil)
assert.Equal(t, resultMonadAlt, resultAlt)
})
t.Run("both produce same results for fallback", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
}
resultMonadAlt := MonadAlt(failing, fallback)("input")(nil)
resultAlt := Alt(fallback)(failing)("input")(nil)
assert.Equal(t, resultMonadAlt, resultAlt)
})
t.Run("both produce same results for error aggregation", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
resultMonadAlt := MonadAlt(failing1, failing2)("input")(nil)
resultAlt := Alt(failing2)(failing1)("input")(nil)
// Both should fail
assert.True(t, either.IsLeft(resultMonadAlt))
assert.True(t, either.IsLeft(resultAlt))
// Both should have same errors
errorsMonadAlt := either.MonadFold(resultMonadAlt,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
errorsAlt := either.MonadFold(resultAlt,
reader.Ask[Errors](),
func(int) Errors { return nil },
)
assert.Equal(t, len(errorsMonadAlt), len(errorsAlt))
})
}

View File

@@ -122,3 +122,268 @@ func ApplicativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
m,
)
}
// AlternativeMonoid creates a Monoid instance for Validate[I, A] that combines both
// applicative and alternative semantics.
//
// This function creates a monoid that:
// 1. When both validators succeed: Combines their results using the provided monoid operation
// 2. When one validator fails: Uses the successful validator's result (alternative behavior)
// 3. When both validators fail: Aggregates all errors from both validators
//
// This is a hybrid approach that combines:
// - ApplicativeMonoid: Combines successful results using the monoid operation
// - AltMonoid: Provides fallback behavior when validators fail
//
// # Type Parameters
//
// - I: The input type that validators accept
// - A: The output type that validators produce (must have a Monoid instance)
//
// # Parameters
//
// - m: A Monoid[A] that defines how to combine values of type A
//
// # Returns
//
// A Monoid[Validate[I, A]] that combines validators using both applicative and alternative semantics.
//
// # Behavior Details
//
// The AlternativeMonoid differs from ApplicativeMonoid in how it handles mixed success/failure:
//
// - **Both succeed**: Results are combined using the monoid operation (like ApplicativeMonoid)
// - **First succeeds, second fails**: Returns the first result (alternative fallback)
// - **First fails, second succeeds**: Returns the second result (alternative fallback)
// - **Both fail**: Aggregates errors from both validators
//
// # Example: String Concatenation with Fallback
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// m := validate.AlternativeMonoid[string, string](S.Monoid)
//
// // Both succeed - results are concatenated
// validator1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success("Hello")
// }
// }
// validator2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success(" World")
// }
// }
// combined := m.Concat(validator1, validator2)
// result := combined("input")(nil)
// // result is validation.Success("Hello World")
//
// # Example: Fallback Behavior
//
// // First fails, second succeeds - uses second result
// failing := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "first failed")(ctx)
// }
// }
// succeeding := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success("fallback")
// }
// }
// combined := m.Concat(failing, succeeding)
// result := combined("input")(nil)
// // result is validation.Success("fallback")
//
// # Example: Error Aggregation
//
// // Both fail - errors are aggregated
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "error 1")(ctx)
// }
// }
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "error 2")(ctx)
// }
// }
// combined := m.Concat(failing1, failing2)
// result := combined("input")(nil)
// // result contains both "error 1" and "error 2"
//
// # Comparison with Other Monoids
//
// - **ApplicativeMonoid**: Always combines results when both succeed, fails if either fails
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
// - **AltMonoid**: Always uses first success, never combines results
//
// # Use Cases
//
// - Validation with fallback strategies and result combination
// - Building validators that accumulate results but provide alternatives
// - Configuration loading with multiple sources and merging
// - Data aggregation with error recovery
//
// # Notes
//
// - Both validators receive the same input value I
// - The empty element of the monoid serves as the identity for the Concat operation
// - Error aggregation ensures no validation failures are lost
// - This follows both applicative and alternative functor laws
//
// # See Also
//
// - ApplicativeMonoid: For pure applicative combination without fallback
// - AltMonoid: For pure alternative behavior without result combination
// - MonadAlt: The underlying alternative operation
func AlternativeMonoid[I, A any](m Monoid[A]) Monoid[Validate[I, A]] {
return monoid.AlternativeMonoid(
Of[I, A],
MonadMap[I, A, func(A) A],
MonadAp[A, I, A],
MonadAlt[I, A],
m,
)
}
// AltMonoid creates a Monoid instance for Validate[I, A] using alternative semantics
// with a provided zero/default validator.
//
// This function creates a monoid where:
// 1. The first successful validator wins (no result combination)
// 2. If the first fails, the second is tried as a fallback
// 3. If both fail, errors are aggregated
// 4. The provided zero validator serves as the identity element
//
// Unlike AlternativeMonoid, AltMonoid does NOT combine successful results - it always
// returns the first success. This makes it ideal for fallback chains and default values.
//
// # Type Parameters
//
// - I: The input type that validators accept
// - A: The output type that validators produce
//
// # Parameters
//
// - zero: A lazy Validate[I, A] that serves as the identity element. This is typically
// a validator that always succeeds with a default value, but can also be a failing
// validator if no default is appropriate.
//
// # Returns
//
// A Monoid[Validate[I, A]] that combines validators using alternative semantics where
// the first success wins.
//
// # Behavior Details
//
// The AltMonoid implements a "first success wins" strategy:
//
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
// - **Concat with Empty**: The zero validator is used as fallback
//
// # Example: Default Value Fallback
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// )
//
// // Create a monoid with a default value of 0
// m := validate.AltMonoid(func() validate.Validate[string, int] {
// return validate.Of[string, int](0)
// })
//
// // First validator succeeds - returns 42, second is not evaluated
// validator1 := validate.Of[string, int](42)
// validator2 := validate.Of[string, int](100)
// combined := m.Concat(validator1, validator2)
// result := combined("input")(nil)
// // result is validation.Success(42)
//
// # Example: Fallback Chain
//
// // Try primary, then fallback, then default
// m := validate.AltMonoid(func() validate.Validate[string, string] {
// return validate.Of[string, string]("default")
// })
//
// primary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.FailureWithMessage[string](input, "primary failed")(ctx)
// }
// }
// secondary := func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// return validation.Success("secondary value")
// }
// }
//
// // Chain: try primary, then secondary, then default
// combined := m.Concat(m.Concat(primary, secondary), m.Empty())
// result := combined("input")(nil)
// // result is validation.Success("secondary value")
//
// # Example: Error Aggregation
//
// // Both fail - errors are aggregated
// m := validate.AltMonoid(func() validate.Validate[string, int] {
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "no default")(ctx)
// }
// }
// })
//
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
// }
// }
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
// }
// }
//
// combined := m.Concat(failing1, failing2)
// result := combined("input")(nil)
// // result contains both "error 1" and "error 2"
//
// # Comparison with Other Monoids
//
// - **ApplicativeMonoid**: Combines results when both succeed using monoid operation
// - **AlternativeMonoid**: Combines results when both succeed, provides fallback when one fails
// - **AltMonoid**: First success wins, never combines results (pure alternative)
//
// # Use Cases
//
// - Configuration loading with fallback sources (try file, then env, then default)
// - Validation with default values
// - Parser combinators with alternative branches
// - Error recovery with multiple strategies
//
// # Notes
//
// - The zero validator is lazily evaluated, only when needed
// - First success short-circuits evaluation (second validator not called)
// - Error aggregation ensures all validation failures are reported
// - This follows the alternative functor laws
//
// # See Also
//
// - AlternativeMonoid: For combining results when both succeed
// - ApplicativeMonoid: For pure applicative combination
// - MonadAlt: The underlying alternative operation
// - Alt: The curried version for pipeline composition
func AltMonoid[I, A any](zero Lazy[Validate[I, A]]) Monoid[Validate[I, A]] {
return monoid.AltMonoid(
zero,
MonadAlt[I, A],
)
}

View File

@@ -1,475 +1,397 @@
// 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 validate
import (
"testing"
E "github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
MO "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/optics/codec/validation"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
var (
intAddMonoid = N.MonoidSum[int]()
strMonoid = S.Monoid
)
// TestAlternativeMonoid tests the AlternativeMonoid function
func TestAlternativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
// Helper function to create a successful validator
func successValidator[I, A any](value A) Validate[I, A] {
return func(input I) Reader[validation.Context, validation.Validation[A]] {
return func(ctx validation.Context) validation.Validation[A] {
return validation.Success(value)
}
}
}
t.Run("empty returns validator that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
// Helper function to create a failing validator
func failureValidator[I, A any](message string) Validate[I, A] {
return func(input I) Reader[validation.Context, validation.Validation[A]] {
return validation.FailureWithMessage[A](input, message)
}
}
assert.Equal(t, validation.Of(""), result)
})
// Helper function to create a validator that uses the input
func inputDependentValidator[A any](f func(A) A) Validate[A, A] {
return func(input A) Reader[validation.Context, validation.Validation[A]] {
return func(ctx validation.Context) validation.Validation[A] {
return validation.Success(f(input))
}
}
}
t.Run("concat combines successful validators using monoid", func(t *testing.T) {
validator1 := Of[string, string]("Hello")
validator2 := Of[string, string](" World")
// TestApplicativeMonoid_EmptyElement tests the empty element of the monoid
func TestApplicativeMonoid_EmptyElement(t *testing.T) {
t.Run("int addition monoid", func(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
empty := m.Empty()
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
result := empty("test")(nil)
assert.Equal(t, validation.Of("Hello World"), result)
})
assert.Equal(t, validation.Of(0), result)
})
t.Run("string concatenation monoid", func(t *testing.T) {
m := ApplicativeMonoid[int](strMonoid)
empty := m.Empty()
result := empty(42)(nil)
assert.Equal(t, validation.Of(""), result)
})
}
// TestApplicativeMonoid_ConcatSuccesses tests concatenating two successful validators
func TestApplicativeMonoid_ConcatSuccesses(t *testing.T) {
t.Run("int addition", func(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](5)
v2 := successValidator[string](3)
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.Equal(t, validation.Of(8), result)
})
t.Run("string concatenation", func(t *testing.T) {
m := ApplicativeMonoid[int](strMonoid)
v1 := successValidator[int]("Hello")
v2 := successValidator[int](" World")
combined := m.Concat(v1, v2)
result := combined(42)(nil)
assert.Equal(t, validation.Of("Hello World"), result)
})
}
// TestApplicativeMonoid_ConcatWithFailure tests concatenating validators where one fails
func TestApplicativeMonoid_ConcatWithFailure(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
t.Run("left failure", func(t *testing.T) {
v1 := failureValidator[string, int]("left error")
v2 := successValidator[string](5)
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 1)
assert.Equal(t, "left error", errors[0].Messsage)
})
t.Run("right failure", func(t *testing.T) {
v1 := successValidator[string](5)
v2 := failureValidator[string, int]("right error")
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 1)
assert.Equal(t, "right error", errors[0].Messsage)
})
t.Run("both failures", func(t *testing.T) {
v1 := failureValidator[string, int]("left error")
v2 := failureValidator[string, int]("right error")
combined := m.Concat(v1, v2)
result := combined("input")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
// Note: The current implementation returns the first error encountered
assert.GreaterOrEqual(t, len(errors), 1)
// At least one of the errors should be present
hasError := false
for _, err := range errors {
if err.Messsage == "left error" || err.Messsage == "right error" {
hasError = true
break
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "first failed"},
})
}
}
}
assert.True(t, hasError, "Should contain at least one validation error")
})
}
succeeding := Of[string, string]("fallback")
// TestApplicativeMonoid_LeftIdentity tests the left identity law
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
v := successValidator[string](42)
assert.Equal(t, validation.Of("fallback"), result)
})
// empty <> v == v
combined := m.Concat(m.Empty(), v)
resultCombined := combined("test")(nil)
resultOriginal := v("test")(nil)
assert.Equal(t, resultOriginal, resultCombined)
}
// TestApplicativeMonoid_RightIdentity tests the right identity law
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v := successValidator[string](42)
// v <> empty == v
combined := m.Concat(v, m.Empty())
resultCombined := combined("test")(nil)
resultOriginal := v("test")(nil)
assert.Equal(t, resultOriginal, resultCombined)
}
// TestApplicativeMonoid_Associativity tests the associativity law
func TestApplicativeMonoid_Associativity(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](1)
v2 := successValidator[string](2)
v3 := successValidator[string](3)
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
left := m.Concat(m.Concat(v1, v2), v3)
right := m.Concat(v1, m.Concat(v2, v3))
resultLeft := left("test")(nil)
resultRight := right("test")(nil)
assert.Equal(t, resultRight, resultLeft)
// Both should equal 6
assert.Equal(t, validation.Of(6), resultLeft)
}
// TestApplicativeMonoid_AssociativityWithFailures tests associativity with failures
func TestApplicativeMonoid_AssociativityWithFailures(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](1)
v2 := failureValidator[string, int]("error 2")
v3 := successValidator[string](3)
// (v1 <> v2) <> v3 == v1 <> (v2 <> v3)
left := m.Concat(m.Concat(v1, v2), v3)
right := m.Concat(v1, m.Concat(v2, v3))
resultLeft := left("test")(nil)
resultRight := right("test")(nil)
// Both should fail with the same error
assert.True(t, E.IsLeft(resultLeft))
assert.True(t, E.IsLeft(resultRight))
_, errorsLeft := E.Unwrap(resultLeft)
_, errorsRight := E.Unwrap(resultRight)
assert.Len(t, errorsLeft, 1)
assert.Len(t, errorsRight, 1)
assert.Equal(t, "error 2", errorsLeft[0].Messsage)
assert.Equal(t, "error 2", errorsRight[0].Messsage)
}
// TestApplicativeMonoid_MultipleValidators tests combining multiple validators
func TestApplicativeMonoid_MultipleValidators(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](10)
v2 := successValidator[string](20)
v3 := successValidator[string](30)
v4 := successValidator[string](40)
// Chain multiple concat operations
combined := m.Concat(
m.Concat(
m.Concat(v1, v2),
v3,
),
v4,
)
result := combined("test")(nil)
assert.Equal(t, validation.Of(100), result)
}
// TestApplicativeMonoid_InputDependent tests validators that depend on input
func TestApplicativeMonoid_InputDependent(t *testing.T) {
m := ApplicativeMonoid[int](intAddMonoid)
// Validator that doubles the input
v1 := inputDependentValidator(N.Mul(2))
// Validator that adds 10 to the input
v2 := inputDependentValidator(N.Add(10))
combined := m.Concat(v1, v2)
result := combined(5)(nil)
// (5 * 2) + (5 + 10) = 10 + 15 = 25
assert.Equal(t, validation.Of(25), result)
}
// TestApplicativeMonoid_ContextPropagation tests that context is properly propagated
func TestApplicativeMonoid_ContextPropagation(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
// Create a validator that captures the context
var capturedContext validation.Context
v1 := func(input string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
capturedContext = ctx
return validation.Success(5)
}
}
v2 := successValidator[string](3)
combined := m.Concat(v1, v2)
// Create a context with some entries
ctx := validation.Context{
{Key: "field1", Type: "int"},
{Key: "field2", Type: "string"},
}
result := combined("test")(ctx)
assert.True(t, E.IsRight(result))
assert.Equal(t, ctx, capturedContext)
}
// TestApplicativeMonoid_ErrorAccumulation tests that errors are accumulated
func TestApplicativeMonoid_ErrorAccumulation(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := failureValidator[string, int]("error 1")
v2 := failureValidator[string, int]("error 2")
v3 := failureValidator[string, int]("error 3")
combined := m.Concat(m.Concat(v1, v2), v3)
result := combined("test")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
// Note: The current implementation returns the first error encountered
// At least one error should be present
assert.GreaterOrEqual(t, len(errors), 1)
hasError := false
for _, err := range errors {
if err.Messsage == "error 1" || err.Messsage == "error 2" || err.Messsage == "error 3" {
hasError = true
break
}
}
assert.True(t, hasError, "Should contain at least one validation error")
}
// TestApplicativeMonoid_MixedSuccessFailure tests mixing successes and failures
func TestApplicativeMonoid_MixedSuccessFailure(t *testing.T) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](10)
v2 := failureValidator[string, int]("error in v2")
v3 := successValidator[string](20)
v4 := failureValidator[string, int]("error in v4")
combined := m.Concat(
m.Concat(
m.Concat(v1, v2),
v3,
),
v4,
)
result := combined("test")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
// Note: The current implementation returns the first error encountered
// At least one error should be present
assert.GreaterOrEqual(t, len(errors), 1)
hasError := false
for _, err := range errors {
if err.Messsage == "error in v2" || err.Messsage == "error in v4" {
hasError = true
break
}
}
assert.True(t, hasError, "Should contain at least one validation error")
}
// TestApplicativeMonoid_DifferentInputTypes tests with different input types
func TestApplicativeMonoid_DifferentInputTypes(t *testing.T) {
t.Run("struct input", func(t *testing.T) {
type Config struct {
Port int
Timeout int
}
m := ApplicativeMonoid[Config](intAddMonoid)
v1 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.Success(cfg.Port)
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
}
v2 := func(cfg Config) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.Success(cfg.Timeout)
failing2 := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
}
combined := m.Concat(v1, v2)
result := combined(Config{Port: 8080, Timeout: 30})(nil)
combined := m.Concat(failing1, failing2)
result := combined("input")(nil)
assert.Equal(t, validation.Of(8110), result) // 8080 + 30
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("concat with empty preserves validator", func(t *testing.T) {
validator := Of[string, string]("test")
empty := m.Empty()
result1 := m.Concat(validator, empty)("input")(nil)
result2 := m.Concat(empty, validator)("input")(nil)
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
})
})
}
// TestApplicativeMonoid_StringConcatenation tests string concatenation scenarios
func TestApplicativeMonoid_StringConcatenation(t *testing.T) {
m := ApplicativeMonoid[string](strMonoid)
t.Run("build sentence", func(t *testing.T) {
v1 := successValidator[string]("The")
v2 := successValidator[string](" quick")
v3 := successValidator[string](" brown")
v4 := successValidator[string](" fox")
combined := m.Concat(
m.Concat(
m.Concat(v1, v2),
v3,
),
v4,
t.Run("with int addition monoid", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := AlternativeMonoid[string, int](intMonoid)
result := combined("input")(nil)
t.Run("empty returns validator with zero", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
assert.Equal(t, validation.Of("The quick brown fox"), result)
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("concat uses fallback when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
succeeding := Of[string, int](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("multiple concat operations", func(t *testing.T) {
validator1 := Of[string, int](1)
validator2 := Of[string, int](2)
validator3 := Of[string, int](3)
validator4 := Of[string, int](4)
combined := m.Concat(m.Concat(m.Concat(validator1, validator2), validator3), validator4)
result := combined("input")(nil)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
})
})
t.Run("with empty strings", func(t *testing.T) {
v1 := successValidator[string]("Hello")
v2 := successValidator[string]("")
v3 := successValidator[string]("World")
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
combined := m.Concat(m.Concat(v1, v2), v3)
result := combined("input")(nil)
validator1 := Of[string, string]("a")
validator2 := Of[string, string]("b")
validator3 := Of[string, string]("c")
assert.Equal(t, validation.Of("HelloWorld"), result)
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), validator1)("input")(nil)
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("right identity", func(t *testing.T) {
result := m.Concat(validator1, m.Empty())("input")(nil)
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(validator1, validator2), validator3)("input")(nil)
right := m.Concat(validator1, m.Concat(validator2, validator3))("input")(nil)
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
})
})
}
// Benchmark tests
func BenchmarkApplicativeMonoid_ConcatSuccesses(b *testing.B) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := successValidator[string](5)
v2 := successValidator[string](3)
combined := m.Concat(v1, v2)
// TestAltMonoid tests the AltMonoid function
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
m := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
})
b.ResetTimer()
for range b.N {
_ = combined("test")(nil)
}
}
func BenchmarkApplicativeMonoid_ConcatFailures(b *testing.B) {
m := ApplicativeMonoid[string](intAddMonoid)
v1 := failureValidator[string, int]("error 1")
v2 := failureValidator[string, int]("error 2")
combined := m.Concat(v1, v2)
b.ResetTimer()
for range b.N {
_ = combined("test")(nil)
}
}
func BenchmarkApplicativeMonoid_MultipleConcat(b *testing.B) {
m := ApplicativeMonoid[string](intAddMonoid)
validators := make([]Validate[string, int], 10)
for i := range validators {
validators[i] = successValidator[string](i)
}
// Chain all validators
combined := validators[0]
for i := 1; i < len(validators); i++ {
combined = m.Concat(combined, validators[i])
}
b.ResetTimer()
for range b.N {
_ = combined("test")(nil)
}
t.Run("empty returns the provided zero validator", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
assert.Equal(t, validation.Of(0), result)
})
t.Run("concat returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := Of[string, int](100)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat uses second as fallback when first fails", func(t *testing.T) {
failing := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "failed"},
})
}
}
succeeding := Of[string, int](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
assert.Equal(t, validation.Of(42), result)
})
t.Run("concat aggregates errors when both fail", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
combined := m.Concat(failing1, failing2)
result := combined("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors from both validators")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
})
t.Run("with failing zero", func(t *testing.T) {
m := AltMonoid(func() Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Messsage: "no default available"},
})
}
}
})
t.Run("empty returns the failing zero validator", func(t *testing.T) {
empty := m.Empty()
result := empty("input")(nil)
assert.True(t, either.IsLeft(result))
})
t.Run("concat with all failures aggregates errors", func(t *testing.T) {
failing1 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 1"},
})
}
}
failing2 := func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
return either.Left[int](validation.Errors{
{Value: input, Messsage: "error 2"},
})
}
}
combined := m.Concat(failing1, failing2)
result := combined("input")(nil)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.GreaterOrEqual(t, len(errors), 2, "Should aggregate errors")
})
})
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Validate[string, string] {
return Of[string, string]("default")
})
primary := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "primary failed"},
})
}
}
secondary := func(input string) Reader[Context, Validation[string]] {
return func(ctx Context) Validation[string] {
return either.Left[string](validation.Errors{
{Value: input, Messsage: "secondary failed"},
})
}
}
tertiary := Of[string, string]("tertiary value")
combined := m.Concat(m.Concat(primary, secondary), tertiary)
result := combined("input")(nil)
assert.Equal(t, validation.Of("tertiary value"), result)
})
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
// AltMonoid - first success wins
altM := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
})
// AlternativeMonoid - combines successes
altMonoid := AlternativeMonoid[string, int](N.MonoidSum[int]())
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
// AltMonoid: returns first success (10)
result1 := altM.Concat(validator1, validator2)("input")(nil)
value1 := either.MonadFold(result1,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value1, "AltMonoid returns first success")
// AlternativeMonoid: combines both successes (10 + 32 = 42)
result2 := altMonoid.Concat(validator1, validator2)("input")(nil)
value2 := either.MonadFold(result2,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value2, "AlternativeMonoid combines successes")
})
}

View File

@@ -16,6 +16,8 @@
package validate
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/codec/decode"
"github.com/IBM/fp-go/v2/optics/codec/validation"
@@ -90,25 +92,72 @@ type (
// "at user.address.zipCode: expected string, got number"
Context = validation.Context
Decode[I, A any] = decode.Decode[I, A]
// Validate is a function that validates input I to produce type A with full context tracking.
// Decode represents a decoding operation that transforms input I into output A
// within a validation context.
//
// Type structure:
// Validate[I, A] = Reader[I, Decode[Context, A]]
// Decode[I, A] = Reader[Context, Validation[A]]
//
// This means:
// 1. Takes an input of type I
// 2. Returns a Reader that depends on validation Context
// 3. That Reader produces a Validation[A] (Either[Errors, A])
// 1. Takes a validation Context (path through nested structures)
// 2. Returns a Validation[A] (Either[Errors, A])
//
// The layered structure enables:
// - Access to the input value being validated
// - Context tracking through nested structures
// - Error accumulation with detailed paths
// - Composition with other validators
// Decode is used as the foundation for validation operations, providing:
// - Context-aware error reporting with detailed paths
// - Error accumulation across multiple validations
// - Composable validation logic
//
// The Decode type is typically not used directly but through the Validate type,
// which adds an additional Reader layer for accessing the input value.
//
// Example:
// decoder := func(ctx Context) Validation[int] {
// // Perform validation and return result
// return validation.Success(42)
// }
// // decoder is a Decode[any, int]
Decode[I, A any] = decode.Decode[I, A]
// Validate represents a composable validator that transforms input I to output A
// with comprehensive error tracking and context propagation.
//
// # Type Structure
//
// Validate[I, A] = Reader[I, Decode[Context, A]]
// = Reader[I, Reader[Context, Validation[A]]]
// = func(I) func(Context) Either[Errors, A]
//
// This three-layer structure provides:
// 1. Input access: The outer Reader[I, ...] gives access to the input value I
// 2. Context tracking: The middle Reader[Context, ...] tracks the validation path
// 3. Error handling: The inner Validation[A] accumulates errors or produces value A
//
// # Purpose
//
// Validate is the core type for building type-safe, composable validators that:
// - Transform and validate data from one type to another
// - Track the path through nested structures for detailed error messages
// - Accumulate multiple validation errors instead of failing fast
// - Compose with other validators using functional patterns
//
// # Key Features
//
// - Context-aware: Automatically tracks validation path (e.g., "user.address.zipCode")
// - Error accumulation: Collects all validation errors, not just the first one
// - Type-safe: Leverages Go's type system to ensure correctness
// - Composable: Validators can be combined using Map, Chain, Ap, and other operators
//
// # Algebraic Structure
//
// Validate forms several algebraic structures:
// - Functor: Transform successful results with Map
// - Applicative: Combine independent validators in parallel with Ap
// - Monad: Chain dependent validators sequentially with Chain
//
// # Example Usage
//
// Basic validator:
//
// Example usage:
// validatePositive := func(n int) Reader[Context, Validation[int]] {
// return func(ctx Context) Validation[int] {
// if n > 0 {
@@ -119,10 +168,33 @@ type (
// }
// // validatePositive is a Validate[int, int]
//
// The Validate type forms:
// - A Functor: Can map over successful results
// - An Applicative: Can combine validators in parallel
// - A Monad: Can chain dependent validations
// Composing validators:
//
// // Transform the result of a validator
// doubled := Map[int, int, int](func(x int) int { return x * 2 })(validatePositive)
//
// // Chain dependent validations
// validateRange := func(n int) Validate[int, int] {
// return func(input int) Reader[Context, Validation[int]] {
// return func(ctx Context) Validation[int] {
// if n <= 100 {
// return validation.Success(n)
// }
// return validation.FailureWithMessage[int](n, "must be <= 100")(ctx)
// }
// }
// }
// combined := Chain(validateRange)(validatePositive)
//
// # Integration
//
// Validate integrates with the broader optics/codec ecosystem:
// - Works with Decode for decoding operations
// - Uses Validation for error handling
// - Leverages Context for detailed error reporting
// - Composes with other codec types for complete encode/decode pipelines
//
// See the package documentation for more examples and patterns.
Validate[I, A any] = Reader[I, Decode[Context, A]]
// Errors is a collection of validation errors that occurred during validation.
@@ -174,4 +246,32 @@ type (
// // toUpper is an Operator[string, string, string]
// // It can be applied to any string validator to uppercase the result
Operator[I, A, B any] = Kleisli[I, Validate[I, A], B]
// Endomorphism represents a function from a type to itself.
//
// Type: Endomorphism[A] = func(A) A
//
// An endomorphism is a morphism (structure-preserving map) where the source
// and target are the same type. In simpler terms, it's a function that takes
// a value of type A and returns a value of the same type A.
//
// Endomorphisms are useful for:
// - Transformations that preserve type (e.g., string normalization)
// - Composable updates and modifications
// - Building pipelines of same-type transformations
// - Implementing the Monoid pattern (composition as binary operation)
//
// Endomorphisms form a Monoid under function composition:
// - Identity: func(a A) A { return a }
// - Concat: func(f, g Endomorphism[A]) Endomorphism[A] {
// return func(a A) A { return f(g(a)) }
// }
//
// Example:
// trim := strings.TrimSpace // Endomorphism[string]
// lower := strings.ToLower // Endomorphism[string]
// normalize := compose(trim, lower) // Endomorphism[string]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Lazy[A any] = lazy.Lazy[A]
)

View File

@@ -119,6 +119,7 @@
package validate
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/optics/codec/decode"
"github.com/IBM/fp-go/v2/reader"
@@ -309,6 +310,364 @@ func Chain[I, A, B any](f Kleisli[I, A, B]) Operator[I, A, B] {
)
}
// ChainLeft sequences a computation on the failure (Left) channel of a validation.
//
// This function operates on the error path of validation, allowing you to transform,
// enrich, or recover from validation failures. It's the dual of Chain - while Chain
// operates on success values, ChainLeft operates on error values.
//
// # Key Behavior
//
// **Critical difference from standard Either operations**: This validation-specific
// implementation **aggregates errors** using the Errors monoid. When the transformation
// function returns a failure, both the original errors AND the new errors are combined,
// ensuring comprehensive error reporting.
//
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
// the success value passes through unchanged.
//
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
// validation, converting Left to Right.
//
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
// errors and the new errors are combined using the Errors monoid.
//
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
// to the original input value I for context-aware error handling.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
// is called only when validation fails, receiving the accumulated errors.
//
// # Returns
//
// An Operator[I, A, A] that transforms validators by handling their error cases.
//
// # Example: Error Recovery
//
// // Validator that may fail
// validatePositive := func(n int) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// if n > 0 {
// return validation.Success(n)
// }
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
// }
// }
//
// // Recover from specific errors with a default value
// withDefault := ChainLeft(func(errs Errors) Validate[int, int] {
// for _, err := range errs {
// if err.Messsage == "must be positive" {
// return Of[int](0) // recover with default
// }
// }
// return func(input int) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return either.Left[int](errs)
// }
// }
// })
//
// validator := withDefault(validatePositive)
// result := validator(-5)(nil)
// // Result: Success(0) - recovered from failure
//
// # Example: Error Context Addition
//
// // Add contextual information to errors
// addContext := ChainLeft(func(errs Errors) Validate[string, int] {
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to validate user age",
// },
// })
// }
// }
// })
//
// validator := addContext(someValidator)
// // Errors will include both original error and context
//
// # Example: Input-Dependent Recovery
//
// // Recover with different defaults based on input
// smartDefault := ChainLeft(func(errs Errors) Validate[string, int] {
// return func(input string) Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// // Use input to determine appropriate default
// if strings.Contains(input, "http") {
// return validation.Of(80)
// }
// if strings.Contains(input, "https") {
// return validation.Of(443)
// }
// return validation.Of(8080)
// }
// }
// })
//
// # Notes
//
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
// - The handler has access to both the errors and the original input
// - Success values bypass the handler completely
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
// - Use OrElse as a semantic alias when emphasizing fallback/alternative logic
func ChainLeft[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return readert.Chain[Validate[I, A]](
decode.ChainLeft,
f,
)
}
// MonadChainLeft sequences a computation on the failure (Left) channel of a validation.
//
// This is the direct application version of ChainLeft. It operates on the error path
// of validation, allowing you to transform, enrich, or recover from validation failures.
// It's the dual of Chain - while Chain operates on success values, MonadChainLeft
// operates on error values.
//
// # Key Behavior
//
// **Critical difference from standard Either operations**: This validation-specific
// implementation **aggregates errors** using the Errors monoid. When the transformation
// function returns a failure, both the original errors AND the new errors are combined,
// ensuring comprehensive error reporting.
//
// 1. **Success Pass-Through**: If validation succeeds, the handler is never called and
// the success value passes through unchanged.
//
// 2. **Error Recovery**: The handler can recover from failures by returning a successful
// validation, converting Left to Right.
//
// 3. **Error Aggregation**: When the handler also returns a failure, both the original
// errors and the new errors are combined using the Errors monoid.
//
// 4. **Input Access**: The handler returns a Validate[I, A] function, giving it access
// to the original input value I for context-aware error handling.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - fa: The Validate[I, A] to transform
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
// is called only when validation fails, receiving the accumulated errors.
//
// # Returns
//
// A Validate[I, A] that handles error cases according to the provided function.
//
// # Example: Error Recovery
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// )
//
// // Validator that may fail
// validatePositive := func(n int) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// if n > 0 {
// return validation.Success(n)
// }
// return validation.FailureWithMessage[int](n, "must be positive")(ctx)
// }
// }
//
// // Recover from specific errors with a default value
// withDefault := func(errs validation.Errors) validate.Validate[int, int] {
// for _, err := range errs {
// if err.Messsage == "must be positive" {
// return validate.Of[int](0) // recover with default
// }
// }
// // Propagate other errors
// return func(input int) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return either.Left[int](errs)
// }
// }
// }
//
// validator := validate.MonadChainLeft(validatePositive, withDefault)
// result := validator(-5)(nil)
// // Result: Success(0) - recovered from failure
//
// # Example: Error Context Addition
//
// // Add contextual information to errors
// addContext := func(errs validation.Errors) validate.Validate[string, int] {
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// // Add context error (will be aggregated with original)
// return either.Left[int](validation.Errors{
// {
// Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to validate user age",
// },
// })
// }
// }
// }
//
// validator := validate.MonadChainLeft(someValidator, addContext)
// // Errors will include both original error and context
//
// # Example: Input-Dependent Recovery
//
// // Recover with different defaults based on input
// smartDefault := func(errs validation.Errors) validate.Validate[string, int] {
// return func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// // Use input to determine appropriate default
// if strings.Contains(input, "http:") {
// return validation.Success(80)
// }
// if strings.Contains(input, "https:") {
// return validation.Success(443)
// }
// return validation.Success(8080)
// }
// }
// }
//
// validator := validate.MonadChainLeft(parsePort, smartDefault)
//
// # Notes
//
// - Errors are accumulated, not replaced - this ensures no validation failures are lost
// - The handler has access to both the errors and the original input
// - Success values bypass the handler completely
// - This is the direct application version of ChainLeft
// - This enables sophisticated error handling strategies including recovery, enrichment, and transformation
//
// # See Also
//
// - ChainLeft: The curried, point-free version
// - OrElse: Semantic alias for ChainLeft emphasizing fallback logic
// - MonadAlt: Simplified alternative that ignores error details
// - Alt: Curried version of MonadAlt
func MonadChainLeft[I, A any](fa Validate[I, A], f Kleisli[I, Errors, A]) Validate[I, A] {
return readert.MonadChain(
decode.MonadChainLeft,
fa,
f,
)
}
// OrElse provides an alternative validation when the primary validation fails.
//
// This is a semantic alias for ChainLeft with identical behavior. The name "OrElse"
// emphasizes the intent of providing fallback or alternative validation logic, making
// code more readable when that's the primary use case.
//
// # Relationship to ChainLeft
//
// **OrElse and ChainLeft are functionally identical** - they produce exactly the same
// results for all inputs. The choice between them is purely about code readability:
//
// - Use **OrElse** when emphasizing fallback/alternative validation logic
// - Use **ChainLeft** when emphasizing technical error channel transformation
//
// Both maintain the critical property of **error aggregation**, ensuring all validation
// failures are preserved and reported together.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - f: A Kleisli arrow that takes Errors and returns a Validate[I, A]. This function
// is called only when validation fails, receiving the accumulated errors.
//
// # Returns
//
// An Operator[I, A, A] that transforms validators by providing alternative validation.
//
// # Example: Fallback Validation
//
// // Primary validator that may fail
// validateFromConfig := func(key string) Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// // Try to get value from config
// if value, ok := config[key]; ok {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not found in config")(ctx)
// }
// }
//
// // Use OrElse for semantic clarity - "try config, or else use environment"
// withEnvFallback := OrElse(func(errs Errors) Validate[string, string] {
// return func(key string) Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value := os.Getenv(key); value != "" {
// return validation.Success(value)
// }
// return either.Left[string](errs) // propagate original errors
// }
// }
// })
//
// validator := withEnvFallback(validateFromConfig)
// result := validator("DATABASE_URL")(nil)
// // Tries config first, falls back to environment variable
//
// # Example: Default Value on Failure
//
// // Provide a default value when validation fails
// withDefault := OrElse(func(errs Errors) Validate[int, int] {
// return Of[int](0) // default to 0 on any failure
// })
//
// validator := withDefault(someValidator)
// result := validator(input)(nil)
// // Always succeeds, using default value if validation fails
//
// # Example: Pipeline with Multiple Fallbacks
//
// // Build a validation pipeline with multiple fallback strategies
// validator := F.Pipe2(
// validateFromDatabase,
// OrElse(func(errs Errors) Validate[string, Config] {
// // Try cache as first fallback
// return validateFromCache
// }),
// OrElse(func(errs Errors) Validate[string, Config] {
// // Use default config as final fallback
// return Of[string](defaultConfig)
// }),
// )
// // Tries database, then cache, then default
//
// # Notes
//
// - Identical behavior to ChainLeft - they are aliases
// - Errors are accumulated when transformations fail
// - Success values pass through unchanged
// - The handler has access to both errors and original input
// - Choose OrElse for better readability when providing alternatives
// - See ChainLeft documentation for detailed behavior and additional examples
func OrElse[I, A any](f Kleisli[I, Errors, A]) Operator[I, A, A] {
return ChainLeft(f)
}
// MonadAp applies a validator containing a function to a validator containing a value.
//
// This is the applicative apply operation for Validate. It allows you to apply
@@ -409,3 +768,218 @@ func Ap[B, I, A any](fa Validate[I, A]) Operator[I, func(A) B, B] {
fa,
)
}
// Alt provides an alternative validator when the primary validator fails.
//
// This is the curried, point-free version of MonadAlt. It creates an operator that
// transforms a validator by adding a fallback alternative. When the first validator
// fails, the second (lazily evaluated) validator is tried. If both fail, errors are
// aggregated.
//
// Alt implements the Alternative typeclass pattern, providing a way to express
// "try this, or else try that" logic in a composable way.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
// if the first validator fails.
//
// # Returns
//
// An Operator[I, A, A] that transforms validators by adding alternative fallback logic.
//
// # Behavior
//
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
//
// # Example: Fallback Validation
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// )
//
// // Primary validator that may fail
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// // Try to get value from config
// if value, ok := config[key]; ok {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
// }
// }
//
// // Fallback to environment variable
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value := os.Getenv(key); value != "" {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
// }
// }
//
// // Use Alt to add fallback - point-free style
// withFallback := validate.Alt(func() validate.Validate[string, string] {
// return validateFromEnv
// })
//
// validator := withFallback(validateFromConfig)
// result := validator("DATABASE_URL")(nil)
// // Tries config first, falls back to environment variable
//
// # Example: Pipeline with Multiple Alternatives
//
// // Chain multiple alternatives using function composition
// validator := F.Pipe2(
// validateFromDatabase,
// validate.Alt(func() validate.Validate[string, Config] {
// return validateFromCache
// }),
// validate.Alt(func() validate.Validate[string, Config] {
// return validate.Of[string](defaultConfig)
// }),
// )
// // Tries database, then cache, then default
//
// # Notes
//
// - The second validator is lazily evaluated for efficiency
// - First success short-circuits evaluation
// - Errors are aggregated when both fail
// - This is the point-free version of MonadAlt
// - Useful for building validation pipelines with F.Pipe
//
// # See Also
//
// - MonadAlt: The direct application version
// - ChainLeft: The more general error transformation operator
// - OrElse: Semantic alias for ChainLeft
// - AltMonoid: For combining multiple alternatives with monoid structure
func Alt[I, A any](second Lazy[Validate[I, A]]) Operator[I, A, A] {
return ChainLeft(function.Ignore1of1[Errors](second))
}
// MonadAlt provides an alternative validator when the primary validator fails.
//
// This is the direct application version of Alt. It takes two validators and returns
// a new validator that tries the first, and if it fails, tries the second. If both
// fail, errors from both are aggregated.
//
// MonadAlt implements the Alternative typeclass pattern, enabling "try this, or else
// try that" logic with comprehensive error reporting.
//
// # Type Parameters
//
// - I: The input type
// - A: The type of the validation result
//
// # Parameters
//
// - first: The primary Validate[I, A] to try first
// - second: A lazy Validate[I, A] that serves as the fallback. It's only evaluated
// if the first validator fails.
//
// # Returns
//
// A Validate[I, A] that tries the first validator, falling back to the second if needed.
//
// # Behavior
//
// - **First succeeds**: Returns the first result, second is never evaluated
// - **First fails, second succeeds**: Returns the second result
// - **Both fail**: Aggregates errors from both validators
//
// # Example: Configuration with Fallback
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec/validate"
// "github.com/IBM/fp-go/v2/optics/codec/validation"
// )
//
// // Primary validator
// validateFromConfig := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value, ok := config[key]; ok {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in config")(ctx)
// }
// }
//
// // Fallback validator
// validateFromEnv := func(key string) validate.Reader[validation.Context, validation.Validation[string]] {
// return func(ctx validation.Context) validation.Validation[string] {
// if value := os.Getenv(key); value != "" {
// return validation.Success(value)
// }
// return validation.FailureWithMessage[string](key, "not in env")(ctx)
// }
// }
//
// // Combine with MonadAlt
// validator := validate.MonadAlt(
// validateFromConfig,
// func() validate.Validate[string, string] { return validateFromEnv },
// )
// result := validator("DATABASE_URL")(nil)
// // Tries config first, falls back to environment variable
//
// # Example: Multiple Fallbacks
//
// // Chain multiple alternatives
// validator := validate.MonadAlt(
// validate.MonadAlt(
// validateFromDatabase,
// func() validate.Validate[string, Config] { return validateFromCache },
// ),
// func() validate.Validate[string, Config] { return validate.Of[string](defaultConfig) },
// )
// // Tries database, then cache, then default
//
// # Example: Error Aggregation
//
// failing1 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 1")(ctx)
// }
// }
// failing2 := func(input string) validate.Reader[validation.Context, validation.Validation[int]] {
// return func(ctx validation.Context) validation.Validation[int] {
// return validation.FailureWithMessage[int](input, "error 2")(ctx)
// }
// }
//
// validator := validate.MonadAlt(
// failing1,
// func() validate.Validate[string, int] { return failing2 },
// )
// result := validator("input")(nil)
// // result contains both "error 1" and "error 2"
//
// # Notes
//
// - The second validator is lazily evaluated for efficiency
// - First success short-circuits evaluation (second not called)
// - Errors are aggregated when both fail
// - This is equivalent to Alt but with direct application
// - Both validators receive the same input value
//
// # See Also
//
// - Alt: The curried, point-free version
// - MonadChainLeft: The underlying error transformation operation
// - OrElse: Semantic alias for ChainLeft
// - AltMonoid: For combining multiple alternatives with monoid structure
func MonadAlt[I, A any](first Validate[I, A], second Lazy[Validate[I, A]]) Validate[I, A] {
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
}

View File

@@ -849,3 +849,428 @@ func TestFunctorLaws(t *testing.T) {
}
})
}
// TestChainLeft tests the ChainLeft function
func TestChainLeft(t *testing.T) {
t.Run("transforms failures while preserving successes", func(t *testing.T) {
// Create a failing validator
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "validation failed")(ctx)
}
}
// Handler that recovers from specific errors
handler := ChainLeft(func(errs Errors) Validate[int, int] {
for _, err := range errs {
if err.Messsage == "validation failed" {
return Of[int](0) // recover with default
}
}
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return E.Left[int](errs)
}
}
})
validator := handler(failingValidator)
result := validator(-5)(nil)
assert.Equal(t, validation.Of(0), result, "Should recover from failure")
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[int](42)
handler := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "should not be called")(ctx)
}
}
})
validator := handler(successValidator)
result := validator(100)(nil)
assert.Equal(t, validation.Of(42), result, "Success should pass through unchanged")
})
t.Run("aggregates errors when transformation also fails", func(t *testing.T) {
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](s, "original error")(ctx)
}
}
handler := ChainLeft(func(errs Errors) Validate[string, string] {
return func(input string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](input, "additional error")(ctx)
}
}
})
validator := handler(failingValidator)
result := validator("test")(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 2, "Should aggregate both errors")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "original error")
assert.Contains(t, messages, "additional error")
})
t.Run("adds context to errors", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "invalid value")(ctx)
}
}
addContext := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return E.Left[int](validation.Errors{
{
Context: validation.Context{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
Messsage: "failed to validate user age",
},
})
}
}
})
validator := addContext(failingValidator)
result := validator(150)(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 2, "Should have both original and context errors")
})
t.Run("can be composed in pipeline", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "error1")(ctx)
}
}
handler1 := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "error2")(ctx)
}
}
})
handler2 := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "error3")(ctx)
}
}
})
validator := handler2(handler1(failingValidator))
result := validator(42)(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.GreaterOrEqual(t, len(errors), 2, "Should accumulate errors through pipeline")
})
t.Run("provides access to original input", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "failed")(ctx)
}
}
// Handler uses input to determine recovery strategy
handler := ChainLeft(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
// Use input value to decide on recovery
if input < 0 {
return validation.Of(0)
}
if input > 100 {
return validation.Of(100)
}
return E.Left[int](errs)
}
}
})
validator := handler(failingValidator)
result1 := validator(-10)(nil)
assert.Equal(t, validation.Of(0), result1, "Should recover negative to 0")
result2 := validator(150)(nil)
assert.Equal(t, validation.Of(100), result2, "Should recover large to 100")
})
t.Run("works with different input and output types", func(t *testing.T) {
// Validator that converts string to int
parseValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
}
}
// Handler that provides default based on input string
handler := ChainLeft(func(errs Errors) Validate[string, int] {
return func(input string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
if input == "default" {
return validation.Of(42)
}
return E.Left[int](errs)
}
}
})
validator := handler(parseValidator)
result := validator("default")(nil)
assert.Equal(t, validation.Of(42), result)
})
}
// TestOrElse tests the OrElse function
func TestOrElse(t *testing.T) {
t.Run("provides fallback for failing validation", func(t *testing.T) {
// Primary validator that fails
primaryValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](s, "not found")(ctx)
}
}
// Use OrElse to provide fallback
withFallback := OrElse(func(errs Errors) Validate[string, string] {
return Of[string]("default value")
})
validator := withFallback(primaryValidator)
result := validator("missing")(nil)
assert.Equal(t, validation.Of("default value"), result)
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[string]("success")
withFallback := OrElse(func(errs Errors) Validate[string, string] {
return Of[string]("fallback")
})
validator := withFallback(successValidator)
result := validator("input")(nil)
assert.Equal(t, validation.Of("success"), result, "Should not use fallback for success")
})
t.Run("aggregates errors when fallback also fails", func(t *testing.T) {
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "primary failed")(ctx)
}
}
withFallback := OrElse(func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](input, "fallback failed")(ctx)
}
}
})
validator := withFallback(failingValidator)
result := validator(42)(nil)
assert.True(t, E.IsLeft(result))
_, errors := E.Unwrap(result)
assert.Len(t, errors, 2, "Should aggregate both errors")
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "primary failed")
assert.Contains(t, messages, "fallback failed")
})
t.Run("supports multiple fallback strategies", func(t *testing.T) {
failingValidator := func(s string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
return validation.FailureWithMessage[string](s, "not in database")(ctx)
}
}
// First fallback: try cache
tryCache := OrElse(func(errs Errors) Validate[string, string] {
return func(input string) Reader[validation.Context, validation.Validation[string]] {
return func(ctx validation.Context) validation.Validation[string] {
if input == "cached" {
return validation.Of("from cache")
}
return E.Left[string](errs)
}
}
})
// Second fallback: use default
useDefault := OrElse(func(errs Errors) Validate[string, string] {
return Of[string]("default")
})
// Compose fallbacks
validator := useDefault(tryCache(failingValidator))
// Test with cached value
result1 := validator("cached")(nil)
assert.Equal(t, validation.Of("from cache"), result1)
// Test with non-cached value (should use default)
result2 := validator("other")(nil)
assert.Equal(t, validation.Of("default"), result2)
})
t.Run("provides input-dependent fallback", func(t *testing.T) {
failingValidator := func(s string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](s, "parse failed")(ctx)
}
}
// Fallback with different defaults based on input
smartFallback := OrElse(func(errs Errors) Validate[string, int] {
return func(input string) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
// Provide context-aware defaults
if input == "http" {
return validation.Of(80)
}
if input == "https" {
return validation.Of(443)
}
return validation.Of(8080)
}
}
})
validator := smartFallback(failingValidator)
result1 := validator("http")(nil)
assert.Equal(t, validation.Of(80), result1)
result2 := validator("https")(nil)
assert.Equal(t, validation.Of(443), result2)
result3 := validator("other")(nil)
assert.Equal(t, validation.Of(8080), result3)
})
t.Run("is equivalent to ChainLeft", func(t *testing.T) {
// Create identical handlers
handler := func(errs Errors) Validate[int, int] {
return func(input int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
if input < 0 {
return validation.Of(0)
}
return E.Left[int](errs)
}
}
}
failingValidator := func(n int) Reader[validation.Context, validation.Validation[int]] {
return func(ctx validation.Context) validation.Validation[int] {
return validation.FailureWithMessage[int](n, "failed")(ctx)
}
}
// Apply with ChainLeft
withChainLeft := ChainLeft(handler)(failingValidator)
// Apply with OrElse
withOrElse := OrElse(handler)(failingValidator)
// Test with same inputs
inputs := []int{-10, 0, 10, -5, 100}
for _, input := range inputs {
result1 := withChainLeft(input)(nil)
result2 := withOrElse(input)(nil)
// Results should be identical
assert.Equal(t, E.IsLeft(result1), E.IsLeft(result2))
if E.IsRight(result1) {
val1, _ := E.Unwrap(result1)
val2, _ := E.Unwrap(result2)
assert.Equal(t, val1, val2, "OrElse and ChainLeft should produce identical results")
}
}
})
t.Run("works in complex validation pipeline", func(t *testing.T) {
type Config struct {
Port int
Host string
}
// Validator that tries to parse config
parseConfig := func(s string) Reader[validation.Context, validation.Validation[Config]] {
return func(ctx validation.Context) validation.Validation[Config] {
return validation.FailureWithMessage[Config](s, "invalid config")(ctx)
}
}
// Fallback to environment variables
tryEnv := OrElse(func(errs Errors) Validate[string, Config] {
return func(input string) Reader[validation.Context, validation.Validation[Config]] {
return func(ctx validation.Context) validation.Validation[Config] {
// Simulate env var lookup
if input == "from_env" {
return validation.Of(Config{Port: 8080, Host: "localhost"})
}
return E.Left[Config](errs)
}
}
})
// Final fallback to defaults
useDefaults := OrElse(func(errs Errors) Validate[string, Config] {
return Of[string](Config{Port: 3000, Host: "0.0.0.0"})
})
// Build pipeline
validator := useDefaults(tryEnv(parseConfig))
// Test with env fallback
result1 := validator("from_env")(nil)
assert.True(t, E.IsRight(result1))
if E.IsRight(result1) {
cfg, _ := E.Unwrap(result1)
assert.Equal(t, 8080, cfg.Port)
assert.Equal(t, "localhost", cfg.Host)
}
// Test with default fallback
result2 := validator("other")(nil)
assert.True(t, E.IsRight(result2))
if E.IsRight(result2) {
cfg, _ := E.Unwrap(result2)
assert.Equal(t, 3000, cfg.Port)
assert.Equal(t, "0.0.0.0", cfg.Host)
}
})
}

View File

@@ -0,0 +1,162 @@
# OrElse is Equivalent to ChainLeft
## Overview
In [`optics/codec/validation/monad.go`](monad.go:474-476), the [`OrElse`](monad.go:474) function is defined as a simple alias for [`ChainLeft`](monad.go:304):
```go
//go:inline
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
return ChainLeft(f)
}
```
This means **`OrElse` and `ChainLeft` are functionally identical** - they produce exactly the same results for all inputs.
## Why Have Both?
While they are technically the same, they serve different **semantic purposes**:
### ChainLeft - Technical Perspective
[`ChainLeft`](monad.go:304-309) emphasizes the **technical operation**: it chains a computation on the Left (failure) channel of the Either/Validation monad. This name comes from category theory and functional programming terminology.
### OrElse - Semantic Perspective
[`OrElse`](monad.go:474-476) emphasizes the **intent**: it provides an alternative or fallback when validation fails. The name reads naturally in code: "try this validation, **or else** try this alternative."
## Key Behavior
Both functions share the same critical behavior that distinguishes them from standard Either operations:
### Error Aggregation
When the transformation function returns a failure, **both the original errors AND the new errors are combined** using the Errors monoid. This ensures no validation errors are lost.
```go
// Example: Error aggregation
result := OrElse(func(errs Errors) Validation[string] {
return Failures[string](Errors{
&ValidationError{Messsage: "additional error"},
})
})(Failures[string](Errors{
&ValidationError{Messsage: "original error"},
}))
// Result contains BOTH errors: ["original error", "additional error"]
```
### Success Pass-Through
Success values pass through unchanged - the function is never called:
```go
result := OrElse(func(errs Errors) Validation[int] {
return Failures[int](Errors{
&ValidationError{Messsage: "never called"},
})
})(Success(42))
// Result: Success(42) - unchanged
```
### Error Recovery
The function can recover from failures by returning a Success:
```go
recoverFromNotFound := OrElse(func(errs Errors) Validation[int] {
for _, err := range errs {
if err.Messsage == "not found" {
return Success(0) // recover with default
}
}
return Failures[int](errs)
})
result := recoverFromNotFound(Failures[int](Errors{
&ValidationError{Messsage: "not found"},
}))
// Result: Success(0) - recovered from failure
```
## Use Cases
### 1. Fallback Validation (OrElse reads better)
```go
validatePositive := func(x int) Validation[int] {
if x > 0 {
return Success(x)
}
return Failures[int](Errors{
&ValidationError{Messsage: "must be positive"},
})
}
// Use OrElse for semantic clarity
withDefault := OrElse(func(errs Errors) Validation[int] {
return Success(1) // default to 1 if validation fails
})
result := F.Pipe1(validatePositive(-5), withDefault)
// Result: Success(1)
```
### 2. Error Context Addition (ChainLeft reads better)
```go
addContext := ChainLeft(func(errs Errors) Validation[string] {
return Failures[string](Errors{
&ValidationError{
Messsage: "validation failed in user.email field",
},
})
})
result := F.Pipe1(
Failures[string](Errors{
&ValidationError{Messsage: "invalid format"},
}),
addContext,
)
// Result contains: ["invalid format", "validation failed in user.email field"]
```
### 3. Pipeline Composition
Both can be used in pipelines, with errors accumulating at each step:
```go
result := F.Pipe2(
Failures[int](Errors{
&ValidationError{Messsage: "database error"},
}),
OrElse(func(errs Errors) Validation[int] {
return Failures[int](Errors{
&ValidationError{Messsage: "context added"},
})
}),
OrElse(func(errs Errors) Validation[int] {
return Failures[int](errs) // propagate
}),
)
// Errors accumulate at each step in the pipeline
```
## Verification
The test suite in [`monad_test.go`](monad_test.go:1698) includes comprehensive tests proving that `OrElse` and `ChainLeft` are equivalent:
- ✅ Identical behavior for Success values
- ✅ Identical behavior for error recovery
- ✅ Identical behavior for error aggregation
- ✅ Identical behavior in pipeline composition
- ✅ Identical behavior for multiple error scenarios
Run the tests:
```bash
go test -v -run TestOrElse ./optics/codec/validation
```
## Conclusion
**`OrElse` is exactly the same as `ChainLeft`** - they are aliases with identical implementations and behavior. The choice between them is purely about **code readability and semantic intent**:
- Use **`OrElse`** when emphasizing fallback/alternative validation logic
- Use **`ChainLeft`** when emphasizing technical error channel transformation
Both maintain the critical validation property of **error aggregation**, ensuring all validation failures are preserved and reported together.

View File

@@ -0,0 +1,318 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validation
import (
"github.com/IBM/fp-go/v2/function"
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type S to be used with the Bind operation.
// This is the starting point for building up a context using do-notation style.
//
// Example:
//
// type Result struct {
// x int
// y string
// }
// result := Do(Result{})
func Do[S any](
empty S,
) Validation[S] {
return Of(empty)
}
// Bind attaches the result of a computation to a context S1 to produce a context S2.
// This is used in do-notation style to sequentially build up a context.
//
// Example:
//
// type State struct { x int; y int }
// result := F.Pipe2(
// Do(State{}),
// Bind(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, func(s State) Validation[int] { return Success(42) }),
// )
func Bind[S1, S2, A any](
setter func(A) func(S1) S2,
f Kleisli[S1, A],
) Operator[S1, S2] {
return C.Bind(
Chain[S1, S2],
Map[A, S2],
setter,
f,
)
}
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Unlike Bind, the computation function returns a plain value, not an Option.
//
// Example:
//
// type State struct { x int; computed int }
// result := F.Pipe2(
// Do(State{x: 5}),
// Let(func(c int) func(State) State {
// return func(s State) State { s.computed = c; return s }
// }, func(s State) int { return s.x * 2 }),
// )
func Let[S1, S2, B any](
key func(B) func(S1) S2,
f func(S1) B,
) Operator[S1, S2] {
return F.Let(
Map[S1, S2],
key,
f,
)
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
//
// Example:
//
// type State struct { x int; name string }
// result := F.Pipe2(
// Do(State{x: 5}),
// LetTo(func(n string) func(State) State {
// return func(s State) State { s.name = n; return s }
// }, "example"),
// )
func LetTo[S1, S2, B any](
key func(B) func(S1) S2,
b B,
) Operator[S1, S2] {
return F.LetTo(
Map[S1, S2],
key,
b,
)
}
// BindTo initializes a new state S1 from a value T.
// This is typically used as the first operation after creating a Validation value.
//
// Example:
//
// type State struct { value int }
// result := F.Pipe1(
// Success(42),
// BindTo(func(x int) State { return State{value: x} }),
// )
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return C.BindTo(
Map[T, S1],
setter,
)
}
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
// This uses the applicative functor pattern, allowing parallel composition.
//
// IMPORTANT: Unlike Bind which fails fast, ApS aggregates ALL validation errors from both the context
// and the value. If both validations fail, all errors are collected and returned together.
// This is useful for validating multiple independent fields and reporting all errors at once.
//
// Example:
//
// type State struct { x int; y int }
// result := F.Pipe2(
// Do(State{}),
// ApS(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, Success(42)),
// )
//
// Error aggregation example:
//
// stateFailure := Failures[State](Errors{&ValidationError{Messsage: "state error"}})
// valueFailure := Failures[int](Errors{&ValidationError{Messsage: "value error"}})
// result := ApS(setter, valueFailure)(stateFailure)
// // Result contains BOTH errors: ["state error", "value error"]
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Validation[T],
) Operator[S1, S2] {
return A.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
setter,
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// IMPORTANT: Like ApS, this function aggregates ALL validation errors. If both the context
// and the value fail validation, all errors are collected and returned together.
// This enables comprehensive error reporting for complex nested structures.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type Address struct {
// Street string
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Create a lens for the Address field
// addressLens := lens.MakeLens(
// func(p Person) Address { return p.Address },
// func(p Person, a Address) Person { p.Address = a; return p },
// )
//
// // Use ApSL to update the address
// result := F.Pipe2(
// Success(Person{Name: "Alice"}),
// ApSL(
// addressLens,
// Success(Address{Street: "Main St", City: "NYC"}),
// ),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
fa Validation[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// a Validation that produces the new value.
//
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
// the current value of the focused field.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but fail if it would exceed 100
// increment := func(v int) Validation[int] {
// if v >= 100 {
// return Failures[int](Errors{&ValidationError{Messsage: "exceeds limit"}})
// }
// return Success(v + 1)
// }
//
// result := F.Pipe1(
// Success(Counter{Value: 42}),
// BindL(valueLens, increment),
// ) // Success(Counter{Value: 43})
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return Bind(lens.Set, function.Flow2(lens.Get, f))
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in Validation).
//
// This is useful for pure transformations that cannot fail, such as mathematical operations,
// string manipulations, or other deterministic updates.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// Success(Counter{Value: 21}),
// LetL(valueLens, double),
// ) // Success(Counter{Value: 42})
func LetL[S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
return Let(lens.Set, function.Flow2(lens.Get, f))
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// This is useful for resetting fields, initializing values, or setting fields to
// predetermined constants.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// Success(Config{Debug: true, Timeout: 30}),
// LetToL(debugLens, false),
// ) // Success(Config{Debug: false, Timeout: 30})
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return LetTo(lens.Set, b)
}

View File

@@ -0,0 +1,540 @@
package validation
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
func TestDo(t *testing.T) {
t.Run("creates successful validation with empty state", func(t *testing.T) {
type State struct {
x int
y string
}
result := Do(State{})
assert.Equal(t, Of(State{}), result)
})
t.Run("creates successful validation with initialized state", func(t *testing.T) {
type State struct {
x int
y string
}
initial := State{x: 42, y: "hello"}
result := Do(initial)
assert.Equal(t, Of(initial), result)
})
t.Run("works with different types", func(t *testing.T) {
intResult := Do(0)
assert.Equal(t, Of(0), intResult)
strResult := Do("")
assert.Equal(t, Of(""), strResult)
type Custom struct{ Value int }
customResult := Do(Custom{Value: 100})
assert.Equal(t, Of(Custom{Value: 100}), customResult)
})
}
func TestBind(t *testing.T) {
type State struct {
x int
y int
}
t.Run("binds successful validation to state", func(t *testing.T) {
result := F.Pipe2(
Do(State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Validation[int] { return Success(42) }),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Validation[int] { return Success(10) }),
)
assert.Equal(t, Of(State{x: 42, y: 10}), result)
})
t.Run("propagates failure", func(t *testing.T) {
result := F.Pipe2(
Do(State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Validation[int] { return Success(42) }),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Validation[int] {
return Failures[int](Errors{&ValidationError{Messsage: "y failed"}})
}),
)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "y failed", errors[0].Messsage)
})
t.Run("can access previous state values", func(t *testing.T) {
result := F.Pipe2(
Do(State{}),
Bind(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) Validation[int] { return Success(10) }),
Bind(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) Validation[int] {
// y depends on x
return Success(s.x * 2)
}),
)
assert.Equal(t, Success(State{x: 10, y: 20}), result)
})
}
func TestLet(t *testing.T) {
type State struct {
x int
computed int
}
t.Run("attaches pure computation result to state", func(t *testing.T) {
result := F.Pipe1(
Do(State{x: 5}),
Let(func(c int) func(State) State {
return func(s State) State { s.computed = c; return s }
}, func(s State) int { return s.x * 2 }),
)
assert.Equal(t, Of(State{x: 5, computed: 10}), result)
})
t.Run("preserves failure", func(t *testing.T) {
failure := Failures[State](Errors{&ValidationError{Messsage: "error"}})
result := Let(func(c int) func(State) State {
return func(s State) State { s.computed = c; return s }
}, func(s State) int { return s.x * 2 })(failure)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "error", errors[0].Messsage)
})
t.Run("chains multiple Let operations", func(t *testing.T) {
type State struct {
x int
y int
z int
}
result := F.Pipe3(
Do(State{x: 5}),
Let(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, func(s State) int { return s.x * 2 }),
Let(func(z int) func(State) State {
return func(s State) State { s.z = z; return s }
}, func(s State) int { return s.y + 10 }),
Let(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, func(s State) int { return s.z * 3 }),
)
assert.Equal(t, Of(State{x: 60, y: 10, z: 20}), result)
})
}
func TestLetTo(t *testing.T) {
type State struct {
x int
name string
}
t.Run("attaches constant value to state", func(t *testing.T) {
result := F.Pipe1(
Do(State{x: 5}),
LetTo(func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "example"),
)
assert.Equal(t, Of(State{x: 5, name: "example"}), result)
})
t.Run("preserves failure", func(t *testing.T) {
failure := Failures[State](Errors{&ValidationError{Messsage: "error"}})
result := LetTo(func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "example")(failure)
assert.True(t, either.IsLeft(result))
})
t.Run("sets multiple constant values", func(t *testing.T) {
type State struct {
name string
version int
active bool
}
result := F.Pipe3(
Do(State{}),
LetTo(func(n string) func(State) State {
return func(s State) State { s.name = n; return s }
}, "app"),
LetTo(func(v int) func(State) State {
return func(s State) State { s.version = v; return s }
}, 2),
LetTo(func(a bool) func(State) State {
return func(s State) State { s.active = a; return s }
}, true),
)
assert.Equal(t, Of(State{name: "app", version: 2, active: true}), result)
})
}
func TestBindTo(t *testing.T) {
type State struct {
value int
}
t.Run("initializes state from value", func(t *testing.T) {
result := F.Pipe1(
Success(42),
BindTo(func(x int) State { return State{value: x} }),
)
assert.Equal(t, Of(State{value: 42}), result)
})
t.Run("preserves failure", func(t *testing.T) {
failure := Failures[int](Errors{&ValidationError{Messsage: "error"}})
result := BindTo(func(x int) State { return State{value: x} })(failure)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "error", errors[0].Messsage)
})
t.Run("works with different types", func(t *testing.T) {
type StringState struct {
text string
}
result := F.Pipe1(
Success("hello"),
BindTo(func(s string) StringState { return StringState{text: s} }),
)
assert.Equal(t, Of(StringState{text: "hello"}), result)
})
}
func TestApS(t *testing.T) {
type State struct {
x int
y int
}
t.Run("attaches value using applicative pattern", func(t *testing.T) {
result := F.Pipe1(
Do(State{}),
ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, Success(42)),
)
assert.Equal(t, Of(State{x: 42}), result)
})
t.Run("accumulates errors from both validations", func(t *testing.T) {
stateFailure := Failures[State](Errors{&ValidationError{Messsage: "state error"}})
valueFailure := Failures[int](Errors{&ValidationError{Messsage: "value error"}})
result := ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, valueFailure)(stateFailure)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(State) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "state error")
assert.Contains(t, messages, "value error")
})
t.Run("combines multiple ApS operations", func(t *testing.T) {
result := F.Pipe2(
Do(State{}),
ApS(func(x int) func(State) State {
return func(s State) State { s.x = x; return s }
}, Success(10)),
ApS(func(y int) func(State) State {
return func(s State) State { s.y = y; return s }
}, Success(20)),
)
assert.Equal(t, Of(State{x: 10, y: 20}), result)
})
}
func TestApSL(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
t.Run("updates nested structure using lens", func(t *testing.T) {
addressLens := L.MakeLens(
func(p Person) Address { return p.Address },
func(p Person, a Address) Person { p.Address = a; return p },
)
result := F.Pipe1(
Success(Person{Name: "Alice"}),
ApSL(
addressLens,
Success(Address{Street: "Main St", City: "NYC"}),
),
)
expected := Person{
Name: "Alice",
Address: Address{Street: "Main St", City: "NYC"},
}
assert.Equal(t, Of(expected), result)
})
t.Run("accumulates errors", func(t *testing.T) {
addressLens := L.MakeLens(
func(p Person) Address { return p.Address },
func(p Person, a Address) Person { p.Address = a; return p },
)
personFailure := Failures[Person](Errors{&ValidationError{Messsage: "person error"}})
addressFailure := Failures[Address](Errors{&ValidationError{Messsage: "address error"}})
result := ApSL(addressLens, addressFailure)(personFailure)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(Person) Errors { return nil },
)
assert.Len(t, errors, 2)
})
}
func TestBindL(t *testing.T) {
type Counter struct {
Value int
}
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("updates field based on current value", func(t *testing.T) {
increment := func(v int) Validation[int] {
return Success(v + 1)
}
result := F.Pipe1(
Success(Counter{Value: 42}),
BindL(valueLens, increment),
)
assert.Equal(t, Of(Counter{Value: 43}), result)
})
t.Run("fails validation based on current value", func(t *testing.T) {
increment := func(v int) Validation[int] {
if v >= 100 {
return Failures[int](Errors{&ValidationError{Messsage: "exceeds limit"}})
}
return Success(v + 1)
}
result := F.Pipe1(
Success(Counter{Value: 100}),
BindL(valueLens, increment),
)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(Counter) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "exceeds limit", errors[0].Messsage)
})
t.Run("preserves failure", func(t *testing.T) {
increment := func(v int) Validation[int] {
return Success(v + 1)
}
failure := Failures[Counter](Errors{&ValidationError{Messsage: "error"}})
result := BindL(valueLens, increment)(failure)
assert.True(t, either.IsLeft(result))
})
}
func TestLetL(t *testing.T) {
type Counter struct {
Value int
}
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("transforms field with pure function", func(t *testing.T) {
double := func(v int) int { return v * 2 }
result := F.Pipe1(
Success(Counter{Value: 21}),
LetL(valueLens, double),
)
assert.Equal(t, Of(Counter{Value: 42}), result)
})
t.Run("preserves failure", func(t *testing.T) {
double := func(v int) int { return v * 2 }
failure := Failures[Counter](Errors{&ValidationError{Messsage: "error"}})
result := LetL(valueLens, double)(failure)
assert.True(t, either.IsLeft(result))
})
t.Run("chains multiple transformations", func(t *testing.T) {
add10 := func(v int) int { return v + 10 }
double := func(v int) int { return v * 2 }
result := F.Pipe2(
Success(Counter{Value: 5}),
LetL(valueLens, add10),
LetL(valueLens, double),
)
assert.Equal(t, Of(Counter{Value: 30}), result)
})
}
func TestLetToL(t *testing.T) {
type Config struct {
Debug bool
Timeout int
}
debugLens := L.MakeLens(
func(c Config) bool { return c.Debug },
func(c Config, d bool) Config { c.Debug = d; return c },
)
t.Run("sets field to constant value", func(t *testing.T) {
result := F.Pipe1(
Success(Config{Debug: true, Timeout: 30}),
LetToL(debugLens, false),
)
assert.Equal(t, Of(Config{Debug: false, Timeout: 30}), result)
})
t.Run("preserves failure", func(t *testing.T) {
failure := Failures[Config](Errors{&ValidationError{Messsage: "error"}})
result := LetToL(debugLens, false)(failure)
assert.True(t, either.IsLeft(result))
})
t.Run("sets multiple fields", func(t *testing.T) {
timeoutLens := L.MakeLens(
func(c Config) int { return c.Timeout },
func(c Config, t int) Config { c.Timeout = t; return c },
)
result := F.Pipe2(
Success(Config{Debug: true, Timeout: 30}),
LetToL(debugLens, false),
LetToL(timeoutLens, 60),
)
assert.Equal(t, Of(Config{Debug: false, Timeout: 60}), result)
})
}
func TestBindOperationsComposition(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
t.Run("combines Do, Bind, Let, and LetTo", func(t *testing.T) {
result := F.Pipe4(
Do(User{}),
LetTo(func(n string) func(User) User {
return func(u User) User { u.Name = n; return u }
}, "Alice"),
Bind(func(a int) func(User) User {
return func(u User) User { u.Age = a; return u }
}, func(u User) Validation[int] {
// Age validation
if len(u.Name) > 0 {
return Success(25)
}
return Failures[int](Errors{&ValidationError{Messsage: "name required"}})
}),
Let(func(e string) func(User) User {
return func(u User) User { u.Email = e; return u }
}, func(u User) string {
// Derive email from name
return u.Name + "@example.com"
}),
Bind(func(a int) func(User) User {
return func(u User) User { u.Age = a; return u }
}, func(u User) Validation[int] {
// Validate age is positive
if u.Age > 0 {
return Success(u.Age)
}
return Failures[int](Errors{&ValidationError{Messsage: "age must be positive"}})
}),
)
expected := User{Name: "Alice", Age: 25, Email: "Alice@example.com"}
assert.Equal(t, Of(expected), result)
})
}

View File

@@ -1,10 +1,14 @@
package validation
import (
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/applicative"
)
var errorsMonoid = ErrorsMonoid()
// Of creates a successful validation result containing the given value.
// This is the pure/return operation for the Validation monad.
//
@@ -28,37 +32,376 @@ func Of[A any](a A) Validation[A] {
// return func(age int) User { return User{name, age} }
// }))(validateName))(validateAge)
func Ap[B, A any](fa Validation[A]) Operator[func(A) B, B] {
return either.ApV[B, A](ErrorsMonoid())(fa)
return either.ApV[B, A](errorsMonoid)(fa)
}
// MonadAp applies a validation containing a function to a validation containing a value.
// This is the applicative apply operation that **accumulates errors** from both validations.
//
// **Key behavior**: Unlike Either's MonadAp which fails fast (returns first error),
// this validation-specific implementation **accumulates all errors** using the Errors monoid.
// When both the function validation and value validation fail, all errors from both are combined.
//
// This error accumulation is the defining characteristic of the Validation applicative,
// making it ideal for scenarios where you want to collect all validation failures at once
// rather than stopping at the first error.
//
// Behavior:
// - Both succeed: applies the function to the value → Success(result)
// - Function fails, value succeeds: returns function's errors → Failure(func errors)
// - Function succeeds, value fails: returns value's errors → Failure(value errors)
// - Both fail: **combines all errors** → Failure(func errors + value errors)
//
// This is particularly useful for:
// - Form validation: collect all field errors at once
// - Configuration validation: report all invalid settings together
// - Data validation: accumulate all constraint violations
// - Multi-field validation: validate independent fields in parallel
//
// Example - Both succeed:
//
// double := func(x int) int { return x * 2 }
// result := MonadAp(Of(double), Of(21))
// // Result: Success(42)
//
// Example - Error accumulation (key feature):
//
// funcValidation := Failures[func(int) int](Errors{
// &ValidationError{Messsage: "function error"},
// })
// valueValidation := Failures[int](Errors{
// &ValidationError{Messsage: "value error"},
// })
// result := MonadAp(funcValidation, valueValidation)
// // Result: Failure with BOTH errors: ["function error", "value error"]
//
// Example - Validating multiple fields:
//
// type User struct {
// Name string
// Age int
// }
//
// makeUser := func(name string) func(int) User {
// return func(age int) User { return User{name, age} }
// }
//
// nameValidation := validateName("ab") // Fails: too short
// ageValidation := validateAge(16) // Fails: too young
//
// // First apply name
// step1 := MonadAp(Of(makeUser), nameValidation)
// // Then apply age
// result := MonadAp(step1, ageValidation)
// // Result contains ALL validation errors from both fields
func MonadAp[B, A any](fab Validation[func(A) B], fa Validation[A]) Validation[B] {
return either.MonadApV[B, A](ErrorsMonoid())(fab, fa)
return either.MonadApV[B, A](errorsMonoid)(fab, fa)
}
// Map transforms the value inside a successful validation using the provided function.
// If the validation is a failure, the errors are preserved unchanged.
// This is the functor map operation for Validation.
//
// Example:
// Map is used for transforming successful values without changing the validation context.
// It's the most basic operation for working with validated values and forms the foundation
// for more complex validation pipelines.
//
// Behavior:
// - Success: applies function to value → Success(f(value))
// - Failure: preserves errors unchanged → Failure(same errors)
//
// This is useful for:
// - Type transformations: converting validated values to different types
// - Value transformations: normalizing, formatting, or computing derived values
// - Pipeline composition: chaining multiple transformations
// - Preserving validation context: errors pass through unchanged
//
// Example - Transform successful value:
//
// doubled := Map(func(x int) int { return x * 2 })(Of(21))
// // Result: Success(42)
//
// Example - Failure preserved:
//
// result := Map(func(x int) int { return x * 2 })(
// Failures[int](Errors{&ValidationError{Messsage: "invalid"}}),
// )
// // Result: Failure with same error: ["invalid"]
//
// Example - Type transformation:
//
// toString := Map(func(x int) string { return fmt.Sprintf("%d", x) })
// result := toString(Of(42))
// // Result: Success("42")
//
// Example - Chaining transformations:
//
// result := F.Pipe3(
// Of(5),
// Map(func(x int) int { return x + 10 }), // 15
// Map(func(x int) int { return x * 2 }), // 30
// Map(func(x int) string { return fmt.Sprintf("%d", x) }), // "30"
// )
// // Result: Success("30")
func Map[A, B any](f func(A) B) Operator[A, B] {
return either.Map[Errors](f)
}
// MonadMap transforms the value inside a successful validation using the provided function.
// If the validation is a failure, the errors are preserved unchanged.
// This is the non-curried version of [Map].
//
// MonadMap is useful when you have both the validation and the transformation function
// available at the same time, rather than needing to create a reusable operator.
//
// Behavior:
// - Success: applies function to value → Success(f(value))
// - Failure: preserves errors unchanged → Failure(same errors)
//
// Example - Transform successful value:
//
// result := MonadMap(Of(21), func(x int) int { return x * 2 })
// // Result: Success(42)
//
// Example - Failure preserved:
//
// result := MonadMap(
// Failures[int](Errors{&ValidationError{Messsage: "invalid"}}),
// func(x int) int { return x * 2 },
// )
// // Result: Failure with same error: ["invalid"]
//
// Example - Type transformation:
//
// result := MonadMap(Of(42), func(x int) string {
// return fmt.Sprintf("Value: %d", x)
// })
// // Result: Success("Value: 42")
//
// Example - Computing derived values:
//
// type User struct { FirstName, LastName string }
// result := MonadMap(
// Of(User{"John", "Doe"}),
// func(u User) string { return u.FirstName + " " + u.LastName },
// )
// // Result: Success("John Doe")
func MonadMap[A, B any](fa Validation[A], f func(A) B) Validation[B] {
return either.MonadMap(fa, f)
}
// Chain is the curried version of [MonadChain].
// Sequences two validation computations where the second depends on the first.
//
// Example:
//
// validatePositive := func(x int) Validation[int] {
// if x > 0 { return Success(x) }
// return Failure("must be positive")
// }
// result := Chain(validatePositive)(Success(42)) // Success(42)
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return either.Chain(f)
}
// MonadChain sequences two validation computations where the second depends on the first.
// If the first validation fails, returns the failure without executing the second.
// This is the monadic bind operation for Validation.
//
// Example:
//
// result := MonadChain(
// Success(42),
// func(x int) Validation[string] {
// return Success(fmt.Sprintf("Value: %d", x))
// },
// ) // Success("Value: 42")
func MonadChain[A, B any](fa Validation[A], f Kleisli[A, B]) Validation[B] {
return either.MonadChain(fa, f)
}
// chainErrors is an internal helper that chains error transformations while accumulating errors.
// When the transformation function f returns a failure, it concatenates the original errors (e1)
// with the new errors (e2) using the Errors monoid, ensuring all validation errors are preserved.
func chainErrors[A any](f Kleisli[Errors, A]) func(Errors) Validation[A] {
return func(e1 Errors) Validation[A] {
return either.MonadFold(
f(e1),
function.Flow2(array.Concat(e1), either.Left[A]),
Of[A],
)
}
}
// ChainLeft is the curried version of [MonadChainLeft].
// Returns a function that transforms validation failures while preserving successes.
//
// Unlike the standard Either ChainLeft which replaces errors, this validation-specific
// implementation **aggregates errors** using the Errors monoid. When the transformation
// function returns a failure, both the original errors and the new errors are combined,
// ensuring no validation errors are lost.
//
// This is particularly useful for:
// - Error recovery with fallback validation
// - Adding contextual information to existing errors
// - Transforming error types while preserving all error details
// - Building error handling pipelines that accumulate failures
//
// Key behavior:
// - Success values pass through unchanged
// - When transforming failures, if the transformation also fails, **all errors are aggregated**
// - If the transformation succeeds, it recovers from the original failure
//
// Example - Error recovery with aggregation:
//
// recoverFromNotFound := ChainLeft(func(errs Errors) Validation[int] {
// // Check if this is a "not found" error
// for _, err := range errs {
// if err.Messsage == "not found" {
// return Success(0) // recover with default
// }
// }
// // Add context to existing errors
// return Failures[int](Errors{
// &ValidationError{Messsage: "recovery failed"},
// })
// // Result will contain BOTH original errors AND "recovery failed"
// })
//
// result := recoverFromNotFound(Failures[int](Errors{
// &ValidationError{Messsage: "database error"},
// }))
// // Result contains: ["database error", "recovery failed"]
//
// Example - Adding context to errors:
//
// addContext := ChainLeft(func(errs Errors) Validation[string] {
// // Add contextual information
// return Failures[string](Errors{
// &ValidationError{
// Messsage: "validation failed in user.email field",
// },
// })
// // Original errors are preserved and new context is added
// })
//
// result := F.Pipe1(
// Failures[string](Errors{
// &ValidationError{Messsage: "invalid format"},
// }),
// addContext,
// )
// // Result contains: ["invalid format", "validation failed in user.email field"]
//
// Example - Success values pass through:
//
// handler := ChainLeft(func(errs Errors) Validation[int] {
// return Failures[int](Errors{
// &ValidationError{Messsage: "never called"},
// })
// })
// result := handler(Success(42)) // Success(42) - unchanged
func ChainLeft[A any](f Kleisli[Errors, A]) Operator[A, A] {
return either.Fold(
chainErrors(f),
Of[A],
)
}
// MonadChainLeft sequences a computation on the failure (Left) channel of a Validation.
// If the Validation is a failure, applies the function to transform or recover from the errors.
// If the Validation is a success, returns the success value unchanged.
//
// **Critical difference from Either.MonadChainLeft**: This validation-specific implementation
// **aggregates errors** using the Errors monoid. When the transformation function returns a
// failure, both the original errors and the new errors are combined, ensuring comprehensive
// error reporting.
//
// This is the dual of [MonadChain] - while Chain operates on success values, ChainLeft
// operates on failure values. It's particularly useful for:
// - Error recovery: converting specific errors into successful values
// - Error enrichment: adding context or transforming error messages
// - Fallback logic: providing alternative validations when the first fails
// - Error aggregation: combining multiple validation failures
//
// The function parameter receives the collection of validation errors and must return
// a new Validation[A]. This allows you to:
// - Recover by returning Success(value)
// - Transform errors by returning Failures(newErrors) - **original errors are preserved**
// - Implement conditional error handling based on error content
//
// Example - Error recovery:
//
// result := MonadChainLeft(
// Failures[int](Errors{
// &ValidationError{Messsage: "not found"},
// }),
// func(errs Errors) Validation[int] {
// // Check if we can recover
// for _, err := range errs {
// if err.Messsage == "not found" {
// return Success(0) // recover with default value
// }
// }
// return Failures[int](errs) // propagate errors
// },
// ) // Success(0)
//
// Example - Error aggregation (key feature):
//
// result := MonadChainLeft(
// Failures[string](Errors{
// &ValidationError{Messsage: "error 1"},
// &ValidationError{Messsage: "error 2"},
// }),
// func(errs Errors) Validation[string] {
// // Transformation also fails
// return Failures[string](Errors{
// &ValidationError{Messsage: "error 3"},
// })
// },
// )
// // Result contains ALL errors: ["error 1", "error 2", "error 3"]
// // This is different from Either.MonadChainLeft which would only keep "error 3"
//
// Example - Adding context to errors:
//
// result := MonadChainLeft(
// Failures[int](Errors{
// &ValidationError{Value: "abc", Messsage: "invalid number"},
// }),
// func(errs Errors) Validation[int] {
// // Add contextual information
// contextErrors := Errors{
// &ValidationError{
// Context: []ContextEntry{{Key: "user", Type: "User"}, {Key: "age", Type: "int"}},
// Messsage: "failed to parse user age",
// },
// }
// return Failures[int](contextErrors)
// },
// )
// // Result contains both original error and context:
// // ["invalid number", "failed to parse user age"]
//
// Example - Success values pass through:
//
// result := MonadChainLeft(
// Success(42),
// func(errs Errors) Validation[int] {
// return Failures[int](Errors{
// &ValidationError{Messsage: "never called"},
// })
// },
// ) // Success(42) - unchanged
func MonadChainLeft[A any](fa Validation[A], f Kleisli[Errors, A]) Validation[A] {
return either.MonadFold(
fa,
chainErrors(f),
Of[A],
)
}
// Applicative creates an Applicative instance for Validation with error accumulation.
//
// This returns a lawful Applicative that accumulates validation errors using the Errors monoid.
@@ -123,6 +466,176 @@ func MonadChain[A, B any](fa Validation[A], f Kleisli[A, B]) Validation[B] {
// An Applicative instance with Of, Map, and Ap operations that accumulate errors
func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Validation[B], Validation[func(A) B]] {
return either.ApplicativeV[Errors, A, B](
ErrorsMonoid(),
errorsMonoid,
)
}
//go:inline
func OrElse[A any](f Kleisli[Errors, A]) Operator[A, A] {
return ChainLeft(f)
}
// MonadAlt implements the Alternative operation for Validation, providing fallback behavior.
// If the first validation fails, it evaluates and returns the second validation as an alternative.
// If the first validation succeeds, it returns the first validation without evaluating the second.
//
// This is the fundamental operation for the Alt typeclass, enabling "try first, fallback to second"
// semantics. It's particularly useful for:
// - Providing default values when validation fails
// - Trying multiple validation strategies in sequence
// - Building validation pipelines with fallback logic
// - Implementing optional validation with defaults
//
// **Key behavior**: When both validations fail, MonadAlt DOES accumulate errors from both
// validations using the Errors monoid. This is different from standard Either Alt behavior.
// The error accumulation happens through the underlying ChainLeft/chainErrors mechanism.
//
// The second parameter is lazy (Lazy[Validation[A]]) to avoid unnecessary computation when
// the first validation succeeds. The second validation is only evaluated if needed.
//
// Behavior:
// - First succeeds: returns first validation (second is not evaluated)
// - First fails, second succeeds: returns second validation
// - Both fail: aggregates errors from both validations
//
// This is useful for:
// - Fallback values: provide defaults when primary validation fails
// - Alternative strategies: try different validation approaches
// - Optional validation: make validation optional with a default
// - Chaining attempts: try multiple sources until one succeeds
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - first: The primary validation to try
// - second: A lazy computation producing the fallback validation (only evaluated if first fails)
//
// Returns:
//
// The first validation if it succeeds, otherwise the second validation
//
// Example - Fallback to default:
//
// primary := parseConfig("config.json") // Fails
// fallback := func() Validation[Config] {
// return Success(defaultConfig)
// }
// result := MonadAlt(primary, fallback)
// // Result: Success(defaultConfig)
//
// Example - First succeeds (second not evaluated):
//
// primary := Success(42)
// fallback := func() Validation[int] {
// panic("never called") // This won't execute
// }
// result := MonadAlt(primary, fallback)
// // Result: Success(42)
//
// Example - Chaining multiple alternatives:
//
// result := MonadAlt(
// parseFromEnv("API_KEY"),
// func() Validation[string] {
// return MonadAlt(
// parseFromFile(".env"),
// func() Validation[string] {
// return Success("default-key")
// },
// )
// },
// )
// // Tries: env var → file → default (uses first that succeeds)
//
// Example - Error accumulation when both fail:
//
// v1 := Failures[int](Errors{
// &ValidationError{Messsage: "error 1"},
// &ValidationError{Messsage: "error 2"},
// })
// v2 := func() Validation[int] {
// return Failures[int](Errors{
// &ValidationError{Messsage: "error 3"},
// })
// }
// result := MonadAlt(v1, v2)
// // Result: Failures with ALL errors ["error 1", "error 2", "error 3"]
// // The errors from v1 are aggregated with errors from v2
func MonadAlt[A any](first Validation[A], second Lazy[Validation[A]]) Validation[A] {
return MonadChainLeft(first, function.Ignore1of1[Errors](second))
}
// Alt is the curried version of [MonadAlt].
// Returns a function that provides fallback behavior for a Validation.
//
// This is useful for creating reusable fallback operators that can be applied
// to multiple validations, or for use in function composition pipelines.
//
// The returned function takes a validation and returns either that validation
// (if successful) or the provided alternative (if the validation fails).
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - second: A lazy computation producing the fallback validation
//
// Returns:
//
// A function that takes a Validation[A] and returns a Validation[A] with fallback behavior
//
// Example - Creating a reusable fallback operator:
//
// withDefault := Alt(func() Validation[int] {
// return Success(0)
// })
//
// result1 := withDefault(parseNumber("42")) // Success(42)
// result2 := withDefault(parseNumber("abc")) // Success(0) - fallback
// result3 := withDefault(parseNumber("123")) // Success(123)
//
// Example - Using in a pipeline:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe2(
// parseFromEnv("CONFIG_PATH"),
// Alt(func() Validation[string] {
// return parseFromFile("config.json")
// }),
// Alt(func() Validation[string] {
// return Success("./default-config.json")
// }),
// )
// // Tries: env var → file → default path
//
// Example - Combining with Map:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe2(
// validatePositive(-5), // Fails
// Alt(func() Validation[int] { return Success(1) }),
// Map(func(x int) int { return x * 2 }),
// )
// // Result: Success(2) - uses fallback value 1, then doubles it
//
// Example - Multiple fallback layers:
//
// primaryFallback := Alt(func() Validation[Config] {
// return loadFromFile("backup.json")
// })
// secondaryFallback := Alt(func() Validation[Config] {
// return Success(defaultConfig)
// })
//
// result := F.Pipe2(
// loadFromFile("config.json"),
// primaryFallback,
// secondaryFallback,
// )
// // Tries: config.json → backup.json → default
func Alt[A any](second Lazy[Validation[A]]) Operator[A, A] {
return ChainLeft(function.Ignore1of1[Errors](second))
}

File diff suppressed because it is too large Load Diff

View File

@@ -52,3 +52,177 @@ func ApplicativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
m,
)
}
// AlternativeMonoid creates a Monoid instance for Validation[A] using the Alternative pattern.
// This combines the applicative error-accumulation behavior with the alternative fallback behavior,
// allowing you to both accumulate errors and provide fallback alternatives.
//
// The Alternative pattern provides two key operations:
// - Applicative operations (Of, Map, Ap): accumulate errors when combining validations
// - Alternative operation (Alt): provide fallback when a validation fails
//
// This monoid is particularly useful when you want to:
// - Try multiple validation strategies and fall back to alternatives
// - Combine successful values using the provided monoid
// - Accumulate all errors from failed attempts
// - Build validation pipelines with fallback logic
//
// The resulting monoid:
// - Empty: Returns a successful validation with the empty value from the inner monoid
// - Concat: Combines two validations using both applicative and alternative semantics:
// - If first succeeds and second succeeds: combines values using inner monoid
// - If first fails: tries second as fallback (alternative behavior)
// - If both fail: accumulates all errors
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - m: The monoid for combining successful values of type A
//
// Returns:
//
// A Monoid[Validation[A]] that combines applicative and alternative behaviors
//
// Example - Combining successful validations:
//
// import "github.com/IBM/fp-go/v2/string"
//
// m := AlternativeMonoid(string.Monoid)
// v1 := Success("Hello")
// v2 := Success(" World")
// result := m.Concat(v1, v2)
// // Result: Success("Hello World")
//
// Example - Fallback behavior:
//
// m := AlternativeMonoid(string.Monoid)
// v1 := Failures[string](Errors{&ValidationError{Messsage: "first failed"}})
// v2 := Success("fallback value")
// result := m.Concat(v1, v2)
// // Result: Success("fallback value") - second validation used as fallback
//
// Example - Error accumulation when both fail:
//
// m := AlternativeMonoid(string.Monoid)
// v1 := Failures[string](Errors{&ValidationError{Messsage: "error 1"}})
// v2 := Failures[string](Errors{&ValidationError{Messsage: "error 2"}})
// result := m.Concat(v1, v2)
// // Result: Failures with accumulated errors: ["error 1", "error 2"]
//
// Example - Building validation with fallbacks:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// m := AlternativeMonoid(N.MonoidSum[int]())
//
// // Try to parse from different sources
// fromEnv := parseFromEnv() // Fails
// fromConfig := parseFromConfig() // Succeeds with 42
// fromDefault := Success(0) // Default fallback
//
// result := m.Concat(m.Concat(fromEnv, fromConfig), fromDefault)
// // Result: Success(42) - uses first successful validation
func AlternativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
return M.AlternativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadAp[A, A],
MonadAlt[A],
m,
)
}
// AltMonoid creates a Monoid instance for Validation[A] using the Alt (alternative) operation.
// This monoid provides a way to combine validations with fallback behavior, where the second
// validation is used as an alternative if the first one fails.
//
// The Alt operation implements the "try first, fallback to second" pattern, which is useful
// for validation scenarios where you want to attempt multiple validation strategies in sequence
// and use the first one that succeeds.
//
// The resulting monoid:
// - Empty: Returns the provided zero value (a lazy computation that produces a Validation[A])
// - Concat: Combines two validations using Alt semantics:
// - If first succeeds: returns the first validation (ignores second)
// - If first fails: returns the second validation as fallback
//
// This is different from [AlternativeMonoid] in that:
// - AltMonoid uses a custom zero value (provided by the user)
// - AlternativeMonoid derives the zero from an inner monoid
// - AltMonoid is simpler and only provides fallback behavior
// - AlternativeMonoid combines applicative and alternative behaviors
//
// Type Parameters:
// - A: The type of the successful value
//
// Parameters:
// - zero: A lazy computation that produces the identity/empty Validation[A].
// This is typically a successful validation with a default value, or could be
// a failure representing "no validation attempted"
//
// Returns:
//
// A Monoid[Validation[A]] that combines validations with fallback behavior
//
// Example - Using default value as zero:
//
// m := AltMonoid(func() Validation[int] { return Success(0) })
//
// v1 := Failures[int](Errors{&ValidationError{Messsage: "failed"}})
// v2 := Success(42)
//
// result := m.Concat(v1, v2)
// // Result: Success(42) - falls back to second validation
//
// empty := m.Empty()
// // Result: Success(0) - the provided zero value
//
// Example - Chaining multiple fallbacks:
//
// m := AltMonoid(func() Validation[string] {
// return Success("default")
// })
//
// primary := parseFromPrimarySource() // Fails
// secondary := parseFromSecondary() // Fails
// tertiary := parseFromTertiary() // Succeeds with "value"
//
// result := m.Concat(m.Concat(primary, secondary), tertiary)
// // Result: Success("value") - uses first successful validation
//
// Example - All validations fail:
//
// m := AltMonoid(func() Validation[int] {
// return Failures[int](Errors{&ValidationError{Messsage: "no default"}})
// })
//
// v1 := Failures[int](Errors{&ValidationError{Messsage: "error 1"}})
// v2 := Failures[int](Errors{&ValidationError{Messsage: "error 2"}})
//
// result := m.Concat(v1, v2)
// // Result: Failures with errors from v2: ["error 2"]
// // Note: Unlike AlternativeMonoid, errors are NOT accumulated
//
// Example - Building a validation pipeline with fallbacks:
//
// m := AltMonoid(func() Validation[Config] {
// return Success(defaultConfig)
// })
//
// // Try multiple configuration sources in order
// configs := []Validation[Config]{
// loadFromFile("config.json"), // Try file first
// loadFromEnv(), // Then environment
// loadFromRemote("api.example.com"), // Then remote API
// }
//
// // Fold using the monoid to get first successful config
// result := A.MonoidFold(m)(configs)
// // Result: First successful config, or defaultConfig if all fail
func AltMonoid[A any](zero Lazy[Validation[A]]) Monoid[Validation[A]] {
return M.AltMonoid(
zero,
MonadAlt[A],
)
}

View File

@@ -74,12 +74,7 @@ func TestApplicativeMonoid(t *testing.T) {
t.Run("empty returns successful validation with empty string", func(t *testing.T) {
empty := m.Empty()
assert.True(t, either.IsRight(empty))
value := either.MonadFold(empty,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
assert.Equal(t, Success(""), empty)
})
t.Run("concat combines successful validations", func(t *testing.T) {
@@ -88,12 +83,7 @@ func TestApplicativeMonoid(t *testing.T) {
result := m.Concat(v1, v2)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "Hello World", value)
assert.Equal(t, Success("Hello World"), result)
})
t.Run("concat with failure returns failure", func(t *testing.T) {
@@ -141,17 +131,8 @@ func TestApplicativeMonoid(t *testing.T) {
result1 := m.Concat(v, empty)
result2 := m.Concat(empty, v)
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
assert.Equal(t, Of("test"), result1)
assert.Equal(t, Of("test"), result2)
})
})
@@ -166,11 +147,7 @@ func TestApplicativeMonoid(t *testing.T) {
t.Run("empty returns zero", func(t *testing.T) {
empty := m.Empty()
value := either.MonadFold(empty,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
assert.Equal(t, Of(0), empty)
})
t.Run("concat adds values", func(t *testing.T) {
@@ -179,11 +156,7 @@ func TestApplicativeMonoid(t *testing.T) {
result := m.Concat(v1, v2)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Of(42), result)
})
t.Run("multiple concat operations", func(t *testing.T) {
@@ -194,11 +167,7 @@ func TestApplicativeMonoid(t *testing.T) {
result := m.Concat(m.Concat(m.Concat(v1, v2), v3), v4)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
assert.Equal(t, Of(10), result)
})
})
}
@@ -245,21 +214,13 @@ func TestMonoidLaws(t *testing.T) {
t.Run("left identity", func(t *testing.T) {
// empty + a = a
result := m.Concat(m.Empty(), v1)
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
assert.Equal(t, Of("a"), result)
})
t.Run("right identity", func(t *testing.T) {
// a + empty = a
result := m.Concat(v1, m.Empty())
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
assert.Equal(t, Of("a"), result)
})
t.Run("associativity", func(t *testing.T) {
@@ -268,17 +229,8 @@ func TestMonoidLaws(t *testing.T) {
left := m.Concat(m.Concat(v1, v2), v3)
right := m.Concat(v1, m.Concat(v2, v3))
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
assert.Equal(t, Of("abc"), left)
assert.Equal(t, Of("abc"), right)
})
})
}
@@ -332,11 +284,7 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
result := m.Concat(v1, v2)
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, 15, value.Count)
assert.Equal(t, Of(Counter{Count: 15}), result)
})
t.Run("empty concat empty", func(t *testing.T) {
@@ -344,10 +292,6 @@ func TestApplicativeMonoidEdgeCases(t *testing.T) {
result := m.Concat(m.Empty(), m.Empty())
value := either.MonadFold(result,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
assert.Equal(t, Of(""), result)
})
}

View File

@@ -1,7 +1,24 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validation
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -9,13 +26,35 @@ import (
type (
// Result represents a computation that may succeed with a value of type A or fail with an error.
// This is an alias for result.Result[A], which is Either[error, A].
//
// Used for converting validation results to standard Go error handling patterns.
Result[A any] = result.Result[A]
// Either represents a value that can be one of two types: Left (error) or Right (success).
// This is an alias for either.Either[E, A], a disjoint union type.
//
// In the validation context:
// - Left[E]: Contains error information of type E
// - Right[A]: Contains a successfully validated value of type A
Either[E, A any] = either.Either[E, A]
// ContextEntry represents a single entry in the validation context path.
// It tracks the location and type information during nested validation.
// It tracks the location and type information during nested validation,
// enabling precise error reporting with full path information.
//
// Fields:
// - Key: The key or field name (e.g., "email", "address", "items[0]")
// - Type: The expected type name (e.g., "string", "int", "User")
// - Actual: The actual value being validated (for error reporting)
//
// Example:
//
// entry := ContextEntry{
// Key: "user.email",
// Type: "string",
// Actual: 12345,
// }
ContextEntry struct {
Key string // The key or field name (for objects/maps)
Type string // The expected type name
@@ -23,42 +62,202 @@ type (
}
// Context is a stack of ContextEntry values representing the path through
// nested structures during validation. Used to provide detailed error messages.
// nested structures during validation. Used to provide detailed error messages
// that show exactly where in a nested structure a validation failure occurred.
//
// The context builds up as validation descends into nested structures:
// - [] - root level
// - [{Key: "user"}] - inside user object
// - [{Key: "user"}, {Key: "address"}] - inside user.address
// - [{Key: "user"}, {Key: "address"}, {Key: "zipCode"}] - at user.address.zipCode
//
// Example:
//
// ctx := Context{
// {Key: "user", Type: "User"},
// {Key: "address", Type: "Address"},
// {Key: "zipCode", Type: "string"},
// }
// // Represents path: user.address.zipCode
Context = []ContextEntry
// ValidationError represents a single validation failure with context.
// ValidationError represents a single validation failure with full context information.
// It implements the error interface and provides detailed information about what failed,
// where it failed, and why it failed.
//
// Fields:
// - Value: The actual value that failed validation
// - Context: The path to the value in nested structures (e.g., user.address.zipCode)
// - Messsage: Human-readable error description
// - Cause: Optional underlying error that caused the validation failure
//
// The ValidationError type implements:
// - error interface: For standard Go error handling
// - fmt.Formatter: For custom formatting with %v, %+v
// - slog.LogValuer: For structured logging with slog
//
// Example:
//
// err := &ValidationError{
// Value: "not-an-email",
// Context: []ContextEntry{{Key: "user"}, {Key: "email"}},
// Messsage: "invalid email format",
// Cause: nil,
// }
// fmt.Printf("%v", err) // at user.email: invalid email format
// fmt.Printf("%+v", err) // at user.email: invalid email format
// // value: "not-an-email"
ValidationError struct {
Value any // The value that failed validation
Context Context // The path to the value in nested structures
Messsage string // Human-readable error message
Cause error
Cause error // Optional underlying error cause
}
// Errors is a collection of validation errors.
// This type is used to accumulate multiple validation failures,
// allowing all errors to be reported at once rather than failing fast.
//
// Example:
//
// errors := Errors{
// &ValidationError{Value: "", Messsage: "name is required"},
// &ValidationError{Value: "invalid", Messsage: "invalid email"},
// &ValidationError{Value: -1, Messsage: "age must be positive"},
// }
Errors = []*ValidationError
// validationErrors wraps a collection of validation errors with an optional root cause.
// It provides structured error information for validation failures.
// It provides structured error information for validation failures and implements
// the error interface for integration with standard Go error handling.
//
// This type is internal and created via MakeValidationErrors.
// It implements:
// - error interface: For standard Go error handling
// - fmt.Formatter: For custom formatting with %v, %+v
// - slog.LogValuer: For structured logging with slog
//
// Fields:
// - errors: The collection of individual validation errors
// - cause: Optional root cause error
validationErrors struct {
errors Errors
cause error
}
// Validation represents the result of a validation operation.
// Left contains validation errors, Right contains the successfully validated value.
// It's an Either type where:
// - Left(Errors): Validation failed with one or more errors
// - Right(A): Successfully validated value of type A
//
// This type supports applicative operations, allowing multiple validations
// to be combined while accumulating all errors rather than failing fast.
//
// Example:
//
// // Success case
// valid := Success(42) // Right(42)
//
// // Failure case
// invalid := Failures[int](Errors{
// &ValidationError{Messsage: "must be positive"},
// }) // Left([...])
//
// // Combining validations (accumulates all errors)
// result := Ap(Ap(Of(func(x int) func(y int) int {
// return func(y int) int { return x + y }
// }))(validateX))(validateY)
Validation[A any] = Either[Errors, A]
// Reader represents a computation that depends on an environment R and produces a value A.
// This is an alias for reader.Reader[R, A], which is func(R) A.
//
// In the validation context, Reader is used for context-dependent validation operations
// where the validation logic needs access to the current validation context path.
//
// Example:
//
// validateWithContext := func(ctx Context) Validation[int] {
// // Use ctx to provide detailed error messages
// return Success(42)
// }
Reader[R, A any] = reader.Reader[R, A]
// Kleisli represents a function from A to a validated B.
// It's a Reader that takes an input A and produces a Validation[B].
// This is the fundamental building block for composable validation operations.
//
// Type: func(A) Validation[B]
//
// Kleisli arrows can be composed using Chain/Bind operations to build
// complex validation pipelines from simple validation functions.
//
// Example:
//
// validatePositive := func(x int) Validation[int] {
// if x > 0 {
// return Success(x)
// }
// return Failures[int](/* error */)
// }
//
// validateEven := func(x int) Validation[int] {
// if x%2 == 0 {
// return Success(x)
// }
// return Failures[int](/* error */)
// }
//
// // Compose validations
// validatePositiveEven := Chain(validateEven)(Success(42))
Kleisli[A, B any] = Reader[A, Validation[B]]
// Operator represents a validation transformation that takes a validated A and produces a validated B.
// It's a specialized Kleisli arrow for composing validation operations.
// It's a specialized Kleisli arrow for composing validation operations where the input
// is already a Validation[A].
//
// Type: func(Validation[A]) Validation[B]
//
// Operators are used to transform and compose validation results, enabling
// functional composition of validation pipelines.
//
// Example:
//
// // Transform a validated int to a validated string
// intToString := Map(func(x int) string {
// return strconv.Itoa(x)
// }) // Operator[int, string]
//
// result := intToString(Success(42)) // Success("42")
Operator[A, B any] = Kleisli[Validation[A], B]
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
// This is an alias for monoid.Monoid[A].
//
// In the validation context, monoids are used to combine validation results:
// - ApplicativeMonoid: Combines successful validations using the monoid operation
// - AlternativeMonoid: Provides fallback behavior for failed validations
//
// Example:
//
// import N "github.com/IBM/fp-go/v2/number"
//
// intAdd := N.MonoidSum[int]()
// m := ApplicativeMonoid(intAdd)
// result := m.Concat(Success(5), Success(3)) // Success(8)
Monoid[A any] = monoid.Monoid[A]
// Endomorphism represents a function from a type to itself: func(A) A.
// This is an alias for endomorphism.Endomorphism[A].
//
// In the validation context, endomorphisms are used with LetL to transform
// values within a validation context using pure functions.
//
// Example:
//
// double := func(x int) int { return x * 2 } // Endomorphism[int]
// result := LetL(lens, double)(Success(21)) // Success(42)
Endomorphism[A any] = endomorphism.Endomorphism[A]
Lazy[A any] = lazy.Lazy[A]
)

View File

@@ -186,12 +186,7 @@ func TestSuccess(t *testing.T) {
t.Run("creates right either with value", func(t *testing.T) {
result := Success(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, Success(42), result)
})
t.Run("works with different types", func(t *testing.T) {
@@ -327,7 +322,7 @@ func TestValidationIntegration(t *testing.T) {
&ValidationError{Value: "bad", Messsage: "error"},
})
assert.True(t, either.IsRight(success))
assert.Equal(t, Success(42), success)
assert.True(t, either.IsLeft(failure))
})
@@ -512,12 +507,7 @@ func TestToResult(t *testing.T) {
result := ToResult(validation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(error) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
assert.Equal(t, either.Of[error](42), result)
})
t.Run("converts failed validation to result with error", func(t *testing.T) {
@@ -569,23 +559,18 @@ func TestToResult(t *testing.T) {
// String type
strValidation := Success("hello")
strResult := ToResult(strValidation)
assert.True(t, either.IsRight(strResult))
assert.Equal(t, either.Of[error]("hello"), strResult)
// Bool type
boolValidation := Success(true)
boolResult := ToResult(boolValidation)
assert.True(t, either.IsRight(boolResult))
assert.Equal(t, either.Of[error](true), boolResult)
// Struct type
type User struct{ Name string }
userValidation := Success(User{Name: "Alice"})
userResult := ToResult(userValidation)
assert.True(t, either.IsRight(userResult))
user := either.MonadFold(userResult,
func(error) User { return User{} },
F.Identity[User],
)
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, either.Of[error](User{Name: "Alice"}), userResult)
})
t.Run("preserves error context in result", func(t *testing.T) {

View File

@@ -0,0 +1,153 @@
// 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 prism
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
P "github.com/IBM/fp-go/v2/optics/prism"
)
// Compose creates a Kleisli arrow that composes a prism with an isomorphism.
//
// This function takes a Prism[A, B] and returns a Kleisli arrow that can transform
// any Iso[S, A] into a Prism[S, B]. The resulting prism changes the source type from
// A to S using the bidirectional transformation provided by the isomorphism, while
// maintaining the same focus type B.
//
// The composition works as follows:
// - GetOption: First transforms S to A using the iso's Get, then extracts B from A using the prism's GetOption
// - ReverseGet: First constructs A from B using the prism's ReverseGet, then transforms A to S using the iso's ReverseGet
//
// This is the dual operation of optics/prism/iso.Compose:
// - optics/prism/iso.Compose: Transforms the focus type (A → B) while keeping source type (S) constant
// - optics/iso/prism.Compose: Transforms the source type (A → S) while keeping focus type (B) constant
//
// This is particularly useful when you have a prism that works with one type but you
// need to adapt it to work with a different source type that has a lossless bidirectional
// transformation to the original type.
//
// Type Parameters:
// - S: The new source type after applying the isomorphism
// - A: The original source type of the prism
// - B: The focus type (remains constant through composition)
//
// Parameters:
// - ab: A prism that extracts B from A
//
// Returns:
// - A Kleisli arrow (function) that takes an Iso[S, A] and returns a Prism[S, B]
//
// Laws:
// The composed prism must satisfy the prism laws:
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
//
// These laws are preserved because:
// - The isomorphism satisfies: ia.ReverseGet(ia.Get(s)) == s and ia.Get(ia.ReverseGet(a)) == a
// - The original prism satisfies the prism laws
//
// Haskell Equivalent:
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
// specifically when composing an Iso with a Prism:
//
// iso . prism :: Iso s a -> Prism a b -> Prism s b
//
// In Haskell's lens library, this is part of the general optic composition mechanism.
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Iso.html
//
// Example - Composing with Either prism:
//
// import (
// "github.com/IBM/fp-go/v2/either"
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Create an isomorphism between []byte and string
// bytesStringIso := iso.MakeIso(
// func(b []byte) string { return string(b) },
// func(s string) []byte { return []byte(s) },
// )
//
// // Compose them to get a prism that works with []byte as source
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
//
// // Use the composed prism
// // First converts []byte to string via iso, then extracts Right value
// bytes := []byte("hello")
// either := either.Right[error](string(bytes))
// result := bytesPrism.GetOption(bytes) // Extracts "hello" if Right
//
// // Construct []byte from string
// constructed := bytesPrism.ReverseGet("world")
// // Returns []byte("world") wrapped in Right
//
// Example - Composing with custom types:
//
// type JSON []byte
// type Config struct {
// Host string
// Port int
// }
//
// // Isomorphism between JSON and []byte
// jsonIso := iso.MakeIso(
// func(j JSON) []byte { return []byte(j) },
// func(b []byte) JSON { return JSON(b) },
// )
//
// // Prism that extracts Config from []byte (via JSON parsing)
// configPrism := prism.MakePrism(
// func(b []byte) option.Option[Config] {
// var cfg Config
// if err := json.Unmarshal(b, &cfg); err != nil {
// return option.None[Config]()
// }
// return option.Some(cfg)
// },
// func(cfg Config) []byte {
// b, _ := json.Marshal(cfg)
// return b
// },
// )
//
// // Compose to work with JSON type instead of []byte
// jsonConfigPrism := IP.Compose(configPrism)(jsonIso)
//
// jsonData := JSON(`{"host":"localhost","port":8080}`)
// config := jsonConfigPrism.GetOption(jsonData)
// // config is Some(Config{Host: "localhost", Port: 8080})
//
// See also:
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
// - github.com/IBM/fp-go/v2/optics/prism/iso for the dual composition (transforming focus type)
func Compose[S, A, B any](ab Prism[A, B]) P.Kleisli[S, Iso[S, A], B] {
return func(ia Iso[S, A]) Prism[S, B] {
return P.MakePrismWithName(
F.Flow2(ia.Get, ab.GetOption),
F.Flow2(ab.ReverseGet, ia.ReverseGet),
fmt.Sprintf("IsoCompose[%s -> %s]", ia, ab),
)
}
}

View File

@@ -0,0 +1,435 @@
// 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 prism
import (
"encoding/json"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestComposeWithEitherPrism tests composing a prism with an isomorphism using Either
func TestComposeWithEitherPrism(t *testing.T) {
// Create a prism that extracts Right values from Either[error, string]
rightPrism := P.FromEither[error, string]()
// Create an isomorphism between []byte and Either[error, string]
bytesEitherIso := I.MakeIso(
func(b []byte) E.Either[error, string] {
return E.Right[error](string(b))
},
func(e E.Either[error, string]) []byte {
return []byte(E.GetOrElse(func(error) string { return "" })(e))
},
)
// Compose them: Prism[Either, string] with Iso[[]byte, Either] -> Prism[[]byte, string]
bytesPrism := Compose[[]byte](rightPrism)(bytesEitherIso)
t.Run("GetOption extracts string from []byte", func(t *testing.T) {
bytes := []byte("hello")
result := bytesPrism.GetOption(bytes)
assert.True(t, O.IsSome(result))
str := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, "hello", str)
})
t.Run("ReverseGet constructs []byte from string", func(t *testing.T) {
value := "world"
result := bytesPrism.ReverseGet(value)
assert.Equal(t, []byte("world"), result)
})
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
original := "test"
// ReverseGet to create []byte
bytes := bytesPrism.ReverseGet(original)
// GetOption to extract string back
result := bytesPrism.GetOption(bytes)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, original, extracted)
})
}
// TestComposeWithOptionPrism tests composing a prism with an isomorphism using Option
func TestComposeWithOptionPrism(t *testing.T) {
// Create a prism that extracts Some values from Option[int]
somePrism := P.FromOption[int]()
// Create an isomorphism between string and Option[int]
stringOptionIso := I.MakeIso(
func(s string) O.Option[int] {
i, err := strconv.Atoi(s)
if err != nil {
return O.None[int]()
}
return O.Some(i)
},
func(opt O.Option[int]) string {
return strconv.Itoa(O.GetOrElse(F.Constant(0))(opt))
},
)
// Compose them: Prism[Option, int] with Iso[string, Option] -> Prism[string, int]
stringPrism := Compose[string](somePrism)(stringOptionIso)
t.Run("GetOption extracts int from valid string", func(t *testing.T) {
result := stringPrism.GetOption("42")
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 42, num)
})
t.Run("GetOption returns None for invalid string", func(t *testing.T) {
result := stringPrism.GetOption("invalid")
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs string from int", func(t *testing.T) {
result := stringPrism.ReverseGet(100)
assert.Equal(t, "100", result)
})
}
// Custom types for testing
type Celsius float64
type Fahrenheit float64
type Temperature interface {
isTemperature()
}
type CelsiusTemp struct {
Value Celsius
}
func (c CelsiusTemp) isTemperature() {}
type FahrenheitTemp struct {
Value Fahrenheit
}
func (f FahrenheitTemp) isTemperature() {}
// TestComposeWithCustomPrism tests composing with custom types
func TestComposeWithCustomPrism(t *testing.T) {
// Prism that extracts Celsius from Temperature
celsiusPrism := P.MakePrism(
func(t Temperature) O.Option[Celsius] {
if ct, ok := t.(CelsiusTemp); ok {
return O.Some(ct.Value)
}
return O.None[Celsius]()
},
func(c Celsius) Temperature {
return CelsiusTemp{Value: c}
},
)
// Isomorphism between Fahrenheit and Temperature
fahrenheitTempIso := I.MakeIso(
func(f Fahrenheit) Temperature {
celsius := Celsius((f - 32) * 5 / 9)
return CelsiusTemp{Value: celsius}
},
func(t Temperature) Fahrenheit {
if ct, ok := t.(CelsiusTemp); ok {
return Fahrenheit(ct.Value*9/5 + 32)
}
return 0
},
)
// Compose: Prism[Temperature, Celsius] with Iso[Fahrenheit, Temperature] -> Prism[Fahrenheit, Celsius]
fahrenheitPrism := Compose[Fahrenheit](celsiusPrism)(fahrenheitTempIso)
t.Run("GetOption extracts Celsius from Fahrenheit", func(t *testing.T) {
fahrenheit := Fahrenheit(68)
result := fahrenheitPrism.GetOption(fahrenheit)
assert.True(t, O.IsSome(result))
celsius := O.GetOrElse(F.Constant(Celsius(0)))(result)
assert.InDelta(t, 20.0, float64(celsius), 0.01)
})
t.Run("ReverseGet constructs Fahrenheit from Celsius", func(t *testing.T) {
celsius := Celsius(20)
result := fahrenheitPrism.ReverseGet(celsius)
assert.InDelta(t, 68.0, float64(result), 0.01)
})
t.Run("Round-trip preserves value", func(t *testing.T) {
original := Celsius(25)
// ReverseGet to create Fahrenheit
fahrenheit := fahrenheitPrism.ReverseGet(original)
// GetOption to extract Celsius back
result := fahrenheitPrism.GetOption(fahrenheit)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(Celsius(0)))(result)
assert.InDelta(t, float64(original), float64(extracted), 0.01)
})
}
// TestComposeIdentityIso tests composing with an identity isomorphism
func TestComposeIdentityIso(t *testing.T) {
// Prism that extracts Right values
rightPrism := P.FromEither[error, string]()
// Identity isomorphism on Either
idIso := I.Id[E.Either[error, string]]()
// Compose with identity should not change behavior
composedPrism := Compose[E.Either[error, string]](rightPrism)(idIso)
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
either := E.Right[error]("test")
// Original prism
originalResult := rightPrism.GetOption(either)
// Composed prism
composedResult := composedPrism.GetOption(either)
assert.Equal(t, originalResult, composedResult)
})
t.Run("ReverseGet produces same result", func(t *testing.T) {
value := "test"
// Original prism
originalResult := rightPrism.ReverseGet(value)
// Composed prism
composedResult := composedPrism.ReverseGet(value)
assert.Equal(t, originalResult, composedResult)
})
}
// TestComposeChaining tests chaining multiple compositions
func TestComposeChaining(t *testing.T) {
// Prism: extracts Right values from Either[error, int]
rightPrism := P.FromEither[error, int]()
// Iso 1: string to Either[error, int]
stringEitherIso := I.MakeIso(
func(s string) E.Either[error, int] {
i, err := strconv.Atoi(s)
if err != nil {
return E.Left[int](err)
}
return E.Right[error](i)
},
func(e E.Either[error, int]) string {
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
},
)
// Iso 2: []byte to string
bytesStringIso := I.MakeIso(
func(b []byte) string { return string(b) },
func(s string) []byte { return []byte(s) },
)
// First composition: Prism[Either, int] with Iso[string, Either] -> Prism[string, int]
step1 := Compose[string](rightPrism)(stringEitherIso)
// Second composition: Prism[string, int] with Iso[[]byte, string] -> Prism[[]byte, int]
step2 := Compose[[]byte](step1)(bytesStringIso)
t.Run("Chained composition extracts correctly", func(t *testing.T) {
bytes := []byte("42")
result := step2.GetOption(bytes)
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 42, num)
})
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
num := 100
result := step2.ReverseGet(num)
assert.Equal(t, []byte("100"), result)
})
}
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
func TestComposePrismLaws(t *testing.T) {
// Create a prism
prism := P.FromEither[error, int]()
// Create an isomorphism from string to Either[error, int]
iso := I.MakeIso(
func(s string) E.Either[error, int] {
i, err := strconv.Atoi(s)
if err != nil {
return E.Left[int](err)
}
return E.Right[error](i)
},
func(e E.Either[error, int]) string {
return strconv.Itoa(E.GetOrElse(func(error) int { return 0 })(e))
},
)
// Compose them
composed := Compose[string](prism)(iso)
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
value := 42
// ReverseGet then GetOption should return Some(value)
source := composed.ReverseGet(value)
result := composed.GetOption(source)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, value, extracted)
})
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
source := "100"
// First GetOption
firstResult := composed.GetOption(source)
assert.True(t, O.IsSome(firstResult))
// Extract the value
value := O.GetOrElse(F.Constant(0))(firstResult)
// ReverseGet then GetOption again
reconstructed := composed.ReverseGet(value)
secondResult := composed.GetOption(reconstructed)
assert.True(t, O.IsSome(secondResult))
finalValue := O.GetOrElse(F.Constant(0))(secondResult)
assert.Equal(t, value, finalValue)
})
}
// TestComposeWithJSON tests a practical example with JSON parsing
func TestComposeWithJSON(t *testing.T) {
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
// Prism that extracts Config from []byte (via JSON parsing)
configPrism := P.MakePrism(
func(b []byte) O.Option[Config] {
var cfg Config
if err := json.Unmarshal(b, &cfg); err != nil {
return O.None[Config]()
}
return O.Some(cfg)
},
func(cfg Config) []byte {
b, _ := json.Marshal(cfg)
return b
},
)
// Isomorphism between string and []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Compose: Prism[[]byte, Config] with Iso[string, []byte] -> Prism[string, Config]
stringConfigPrism := Compose[string](configPrism)(stringBytesIso)
t.Run("GetOption parses valid JSON string", func(t *testing.T) {
jsonStr := `{"host":"localhost","port":8080}`
result := stringConfigPrism.GetOption(jsonStr)
assert.True(t, O.IsSome(result))
cfg := O.GetOrElse(F.Constant(Config{}))(result)
assert.Equal(t, "localhost", cfg.Host)
assert.Equal(t, 8080, cfg.Port)
})
t.Run("GetOption returns None for invalid JSON", func(t *testing.T) {
invalidJSON := `{invalid json}`
result := stringConfigPrism.GetOption(invalidJSON)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet creates JSON string from Config", func(t *testing.T) {
cfg := Config{Host: "example.com", Port: 443}
result := stringConfigPrism.ReverseGet(cfg)
// Parse it back to verify
var parsed Config
err := json.Unmarshal([]byte(result), &parsed)
assert.NoError(t, err)
assert.Equal(t, cfg, parsed)
})
}
// TestComposeWithEmptyValues tests edge cases with empty/zero values
func TestComposeWithEmptyValues(t *testing.T) {
// Prism that extracts Right values
prism := P.FromEither[error, []byte]()
// Isomorphism between string and Either[error, []byte]
iso := I.MakeIso(
func(s string) E.Either[error, []byte] {
return E.Right[error]([]byte(s))
},
func(e E.Either[error, []byte]) string {
return string(E.GetOrElse(func(error) []byte { return []byte{} })(e))
},
)
composed := Compose[string](prism)(iso)
t.Run("Empty string is handled correctly", func(t *testing.T) {
result := composed.GetOption("")
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
assert.Equal(t, []byte{}, bytes)
})
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
bytes := []byte{}
result := composed.ReverseGet(bytes)
assert.Equal(t, "", result)
})
}

View File

@@ -0,0 +1,99 @@
// 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 prism provides utilities for composing prisms with isomorphisms.
//
// This package enables the composition of prisms (optics for sum types) with
// isomorphisms (bidirectional transformations), allowing you to transform the
// source type of a prism using an isomorphism. This is the inverse operation
// of optics/prism/iso, where we transform the focus type instead of the source type.
//
// # Key Concepts
//
// A Prism[S, A] is an optic that focuses on a specific variant within a sum type S,
// extracting values of type A. An Iso[S, A] represents a bidirectional transformation
// between types S and A without loss of information.
//
// When you compose a Prism[A, B] with an Iso[S, A], you get a Prism[S, B] that:
// - Transforms S to A using the isomorphism's Get
// - Extracts values of type B from A (using the prism)
// - Can construct S from B by first using the prism's ReverseGet to get A, then the iso's ReverseGet
//
// # Example Usage
//
// import (
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// IP "github.com/IBM/fp-go/v2/optics/iso/prism"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between []byte and string
// bytesStringIso := iso.MakeIso(
// func(b []byte) string { return string(b) },
// func(s string) []byte { return []byte(s) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that works with []byte as source
// bytesPrism := IP.Compose(rightPrism)(bytesStringIso)
//
// // Use the composed prism
// bytes := []byte(`{"status":"ok"}`)
// // First converts bytes to string via iso, then extracts Right value
// result := bytesPrism.GetOption(either.Right[error](string(bytes)))
//
// # Comparison with optics/prism/iso
//
// This package (optics/iso/prism) is the dual of optics/prism/iso:
// - optics/prism/iso: Composes Iso[A, B] with Prism[S, A] → Prism[S, B] (transforms focus type)
// - optics/iso/prism: Composes Prism[A, B] with Iso[S, A] → Prism[S, B] (transforms source type)
//
// # Type Aliases
//
// This package re-exports key types from the iso and prism packages for convenience:
// - Iso[S, A]: An isomorphism between types S and A
// - Prism[S, A]: A prism focusing on type A within sum type S
package prism
import (
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
)
type (
// Iso represents an isomorphism between types S and A.
// It is a bidirectional transformation that converts between two types
// without any loss of information.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
Iso[S, A any] = I.Iso[S, A]
// Prism is an optic used to select part of a sum type (tagged union).
// It provides operations to extract and construct values within sum types.
//
// Type Parameters:
// - S: The source type (sum type)
// - A: The focus type (variant within the sum type)
//
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
Prism[S, A any] = P.Prism[S, A]
)

View File

@@ -0,0 +1,156 @@
// 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 iso
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
)
// Compose creates an operator that composes an isomorphism with a prism.
//
// This function takes an isomorphism Iso[A, B] and returns an operator that can
// transform any Prism[S, A] into a Prism[S, B]. The resulting prism maintains
// the same source type S but changes the focus type from A to B using the
// bidirectional transformation provided by the isomorphism.
//
// The composition works as follows:
// - GetOption: First extracts A from S using the prism, then transforms A to B using the iso's Get
// - ReverseGet: First transforms B to A using the iso's ReverseGet, then constructs S using the prism's ReverseGet
//
// This is particularly useful when you have a prism that focuses on one type but
// you need to work with a different type that has a lossless bidirectional
// transformation to the original type.
//
// Haskell Equivalent:
// This corresponds to the (.) operator for composing optics in Haskell's lens library,
// specifically when composing a Prism with an Iso:
//
// prism . iso :: Prism s a -> Iso a b -> Prism s b
//
// In Haskell's lens library, this is part of the general optic composition mechanism.
// See: https://hackage.haskell.org/package/lens/docs/Control-Lens-Prism.html
//
// Type Parameters:
// - S: The source type (sum type) that the prism operates on
// - A: The original focus type of the prism
// - B: The new focus type after applying the isomorphism
//
// Parameters:
// - ab: An isomorphism between types A and B that defines the bidirectional transformation
//
// Returns:
// - An Operator[S, A, B] that transforms Prism[S, A] into Prism[S, B]
//
// Laws:
// The composed prism must satisfy the prism laws:
// 1. GetOption(ReverseGet(b)) == Some(b) for all b: B
// 2. If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)
//
// These laws are preserved because:
// - The isomorphism satisfies: ab.ReverseGet(ab.Get(a)) == a and ab.Get(ab.ReverseGet(b)) == b
// - The original prism satisfies the prism laws
//
// Example - Composing string/bytes isomorphism with Either prism:
//
// import (
// "github.com/IBM/fp-go/v2/either"
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between string and []byte
// stringBytesIso := iso.MakeIso(
// func(s string) []byte { return []byte(s) },
// func(b []byte) string { return string(b) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that works with []byte instead of string
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
//
// // Extract bytes from a Right value
// success := either.Right[error]("hello")
// result := bytesPrism.GetOption(success)
// // result is Some([]byte("hello"))
//
// // Extract from a Left value returns None
// failure := either.Left[string](errors.New("error"))
// result = bytesPrism.GetOption(failure)
// // result is None
//
// // Construct an Either from bytes
// constructed := bytesPrism.ReverseGet([]byte("world"))
// // constructed is Right("world")
//
// Example - Composing with custom types:
//
// type Celsius float64
// type Fahrenheit float64
//
// // Isomorphism between Celsius and Fahrenheit
// tempIso := iso.MakeIso(
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
// )
//
// // Prism that extracts temperature from a weather report
// type WeatherReport struct {
// Temperature Celsius
// Condition string
// }
// tempPrism := prism.MakePrism(
// func(w WeatherReport) option.Option[Celsius] {
// return option.Some(w.Temperature)
// },
// func(c Celsius) WeatherReport {
// return WeatherReport{Temperature: c}
// },
// )
//
// // Compose to work with Fahrenheit instead
// fahrenheitPrism := PI.Compose(tempIso)(tempPrism)
//
// report := WeatherReport{Temperature: 20, Condition: "sunny"}
// temp := fahrenheitPrism.GetOption(report)
// // temp is Some(68.0) in Fahrenheit
//
// See also:
// - github.com/IBM/fp-go/v2/optics/iso for isomorphism operations
// - github.com/IBM/fp-go/v2/optics/prism for prism operations
// - Operator for the type signature of the returned function
func Compose[S, A, B any](ab Iso[A, B]) Operator[S, A, B] {
return func(pa Prism[S, A]) Prism[S, B] {
return P.MakePrismWithName(
F.Flow2(
pa.GetOption,
O.Map(ab.Get),
),
F.Flow2(
ab.ReverseGet,
pa.ReverseGet,
),
fmt.Sprintf("PrismCompose[%s -> %s]", pa, ab),
)
}
}

View File

@@ -0,0 +1,369 @@
// 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 iso
import (
"errors"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestComposeWithEitherPrism tests composing an isomorphism with an Either prism
func TestComposeWithEitherPrism(t *testing.T) {
// Create an isomorphism between string and []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Create a prism that extracts Right values from Either[error, string]
rightPrism := P.FromEither[error, string]()
// Compose them
bytesPrism := Compose[E.Either[error, string]](stringBytesIso)(rightPrism)
t.Run("GetOption extracts and transforms Right value", func(t *testing.T) {
success := E.Right[error]("hello")
result := bytesPrism.GetOption(success)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, []byte("hello"), bytes)
})
t.Run("GetOption returns None for Left value", func(t *testing.T) {
failure := E.Left[string](errors.New("error"))
result := bytesPrism.GetOption(failure)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Either from transformed value", func(t *testing.T) {
bytes := []byte("world")
result := bytesPrism.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
str := E.GetOrElse(func(error) string { return "" })(result)
assert.Equal(t, "world", str)
})
t.Run("Round-trip through GetOption and ReverseGet", func(t *testing.T) {
// Start with bytes
original := []byte("test")
// ReverseGet to create Either
either := bytesPrism.ReverseGet(original)
// GetOption to extract bytes back
result := bytesPrism.GetOption(either)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, original, extracted)
})
}
// TestComposeWithOptionPrism tests composing an isomorphism with an Option prism
func TestComposeWithOptionPrism(t *testing.T) {
// Create an isomorphism between int and string
intStringIso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Create a prism that extracts Some values from Option[int]
somePrism := P.FromOption[int]()
// Compose them
stringPrism := Compose[O.Option[int]](intStringIso)(somePrism)
t.Run("GetOption extracts and transforms Some value", func(t *testing.T) {
some := O.Some(42)
result := stringPrism.GetOption(some)
assert.True(t, O.IsSome(result))
str := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, "42", str)
})
t.Run("GetOption returns None for None value", func(t *testing.T) {
none := O.None[int]()
result := stringPrism.GetOption(none)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Option from transformed value", func(t *testing.T) {
str := "100"
result := stringPrism.ReverseGet(str)
assert.True(t, O.IsSome(result))
num := O.GetOrElse(F.Constant(0))(result)
assert.Equal(t, 100, num)
})
}
// TestComposeWithCustomPrism tests composing with a custom prism
// Custom types for TestComposeWithCustomPrism
type Celsius float64
type Fahrenheit float64
type Temperature interface {
isTemperature()
}
type CelsiusTemp struct {
Value Celsius
}
func (c CelsiusTemp) isTemperature() {}
type KelvinTemp struct {
Value float64
}
func (k KelvinTemp) isTemperature() {}
func TestComposeWithCustomPrism(t *testing.T) {
// Isomorphism between Celsius and Fahrenheit
tempIso := I.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
// Prism that extracts Celsius from Temperature
celsiusPrism := P.MakePrism(
func(t Temperature) O.Option[Celsius] {
if ct, ok := t.(CelsiusTemp); ok {
return O.Some(ct.Value)
}
return O.None[Celsius]()
},
func(c Celsius) Temperature {
return CelsiusTemp{Value: c}
},
)
// Compose to work with Fahrenheit
fahrenheitPrism := Compose[Temperature](tempIso)(celsiusPrism)
t.Run("GetOption extracts and converts Celsius to Fahrenheit", func(t *testing.T) {
temp := CelsiusTemp{Value: 0}
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsSome(result))
fahrenheit := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
assert.InDelta(t, 32.0, float64(fahrenheit), 0.01)
})
t.Run("GetOption returns None for non-Celsius temperature", func(t *testing.T) {
temp := KelvinTemp{Value: 273.15}
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsNone(result))
})
t.Run("ReverseGet constructs Temperature from Fahrenheit", func(t *testing.T) {
fahrenheit := Fahrenheit(68)
result := fahrenheitPrism.ReverseGet(fahrenheit)
celsiusTemp, ok := result.(CelsiusTemp)
assert.True(t, ok)
assert.InDelta(t, 20.0, float64(celsiusTemp.Value), 0.01)
})
t.Run("Round-trip preserves value", func(t *testing.T) {
original := Fahrenheit(100)
// ReverseGet to create Temperature
temp := fahrenheitPrism.ReverseGet(original)
// GetOption to extract Fahrenheit back
result := fahrenheitPrism.GetOption(temp)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(Fahrenheit(0)))(result)
assert.InDelta(t, float64(original), float64(extracted), 0.01)
})
}
// TestComposeIdentityIso tests composing with an identity isomorphism
func TestComposeIdentityIso(t *testing.T) {
// Identity isomorphism (no transformation)
idIso := I.Id[string]()
// Prism that extracts Right values
rightPrism := P.FromEither[error, string]()
// Compose with identity should not change behavior
composedPrism := Compose[E.Either[error, string]](idIso)(rightPrism)
t.Run("Composed prism behaves like original prism", func(t *testing.T) {
success := E.Right[error]("test")
// Original prism
originalResult := rightPrism.GetOption(success)
// Composed prism
composedResult := composedPrism.GetOption(success)
assert.Equal(t, originalResult, composedResult)
})
t.Run("ReverseGet produces same result", func(t *testing.T) {
value := "test"
// Original prism
originalEither := rightPrism.ReverseGet(value)
// Composed prism
composedEither := composedPrism.ReverseGet(value)
assert.Equal(t, originalEither, composedEither)
})
}
// TestComposeChaining tests chaining multiple compositions
func TestComposeChaining(t *testing.T) {
// First isomorphism: int to string
intStringIso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Second isomorphism: string to []byte
stringBytesIso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
// Prism that extracts Right values
rightPrism := P.FromEither[error, int]()
// Chain compositions: Either[error, int] -> int -> string -> []byte
step1 := Compose[E.Either[error, int]](intStringIso)(rightPrism) // Prism[Either[error, int], string]
step2 := Compose[E.Either[error, int]](stringBytesIso)(step1) // Prism[Either[error, int], []byte]
t.Run("Chained composition extracts and transforms correctly", func(t *testing.T) {
either := E.Right[error](42)
result := step2.GetOption(either)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte{}))(result)
assert.Equal(t, []byte("42"), bytes)
})
t.Run("Chained composition ReverseGet works correctly", func(t *testing.T) {
bytes := []byte("100")
result := step2.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
num := E.GetOrElse(func(error) int { return 0 })(result)
assert.Equal(t, 100, num)
})
}
// TestComposePrismLaws verifies that the composed prism satisfies prism laws
func TestComposePrismLaws(t *testing.T) {
// Create an isomorphism
iso := I.MakeIso(
func(i int) string { return strconv.Itoa(i) },
func(s string) int {
i, _ := strconv.Atoi(s)
return i
},
)
// Create a prism
prism := P.FromEither[error, int]()
// Compose them
composed := Compose[E.Either[error, int]](iso)(prism)
t.Run("Law 1: GetOption(ReverseGet(b)) == Some(b)", func(t *testing.T) {
value := "42"
// ReverseGet then GetOption should return Some(value)
either := composed.ReverseGet(value)
result := composed.GetOption(either)
assert.True(t, O.IsSome(result))
extracted := O.GetOrElse(F.Constant(""))(result)
assert.Equal(t, value, extracted)
})
t.Run("Law 2: If GetOption(s) == Some(a), then GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
either := E.Right[error](100)
// First GetOption
firstResult := composed.GetOption(either)
assert.True(t, O.IsSome(firstResult))
// Extract the value
value := O.GetOrElse(F.Constant(""))(firstResult)
// ReverseGet then GetOption again
reconstructed := composed.ReverseGet(value)
secondResult := composed.GetOption(reconstructed)
assert.True(t, O.IsSome(secondResult))
finalValue := O.GetOrElse(F.Constant(""))(secondResult)
assert.Equal(t, value, finalValue)
})
}
// TestComposeWithEmptyValues tests edge cases with empty/zero values
func TestComposeWithEmptyValues(t *testing.T) {
// Isomorphism that handles empty strings
iso := I.MakeIso(
func(s string) []byte { return []byte(s) },
func(b []byte) string { return string(b) },
)
prism := P.FromEither[error, string]()
composed := Compose[E.Either[error, string]](iso)(prism)
t.Run("Empty string is handled correctly", func(t *testing.T) {
either := E.Right[error]("")
result := composed.GetOption(either)
assert.True(t, O.IsSome(result))
bytes := O.GetOrElse(F.Constant([]byte("default")))(result)
assert.Equal(t, []byte{}, bytes)
})
t.Run("ReverseGet with empty bytes", func(t *testing.T) {
bytes := []byte{}
result := composed.ReverseGet(bytes)
assert.True(t, E.IsRight(result))
str := E.GetOrElse(func(error) string { return "default" })(result)
assert.Equal(t, "", str)
})
}

View File

@@ -0,0 +1,112 @@
// 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 iso provides utilities for composing isomorphisms with prisms.
//
// This package enables the composition of isomorphisms (bidirectional transformations)
// with prisms (optics for sum types), allowing you to transform the focus type of a prism
// using an isomorphism. This is particularly useful when you need to work with prisms
// that focus on a type that can be bidirectionally converted to another type.
//
// # Key Concepts
//
// An Iso[S, A] represents a bidirectional transformation between types S and A without
// loss of information. A Prism[S, A] is an optic that focuses on a specific variant
// within a sum type S, extracting values of type A.
//
// When you compose an Iso[A, B] with a Prism[S, A], you get a Prism[S, B] that:
// - Extracts values of type A from S (using the prism)
// - Transforms them to type B (using the isomorphism's Get)
// - Can construct S from B by reversing the transformation (using the isomorphism's ReverseGet)
//
// # Example Usage
//
// import (
// "github.com/IBM/fp-go/v2/optics/iso"
// "github.com/IBM/fp-go/v2/optics/prism"
// PI "github.com/IBM/fp-go/v2/optics/prism/iso"
// O "github.com/IBM/fp-go/v2/option"
// )
//
// // Create an isomorphism between string and []byte
// stringBytesIso := iso.MakeIso(
// func(s string) []byte { return []byte(s) },
// func(b []byte) string { return string(b) },
// )
//
// // Create a prism that extracts Right values from Either[error, string]
// rightPrism := prism.FromEither[error, string]()
//
// // Compose them to get a prism that extracts Right values as []byte
// bytesPrism := PI.Compose(stringBytesIso)(rightPrism)
//
// // Use the composed prism
// either := either.Right[error]("hello")
// result := bytesPrism.GetOption(either) // Some([]byte("hello"))
//
// # Type Aliases
//
// This package re-exports key types from the iso and prism packages for convenience:
// - Iso[S, A]: An isomorphism between types S and A
// - Prism[S, A]: A prism focusing on type A within sum type S
// - Operator[S, A, B]: A function that transforms Prism[S, A] to Prism[S, B]
package iso
import (
I "github.com/IBM/fp-go/v2/optics/iso"
P "github.com/IBM/fp-go/v2/optics/prism"
)
type (
// Iso represents an isomorphism between types S and A.
// It is a bidirectional transformation that converts between two types
// without any loss of information.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// See github.com/IBM/fp-go/v2/optics/iso for the full Iso API.
Iso[S, A any] = I.Iso[S, A]
// Prism is an optic used to select part of a sum type (tagged union).
// It provides operations to extract and construct values within sum types.
//
// Type Parameters:
// - S: The source type (sum type)
// - A: The focus type (variant within the sum type)
//
// See github.com/IBM/fp-go/v2/optics/prism for the full Prism API.
Prism[S, A any] = P.Prism[S, A]
// Operator represents a function that transforms one prism into another.
// It takes a Prism[S, A] and returns a Prism[S, B], allowing for prism transformations.
//
// This is commonly used with the Compose function to create operators that
// transform the focus type of a prism using an isomorphism.
//
// Type Parameters:
// - S: The source type (remains constant)
// - A: The original focus type
// - B: The new focus type
//
// Example:
//
// // Create an operator that transforms string prisms to []byte prisms
// stringToBytesOp := Compose(stringBytesIso)
// // Apply it to a prism
// bytesPrism := stringToBytesOp(stringPrism)
Operator[S, A, B any] = P.Operator[S, A, B]
)

View File

@@ -23,8 +23,10 @@ import (
"strconv"
"time"
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
J "github.com/IBM/fp-go/v2/json"
"github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
)
@@ -322,6 +324,50 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrismWithName(either.ToOption[E, T], either.Of[E, T], "PrismFromEither")
}
// FromResult creates a prism for extracting values from Result types.
// It provides a safe way to work with Result values (which are Either[error, T]),
// focusing on the success case and handling errors gracefully through the Option type.
//
// This is a convenience function that is equivalent to FromEither[error, T]().
//
// The prism's GetOption attempts to extract the success value from a Result.
// If the Result is successful, it returns Some(value); if it's an error, it returns None.
//
// The prism's ReverseGet always succeeds, wrapping a value into a successful Result.
//
// Type Parameters:
// - T: The value type contained in the Result
//
// Returns:
// - A Prism[Result[T], T] that safely extracts success values
//
// Example:
//
// // Create a prism for extracting successful results
// resultPrism := FromResult[int]()
//
// // Extract from successful result
// success := result.Of[int](42)
// value := resultPrism.GetOption(success) // Some(42)
//
// // Extract from error result
// failure := result.Error[int](errors.New("failed"))
// value = resultPrism.GetOption(failure) // None[int]()
//
// // Wrap value into successful Result
// wrapped := resultPrism.ReverseGet(100) // Result containing 100
//
// // Use with Set to update successful results
// setter := Set[Result[int], int](200)
// result := setter(resultPrism)(success) // Result containing 200
// result = setter(resultPrism)(failure) // Error result (unchanged)
//
// Common use cases:
// - Extracting successful values from Result types
// - Filtering out errors in data pipelines
// - Working with fallible operations that return Result
// - Composing with other prisms for complex error handling
//
//go:inline
func FromResult[T any]() Prism[Result[T], T] {
return FromEither[error, T]()
@@ -1261,3 +1307,71 @@ func MakeURLPrisms() URLPrisms {
RawFragment: _prismRawFragment,
}
}
// ParseJSON creates a prism for parsing and marshaling JSON data.
// It provides a safe way to convert between JSON bytes and Go types,
// handling parsing and marshaling errors gracefully through the Option type.
//
// The prism's GetOption attempts to unmarshal JSON bytes into type A.
// If unmarshaling succeeds, it returns Some(A); if it fails (e.g., invalid JSON
// or type mismatch), it returns None.
//
// The prism's ReverseGet marshals a value of type A into JSON bytes.
// If marshaling fails (which is rare), it returns an empty byte slice.
//
// Type Parameters:
// - A: The Go type to unmarshal JSON into
//
// Returns:
// - A Prism[[]byte, A] that safely handles JSON parsing/marshaling
//
// Example:
//
// // Define a struct type
// type Person struct {
// Name string `json:"name"`
// Age int `json:"age"`
// }
//
// // Create a JSON parsing prism
// jsonPrism := ParseJSON[Person]()
//
// // Parse valid JSON
// jsonData := []byte(`{"name":"Alice","age":30}`)
// person := jsonPrism.GetOption(jsonData)
// // Some(Person{Name: "Alice", Age: 30})
//
// // Parse invalid JSON
// invalidJSON := []byte(`{invalid json}`)
// result := jsonPrism.GetOption(invalidJSON) // None[Person]()
//
// // Marshal to JSON
// p := Person{Name: "Bob", Age: 25}
// jsonBytes := jsonPrism.ReverseGet(p)
// // []byte(`{"name":"Bob","age":25}`)
//
// // Use with Set to update JSON data
// newPerson := Person{Name: "Charlie", Age: 35}
// setter := Set[[]byte, Person](newPerson)
// updated := setter(jsonPrism)(jsonData)
// // []byte(`{"name":"Charlie","age":35}`)
//
// Common use cases:
// - Parsing JSON configuration files
// - Working with JSON API responses
// - Validating and transforming JSON data in pipelines
// - Type-safe JSON deserialization
// - Converting between JSON and Go structs
func ParseJSON[A any]() Prism[[]byte, A] {
return MakePrismWithName(
F.Flow2(
J.Unmarshal[A],
either.ToOption[error, A],
),
F.Flow2(
J.Marshal[A],
either.GetOrElse(F.Constant1[error](array.Empty[byte]())),
),
"JSON",
)
}

View File

@@ -16,11 +16,14 @@
package prism
import (
"errors"
"regexp"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -1396,3 +1399,403 @@ func TestNonEmptyStringValidation(t *testing.T) {
assert.Equal(t, []string{"hello", "world", "test"}, nonEmpty)
})
}
// TestFromResult tests the FromResult prism with Result types
func TestFromResult(t *testing.T) {
t.Run("extract from successful result", func(t *testing.T) {
prism := FromResult[int]()
success := result.Of[int](42)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("extract from error result", func(t *testing.T) {
prism := FromResult[int]()
failure := E.Left[int](errors.New("test error"))
extracted := prism.GetOption(failure)
assert.True(t, O.IsNone(extracted))
})
t.Run("ReverseGet wraps value in successful result", func(t *testing.T) {
prism := FromResult[int]()
wrapped := prism.ReverseGet(100)
// Verify it's a successful result
extracted := prism.GetOption(wrapped)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 100, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("works with string type", func(t *testing.T) {
prism := FromResult[string]()
success := result.Of[string]("hello")
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, "hello", O.GetOrElse(F.Constant(""))(extracted))
})
t.Run("works with struct type", func(t *testing.T) {
type Person struct {
Name string
Age int
}
prism := FromResult[Person]()
person := Person{Name: "Alice", Age: 30}
success := result.Of[Person](person)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
result := O.GetOrElse(F.Constant(Person{}))(extracted)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
// TestFromResultWithSet tests using Set with FromResult prism
func TestFromResultWithSet(t *testing.T) {
t.Run("set on successful result", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
success := result.Of[int](42)
updated := setter(prism)(success)
// Verify the value was updated
extracted := prism.GetOption(updated)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 200, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("set on error result leaves it unchanged", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
failure := E.Left[int](errors.New("test error"))
updated := setter(prism)(failure)
// Verify it's still an error
extracted := prism.GetOption(updated)
assert.True(t, O.IsNone(extracted))
})
}
// TestFromResultPrismLaws tests that FromResult satisfies prism laws
func TestFromResultPrismLaws(t *testing.T) {
prism := FromResult[int]()
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
value := 42
wrapped := prism.ReverseGet(value)
extracted := prism.GetOption(wrapped)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, value, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
value := 42
result1 := prism.ReverseGet(value)
result2 := prism.ReverseGet(value)
// Both should extract the same value
extracted1 := prism.GetOption(result1)
extracted2 := prism.GetOption(result2)
val1 := O.GetOrElse(F.Constant(-1))(extracted1)
val2 := O.GetOrElse(F.Constant(-1))(extracted2)
assert.Equal(t, val1, val2)
})
}
// TestFromResultComposition tests composing FromResult with other prisms
func TestFromResultComposition(t *testing.T) {
t.Run("compose with predicate prism", func(t *testing.T) {
// Create a prism that only matches positive numbers
positivePrism := FromPredicate(func(n int) bool { return n > 0 })
// Compose: Result[int] -> int -> positive int
composed := Compose[result.Result[int]](positivePrism)(FromResult[int]())
// Test with positive number
success := result.Of[int](42)
extracted := composed.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
// Test with negative number
negativeSuccess := result.Of[int](-5)
extracted = composed.GetOption(negativeSuccess)
assert.True(t, O.IsNone(extracted))
// Test with error
failure := E.Left[int](errors.New("test error"))
extracted = composed.GetOption(failure)
assert.True(t, O.IsNone(extracted))
})
}
// TestParseJSON tests the ParseJSON prism with various JSON data
func TestParseJSON(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("parse valid JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
jsonData := []byte(`{"name":"Alice","age":30}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
})
t.Run("parse invalid JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
invalidJSON := []byte(`{invalid json}`)
parsed := prism.GetOption(invalidJSON)
assert.True(t, O.IsNone(parsed))
})
t.Run("parse JSON with missing fields", func(t *testing.T) {
prism := ParseJSON[Person]()
// Missing age field - should use zero value
jsonData := []byte(`{"name":"Bob"}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Bob", person.Name)
assert.Equal(t, 0, person.Age)
})
t.Run("parse JSON with extra fields", func(t *testing.T) {
prism := ParseJSON[Person]()
// Extra field should be ignored
jsonData := []byte(`{"name":"Charlie","age":25,"extra":"ignored"}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Charlie", person.Name)
assert.Equal(t, 25, person.Age)
})
t.Run("ReverseGet marshals to JSON", func(t *testing.T) {
prism := ParseJSON[Person]()
person := Person{Name: "David", Age: 35}
jsonBytes := prism.ReverseGet(person)
// Parse it back to verify
parsed := prism.GetOption(jsonBytes)
assert.True(t, O.IsSome(parsed))
result := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "David", result.Name)
assert.Equal(t, 35, result.Age)
})
t.Run("works with primitive types", func(t *testing.T) {
prism := ParseJSON[int]()
jsonData := []byte(`42`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(parsed))
})
t.Run("works with arrays", func(t *testing.T) {
prism := ParseJSON[[]string]()
jsonData := []byte(`["hello","world","test"]`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
arr := O.GetOrElse(F.Constant([]string{}))(parsed)
assert.Equal(t, []string{"hello", "world", "test"}, arr)
})
t.Run("works with maps", func(t *testing.T) {
prism := ParseJSON[map[string]int]()
jsonData := []byte(`{"a":1,"b":2,"c":3}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
m := O.GetOrElse(F.Constant(map[string]int{}))(parsed)
assert.Equal(t, 1, m["a"])
assert.Equal(t, 2, m["b"])
assert.Equal(t, 3, m["c"])
})
t.Run("works with nested structures", func(t *testing.T) {
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
type PersonWithAddress struct {
Name string `json:"name"`
Address Address `json:"address"`
}
prism := ParseJSON[PersonWithAddress]()
jsonData := []byte(`{"name":"Eve","address":{"street":"123 Main St","city":"NYC"}}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(PersonWithAddress{}))(parsed)
assert.Equal(t, "Eve", person.Name)
assert.Equal(t, "123 Main St", person.Address.Street)
assert.Equal(t, "NYC", person.Address.City)
})
t.Run("parse empty JSON object", func(t *testing.T) {
prism := ParseJSON[Person]()
jsonData := []byte(`{}`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "", person.Name)
assert.Equal(t, 0, person.Age)
})
t.Run("parse null JSON", func(t *testing.T) {
prism := ParseJSON[*Person]()
jsonData := []byte(`null`)
parsed := prism.GetOption(jsonData)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(&Person{}))(parsed)
assert.Nil(t, person)
})
}
// TestParseJSONWithSet tests using Set with ParseJSON prism
func TestParseJSONWithSet(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("set updates JSON data", func(t *testing.T) {
prism := ParseJSON[Person]()
originalJSON := []byte(`{"name":"Alice","age":30}`)
newPerson := Person{Name: "Bob", Age: 25}
setter := Set[[]byte, Person](newPerson)
updatedJSON := setter(prism)(originalJSON)
// Parse the updated JSON
parsed := prism.GetOption(updatedJSON)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Bob", person.Name)
assert.Equal(t, 25, person.Age)
})
t.Run("set on invalid JSON returns original unchanged", func(t *testing.T) {
prism := ParseJSON[Person]()
invalidJSON := []byte(`{invalid}`)
newPerson := Person{Name: "Charlie", Age: 35}
setter := Set[[]byte, Person](newPerson)
result := setter(prism)(invalidJSON)
// Should return original unchanged since it couldn't be parsed
assert.Equal(t, invalidJSON, result)
})
}
// TestParseJSONPrismLaws tests that ParseJSON satisfies prism laws
func TestParseJSONPrismLaws(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
prism := ParseJSON[Person]()
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a)", func(t *testing.T) {
person := Person{Name: "Alice", Age: 30}
jsonBytes := prism.ReverseGet(person)
parsed := prism.GetOption(jsonBytes)
assert.True(t, O.IsSome(parsed))
result := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, person.Name, result.Name)
assert.Equal(t, person.Age, result.Age)
})
t.Run("law 2: ReverseGet is consistent", func(t *testing.T) {
person := Person{Name: "Bob", Age: 25}
json1 := prism.ReverseGet(person)
json2 := prism.ReverseGet(person)
// Both should parse to the same value
parsed1 := prism.GetOption(json1)
parsed2 := prism.GetOption(json2)
result1 := O.GetOrElse(F.Constant(Person{}))(parsed1)
result2 := O.GetOrElse(F.Constant(Person{}))(parsed2)
assert.Equal(t, result1.Name, result2.Name)
assert.Equal(t, result1.Age, result2.Age)
})
}
// TestParseJSONComposition tests composing ParseJSON with other prisms
func TestParseJSONComposition(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
t.Run("compose with predicate prism", func(t *testing.T) {
// Create a prism that only matches adults (age >= 18)
adultPrism := FromPredicate(func(p Person) bool { return p.Age >= 18 })
// Compose: []byte -> Person -> Adult
composed := Compose[[]byte](adultPrism)(ParseJSON[Person]())
// Test with adult
adultJSON := []byte(`{"name":"Alice","age":30}`)
parsed := composed.GetOption(adultJSON)
assert.True(t, O.IsSome(parsed))
person := O.GetOrElse(F.Constant(Person{}))(parsed)
assert.Equal(t, "Alice", person.Name)
// Test with minor
minorJSON := []byte(`{"name":"Bob","age":15}`)
parsed = composed.GetOption(minorJSON)
assert.True(t, O.IsNone(parsed))
// Test with invalid JSON
invalidJSON := []byte(`{invalid}`)
parsed = composed.GetOption(invalidJSON)
assert.True(t, O.IsNone(parsed))
})
}

View File

@@ -3,7 +3,7 @@ package readerio
import "github.com/IBM/fp-go/v2/io"
//go:inline
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, Void] {
return ChainIOK[R](io.FromConsumer(c))
}

View File

@@ -18,6 +18,7 @@ package readerio
import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
@@ -66,4 +67,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

@@ -1104,6 +1104,8 @@ func After[R, E, A any](timestamp time.Time) Operator[R, E, A, A] {
// If the ReaderIOEither is Left, it applies the provided function to the error value,
// which returns a new ReaderIOEither that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative IO computations
// that need access to configuration or dependencies. The error type can be widened from E1 to E2.
//

View File

@@ -531,3 +531,205 @@ func TestReadIO(t *testing.T) {
assert.Equal(t, E.Right[error](25), result)
})
}
// TestChainLeftIdenticalToOrElse proves that ChainLeft and OrElse are identical functions.
// This test verifies that both functions produce the same results for all scenarios with reader context:
// - Left values with error recovery using reader context
// - Left values with error transformation
// - Right values passing through unchanged
// - Error type widening
func TestChainLeftIdenticalToOrElse(t *testing.T) {
type Config struct {
fallbackValue int
retryEnabled bool
}
// Test 1: Left value with error recovery using reader context
t.Run("Left value recovery with reader - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
if e == "recoverable" {
return func(cfg Config) IOE.IOEither[string, int] {
return IOE.Right[string](cfg.fallbackValue)
}
}
return Left[Config, int](e)
}
input := Left[Config, int]("recoverable")
cfg := Config{fallbackValue: 42, retryEnabled: true}
// Using ChainLeft
resultChainLeft := ChainLeft(recoveryFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(recoveryFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](42), resultChainLeft)
})
// Test 2: Left value with error transformation
t.Run("Left value transformation - ChainLeft equals OrElse", func(t *testing.T) {
transformFn := func(e string) ReaderIOEither[Config, string, int] {
return Left[Config, int]("transformed: " + e)
}
input := Left[Config, int]("original error")
cfg := Config{fallbackValue: 0, retryEnabled: false}
// Using ChainLeft
resultChainLeft := ChainLeft(transformFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(transformFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int]("transformed: original error"), resultChainLeft)
})
// Test 3: Right value - both should pass through unchanged
t.Run("Right value passthrough - ChainLeft equals OrElse", func(t *testing.T) {
handlerFn := func(e string) ReaderIOEither[Config, string, int] {
return Left[Config, int]("should not be called")
}
input := Right[Config, string](100)
cfg := Config{fallbackValue: 0, retryEnabled: false}
// Using ChainLeft
resultChainLeft := ChainLeft(handlerFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(handlerFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](100), resultChainLeft)
})
// Test 4: Error type widening
t.Run("Error type widening - ChainLeft equals OrElse", func(t *testing.T) {
widenFn := func(e string) ReaderIOEither[Config, int, int] {
return Left[Config, int](404)
}
input := Left[Config, int]("not found")
cfg := Config{fallbackValue: 0, retryEnabled: false}
// Using ChainLeft
resultChainLeft := ChainLeft(widenFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(widenFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Left[int](404), resultChainLeft)
})
// Test 5: Composition in pipeline with reader context
t.Run("Pipeline composition with reader - ChainLeft equals OrElse", func(t *testing.T) {
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
if e == "network error" {
return func(cfg Config) IOE.IOEither[string, int] {
if cfg.retryEnabled {
return IOE.Right[string](cfg.fallbackValue)
}
return IOE.Left[int]("retry disabled")
}
}
return Left[Config, int](e)
}
input := Left[Config, int]("network error")
cfg := Config{fallbackValue: 99, retryEnabled: true}
// Using ChainLeft in pipeline
resultChainLeft := F.Pipe1(input, ChainLeft(recoveryFn))(cfg)()
// Using OrElse in pipeline
resultOrElse := F.Pipe1(input, OrElse(recoveryFn))(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](99), resultChainLeft)
})
// Test 6: Multiple chained operations with reader context
t.Run("Multiple operations with reader - ChainLeft equals OrElse", func(t *testing.T) {
handler1 := func(e string) ReaderIOEither[Config, string, int] {
if e == "error1" {
return Right[Config, string](1)
}
return Left[Config, int](e)
}
handler2 := func(e string) ReaderIOEither[Config, string, int] {
if e == "error2" {
return func(cfg Config) IOE.IOEither[string, int] {
return IOE.Right[string](cfg.fallbackValue)
}
}
return Left[Config, int](e)
}
input := Left[Config, int]("error2")
cfg := Config{fallbackValue: 2, retryEnabled: false}
// Using ChainLeft
resultChainLeft := F.Pipe2(
input,
ChainLeft(handler1),
ChainLeft(handler2),
)(cfg)()
// Using OrElse
resultOrElse := F.Pipe2(
input,
OrElse(handler1),
OrElse(handler2),
)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](2), resultChainLeft)
})
// Test 7: Reader context is properly threaded through both functions
t.Run("Reader context threading - ChainLeft equals OrElse", func(t *testing.T) {
var chainLeftCfg, orElseCfg *Config
recoveryFn := func(e string) ReaderIOEither[Config, string, int] {
return func(cfg Config) IOE.IOEither[string, int] {
// Capture the config to verify it's passed correctly
if chainLeftCfg == nil {
chainLeftCfg = &cfg
} else {
orElseCfg = &cfg
}
return IOE.Right[string](cfg.fallbackValue)
}
}
input := Left[Config, int]("error")
cfg := Config{fallbackValue: 123, retryEnabled: true}
// Using ChainLeft
resultChainLeft := ChainLeft(recoveryFn)(input)(cfg)()
// Using OrElse
resultOrElse := OrElse(recoveryFn)(input)(cfg)()
// Both should produce identical results
assert.Equal(t, resultOrElse, resultChainLeft)
assert.Equal(t, E.Right[string](123), resultChainLeft)
// Verify both received the same config
assert.NotNil(t, chainLeftCfg)
assert.NotNil(t, orElseCfg)
assert.Equal(t, *chainLeftCfg, *orElseCfg)
assert.Equal(t, cfg, *chainLeftCfg)
})
}

View File

@@ -1,14 +1,107 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"github.com/IBM/fp-go/v2/readerioeither"
)
// ChainConsumer chains a consumer (side-effect function) into a ReaderIOResult computation,
// replacing the success value with Void (empty struct).
//
// This is useful for performing side effects (like logging, printing, or writing to a file)
// where you don't need to preserve the original value. The consumer is only executed if the
// computation succeeds; if it fails with an error, the consumer is skipped.
//
// Type parameters:
// - R: The context/environment type
// - A: The value type to consume
//
// Parameters:
// - c: A consumer function that performs a side effect on the value
//
// Returns:
//
// An Operator that executes the consumer and returns Void on success
//
// Example:
//
// import (
// "context"
// "fmt"
// RIO "github.com/IBM/fp-go/v2/readerioresult"
// )
//
// // Log a value and discard it
// logValue := RIO.ChainConsumer[context.Context](func(x int) {
// fmt.Printf("Value: %d\n", x)
// })
//
// computation := F.Pipe1(
// RIO.Of[context.Context](42),
// logValue,
// )
// // Prints "Value: 42" and returns result.Of(struct{}{})
//
//go:inline
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, Void] {
return readerioeither.ChainConsumer[R, error](c)
}
// ChainFirstConsumer chains a consumer into a ReaderIOResult computation while preserving
// the original value.
//
// This is useful for performing side effects (like logging, printing, or metrics collection)
// where you want to keep the original value for further processing. The consumer is only
// executed if the computation succeeds; if it fails with an error, the consumer is skipped
// and the error is propagated.
//
// Type parameters:
// - R: The context/environment type
// - A: The value type to consume and preserve
//
// Parameters:
// - c: A consumer function that performs a side effect on the value
//
// Returns:
//
// An Operator that executes the consumer and returns the original value on success
//
// Example:
//
// import (
// "context"
// "fmt"
// F "github.com/IBM/fp-go/v2/function"
// N "github.com/IBM/fp-go/v2/number"
// RIO "github.com/IBM/fp-go/v2/readerioresult"
// )
//
// // Log a value but keep it for further processing
// logValue := RIO.ChainFirstConsumer[context.Context](func(x int) {
// fmt.Printf("Processing: %d\n", x)
// })
//
// computation := F.Pipe2(
// RIO.Of[context.Context](10),
// logValue,
// RIO.Map[context.Context](N.Mul(2)),
// )
// // Prints "Processing: 10" and returns result.Of(20)
//
//go:inline
func ChainFirstConsumer[R, A any](c Consumer[A]) Operator[R, A, A] {
return readerioeither.ChainFirstConsumer[R, error](c)

View File

@@ -0,0 +1,360 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerioresult
import (
"context"
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestChainConsumer_Success tests that ChainConsumer executes the consumer
// and returns Void when the computation succeeds
func TestChainConsumer_Success(t *testing.T) {
// Track if consumer was called
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a successful computation and chain the consumer
computation := F.Pipe1(
Of[context.Context](42),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with correct value
assert.Equal(t, 42, consumed)
// Verify result is successful with Void
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) Void { return Void{} })(res)
assert.Equal(t, Void{}, val)
}
}
// TestChainConsumer_Failure tests that ChainConsumer does not execute
// the consumer when the computation fails
func TestChainConsumer_Failure(t *testing.T) {
// Track if consumer was called
consumerCalled := false
consumer := func(x int) {
consumerCalled = true
}
// Create a failing computation
expectedErr := errors.New("test error")
computation := F.Pipe1(
Left[context.Context, int](expectedErr),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was NOT called
assert.False(t, consumerCalled)
// Verify result is an error
assert.True(t, result.IsLeft(res))
}
// TestChainConsumer_MultipleOperations tests chaining multiple operations
// with ChainConsumer in a pipeline
func TestChainConsumer_MultipleOperations(t *testing.T) {
// Track consumer calls
var values []int
consumer := func(x int) {
values = append(values, x)
}
// Create a pipeline with multiple operations
computation := F.Pipe2(
Of[context.Context](10),
Map[context.Context](N.Mul(2)),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with transformed value
assert.Equal(t, []int{20}, values)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_Success tests that ChainFirstConsumer executes
// the consumer and preserves the original value
func TestChainFirstConsumer_Success(t *testing.T) {
// Track if consumer was called
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a successful computation and chain the consumer
computation := F.Pipe1(
Of[context.Context](42),
ChainFirstConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called with correct value
assert.Equal(t, 42, consumed)
// Verify result is successful and preserves original value
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 42, val)
}
}
// TestChainFirstConsumer_Failure tests that ChainFirstConsumer does not
// execute the consumer when the computation fails
func TestChainFirstConsumer_Failure(t *testing.T) {
// Track if consumer was called
consumerCalled := false
consumer := func(x int) {
consumerCalled = true
}
// Create a failing computation
expectedErr := errors.New("test error")
computation := F.Pipe1(
Left[context.Context, int](expectedErr),
ChainFirstConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was NOT called
assert.False(t, consumerCalled)
// Verify result is an error
assert.True(t, result.IsLeft(res))
}
// TestChainFirstConsumer_PreservesValue tests that ChainFirstConsumer
// preserves the value for further processing
func TestChainFirstConsumer_PreservesValue(t *testing.T) {
// Track consumer calls
var logged []int
logger := func(x int) {
logged = append(logged, x)
}
// Create a pipeline that logs intermediate values
computation := F.Pipe3(
Of[context.Context](10),
ChainFirstConsumer[context.Context](logger),
Map[context.Context](N.Mul(2)),
ChainFirstConsumer[context.Context](logger),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer was called at each step
assert.Equal(t, []int{10, 20}, logged)
// Verify final result
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 20, val)
}
}
// TestChainFirstConsumer_WithMap tests combining ChainFirstConsumer with Map
func TestChainFirstConsumer_WithMap(t *testing.T) {
// Track intermediate values
var intermediate int
consumer := func(x int) {
intermediate = x
}
// Create a pipeline with logging and transformation
computation := F.Pipe2(
Of[context.Context](5),
ChainFirstConsumer[context.Context](consumer),
Map[context.Context](N.Mul(3)),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer saw original value
assert.Equal(t, 5, intermediate)
// Verify final result is transformed
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) int { return 0 })(res)
assert.Equal(t, 15, val)
}
}
// TestChainConsumer_WithContext tests that consumers work with context
func TestChainConsumer_WithContext(t *testing.T) {
type Config struct {
Multiplier int
}
// Track consumer calls
var consumed int
consumer := func(x int) {
consumed = x
}
// Create a computation that uses context
computation := F.Pipe2(
Of[Config](10),
Map[Config](N.Mul(2)),
ChainConsumer[Config](consumer),
)
// Execute with context
cfg := Config{Multiplier: 3}
res := computation(cfg)()
// Verify consumer was called
assert.Equal(t, 20, consumed)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_SideEffects tests that ChainFirstConsumer
// can be used for side effects like logging
func TestChainFirstConsumer_SideEffects(t *testing.T) {
// Simulate a logging side effect
var logs []string
logValue := func(x string) {
logs = append(logs, "Processing: "+x)
}
// Create a pipeline with logging
computation := F.Pipe3(
Of[context.Context]("hello"),
ChainFirstConsumer[context.Context](logValue),
Map[context.Context](S.Append(" world")),
ChainFirstConsumer[context.Context](logValue),
)
// Execute the computation
res := computation(context.Background())()
// Verify logs were created
assert.Equal(t, []string{
"Processing: hello",
"Processing: hello world",
}, logs)
// Verify final result
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
val := result.GetOrElse(func(error) string { return "" })(res)
assert.Equal(t, "hello world", val)
}
}
// TestChainConsumer_ComplexType tests consumers with complex types
func TestChainConsumer_ComplexType(t *testing.T) {
type User struct {
Name string
Age int
}
// Track consumed user
var consumedUser *User
consumer := func(u User) {
consumedUser = &u
}
// Create a computation with a complex type
user := User{Name: "Alice", Age: 30}
computation := F.Pipe1(
Of[context.Context](user),
ChainConsumer[context.Context](consumer),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer received the user
assert.NotNil(t, consumedUser)
assert.Equal(t, "Alice", consumedUser.Name)
assert.Equal(t, 30, consumedUser.Age)
// Verify result is successful
assert.True(t, result.IsRight(res))
}
// TestChainFirstConsumer_ComplexType tests ChainFirstConsumer with complex types
func TestChainFirstConsumer_ComplexType(t *testing.T) {
type Product struct {
ID int
Name string
Price float64
}
// Track consumed products
var consumedProducts []Product
consumer := func(p Product) {
consumedProducts = append(consumedProducts, p)
}
// Create a pipeline with complex type
product := Product{ID: 1, Name: "Widget", Price: 9.99}
computation := F.Pipe2(
Of[context.Context](product),
ChainFirstConsumer[context.Context](consumer),
Map[context.Context](func(p Product) Product {
p.Price = p.Price * 1.1 // Apply 10% markup
return p
}),
)
// Execute the computation
res := computation(context.Background())()
// Verify consumer saw original product
assert.Len(t, consumedProducts, 1)
assert.Equal(t, 9.99, consumedProducts[0].Price)
// Verify final result has updated price
assert.True(t, result.IsRight(res))
if result.IsRight(res) {
finalProduct := result.GetOrElse(func(error) Product { return Product{} })(res)
assert.InDelta(t, 10.989, finalProduct.Price, 0.001)
}
}

View File

@@ -25,10 +25,11 @@ import (
"github.com/IBM/fp-go/v2/readerio"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
//go:inline
func FromReaderOption[R, A any](onNone func() error) Kleisli[R, ReaderOption[R, A], A] {
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
return RIOE.FromReaderOption[R, A](onNone)
}
@@ -113,7 +114,7 @@ func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIO
// The Either is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, B] {
func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainEitherK(ma, f)
}
@@ -121,7 +122,7 @@ func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]
// The Either is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, B] {
func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainEitherK(ma, f)
}
@@ -129,7 +130,7 @@ func MonadChainResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
func ChainEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return RIOE.ChainEitherK[R](f)
}
@@ -137,7 +138,7 @@ func ChainEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return RIOE.ChainEitherK[R](f)
}
@@ -145,12 +146,12 @@ func ChainResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, B] {
// Useful for validation or side effects that return Either.
//
//go:inline
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
@@ -158,12 +159,12 @@ func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B])
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func ChainFirstEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
//go:inline
func TapEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func TapEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
@@ -171,12 +172,12 @@ func TapEitherK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
// Useful for validation or side effects that return Either.
//
//go:inline
func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B]) ReaderIOResult[R, A] {
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
@@ -184,12 +185,12 @@ func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Result[B])
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func ChainFirstResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
//go:inline
func TapResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
func TapResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
@@ -230,17 +231,17 @@ func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
}
//go:inline
func ChainReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderOptionK[R, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderOptionK[R, A, B](onNone)
}
//go:inline
func TapReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderOptionK[R, A, B](onNone)
}
@@ -421,7 +422,7 @@ func TapIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, A] {
// If the Option is None, the provided error function is called to produce the error value.
//
//go:inline
func ChainOptionK[R, A, B any](onNone func() error) func(func(A) Option[B]) Operator[R, A, B] {
func ChainOptionK[R, A, B any](onNone Lazy[error]) func(func(A) Option[B]) Operator[R, A, B] {
return RIOE.ChainOptionK[R, A, B](onNone)
}
@@ -619,7 +620,7 @@ func Asks[R, A any](r Reader[R, A]) ReaderIOResult[R, A] {
// If the Option is None, the provided function is called to produce the error.
//
//go:inline
func FromOption[R, A any](onNone func() error) Kleisli[R, Option[A], A] {
func FromOption[R, A any](onNone Lazy[error]) Kleisli[R, Option[A], A] {
return RIOE.FromOption[R, A](onNone)
}

View File

@@ -19,6 +19,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/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
@@ -122,4 +123,6 @@ type (
// Predicate represents a function that tests a value of type A and returns a boolean.
Predicate[A any] = predicate.Predicate[A]
Void = function.Void
)

View File

@@ -475,6 +475,8 @@ func Alt[A any](that Lazy[Result[A]]) Operator[A, A] {
// If the Result is Left, it applies the provided function to the error value,
// which returns a new Result that replaces the original.
//
// Note: OrElse is identical to [ChainLeft] - both provide the same functionality for error recovery.
//
// This is useful for error recovery, fallback logic, or chaining alternative computations.
//
// Example:
@@ -656,3 +658,118 @@ func InstanceOf[A any](a any) Result[A] {
}
return Left[A](fmt.Errorf("expected %T, got %T", res, a))
}
// MonadChainLeft sequences a computation on the Left (error) channel.
// If the Result is Left, applies the function to transform or recover from the error.
// If the Result is Right, returns the Right value unchanged.
//
// This is the dual of [MonadChain] - while Chain operates on Right values,
// ChainLeft operates on Left (error) values. It's particularly useful for:
// - Error recovery: converting specific errors into successful values
// - Error transformation: changing error types or adding context
// - Fallback logic: providing alternative computations when errors occur
//
// Note: MonadChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// The function parameter receives the error value and must return a new Result[A].
// This allows you to:
// - Recover by returning Right[error](value)
// - Transform the error by returning Left[A](newError)
// - Implement conditional error handling based on error content
//
// Example - Error recovery:
//
// result := result.MonadChainLeft(
// result.Left[int](errors.New("not found")),
// func(err error) result.Result[int] {
// if err.Error() == "not found" {
// return result.Right(0) // recover with default value
// }
// return result.Left[int](err) // propagate other errors
// },
// ) // Right(0)
//
// Example - Error type transformation:
//
// result := result.MonadChainLeft(
// result.Left[string](errors.New("database error")),
// func(err error) result.Result[string] {
// return result.Left[string](fmt.Errorf("wrapped: %w", err))
// },
// ) // Left(wrapped error)
//
// Example - Right values pass through:
//
// result := result.MonadChainLeft(
// result.Right(42),
// func(err error) result.Result[int] {
// return result.Right(0) // never called
// },
// ) // Right(42) - unchanged
//
//go:inline
func MonadChainLeft[A any](fa Result[A], f Kleisli[error, A]) Result[A] {
return either.MonadChainLeft(fa, f)
}
// ChainLeft is the curried version of [MonadChainLeft].
// Returns a function that transforms Left (error) values while preserving Right values.
//
// Note: ChainLeft is identical to [OrElse] - both provide the same functionality for error recovery.
//
// This curried form is particularly useful in functional pipelines and for creating
// reusable error handlers that can be composed with other operations.
//
// The returned function can be used with [F.Pipe1], [F.Pipe2], etc., to build
// complex error handling pipelines in a point-free style.
//
// Example - Creating reusable error handlers:
//
// // Handler that recovers from "not found" errors
// recoverNotFound := result.ChainLeft(func(err error) result.Result[int] {
// if err.Error() == "not found" {
// return result.Right(0)
// }
// return result.Left[int](err)
// })
//
// result1 := recoverNotFound(result.Left[int](errors.New("not found"))) // Right(0)
// result2 := recoverNotFound(result.Right(42)) // Right(42)
//
// Example - Using in pipelines:
//
// result := F.Pipe2(
// result.Left[int](errors.New("timeout")),
// result.ChainLeft(func(err error) result.Result[int] {
// if err.Error() == "timeout" {
// return result.Right(999) // fallback value
// }
// return result.Left[int](err)
// }),
// result.Map(func(n int) string {
// return fmt.Sprintf("Value: %d", n)
// }),
// ) // Right("Value: 999")
//
// Example - Composing multiple error handlers:
//
// // First handler: convert error to string
// toStringError := result.ChainLeft(func(err error) result.Result[int] {
// return result.Left[int](errors.New(err.Error()))
// })
//
// // Second handler: add prefix
// addPrefix := result.ChainLeft(func(err error) result.Result[int] {
// return result.Left[int](fmt.Errorf("Error: %w", err))
// })
//
// result := F.Pipe2(
// result.Left[int](errors.New("failed")),
// toStringError,
// addPrefix,
// ) // Left(Error: failed)
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
return either.ChainLeft(f)
}

View File

@@ -356,3 +356,296 @@ func TestInstanceOf(t *testing.T) {
assert.Equal(t, 2, v["b"])
})
}
// TestMonadChainLeft tests the MonadChainLeft function with various scenarios
func TestMonadChainLeft(t *testing.T) {
t.Run("Left value is transformed by function", func(t *testing.T) {
// Transform error to success
result := MonadChainLeft(
Left[int](errors.New("not found")),
func(err error) Result[int] {
if err.Error() == "not found" {
return Right(0) // default value
}
return Left[int](err)
},
)
assert.Equal(t, Of(0), result)
})
t.Run("Left value error is transformed", func(t *testing.T) {
// Transform error with additional context
result := MonadChainLeft(
Left[int](errors.New("database error")),
func(err error) Result[int] {
return Left[int](fmt.Errorf("wrapped: %w", err))
},
)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Contains(t, err.Error(), "wrapped:")
assert.Contains(t, err.Error(), "database error")
})
t.Run("Right value passes through unchanged", func(t *testing.T) {
// Right value should not be affected
result := MonadChainLeft(
Right(42),
func(err error) Result[int] {
return Left[int](errors.New("should not be called"))
},
)
assert.Equal(t, Of(42), result)
})
t.Run("Chain multiple error transformations", func(t *testing.T) {
// First transformation
step1 := MonadChainLeft(
Left[int](errors.New("error1")),
func(err error) Result[int] {
return Left[int](errors.New("error2"))
},
)
// Second transformation
step2 := MonadChainLeft(
step1,
func(err error) Result[int] {
return Left[int](fmt.Errorf("final: %s", err.Error()))
},
)
assert.True(t, IsLeft(step2))
_, err := UnwrapError(step2)
assert.Equal(t, "final: error2", err.Error())
})
t.Run("Error recovery with fallback", func(t *testing.T) {
// Recover from specific errors
result := MonadChainLeft(
Left[int](errors.New("timeout")),
func(err error) Result[int] {
if err.Error() == "timeout" {
return Right(999) // fallback value
}
return Left[int](err)
},
)
assert.Equal(t, Of(999), result)
})
t.Run("Conditional error handling", func(t *testing.T) {
// Handle different error types differently
handleError := func(err error) Result[string] {
switch err.Error() {
case "not found":
return Right("default")
case "timeout":
return Right("retry")
default:
return Left[string](err)
}
}
result1 := MonadChainLeft(Left[string](errors.New("not found")), handleError)
assert.Equal(t, Of("default"), result1)
result2 := MonadChainLeft(Left[string](errors.New("timeout")), handleError)
assert.Equal(t, Of("retry"), result2)
result3 := MonadChainLeft(Left[string](errors.New("other")), handleError)
assert.True(t, IsLeft(result3))
})
t.Run("Type preservation", func(t *testing.T) {
// Ensure type is preserved through transformation
result := MonadChainLeft(
Left[string](errors.New("error")),
func(err error) Result[string] {
return Right("recovered")
},
)
assert.Equal(t, Of("recovered"), result)
})
}
// TestChainLeft tests the curried ChainLeft function
func TestChainLeft(t *testing.T) {
t.Run("Curried function transforms Left value", func(t *testing.T) {
// Create a reusable error handler
handleNotFound := ChainLeft(func(err error) Result[int] {
if err.Error() == "not found" {
return Right(0)
}
return Left[int](err)
})
result := handleNotFound(Left[int](errors.New("not found")))
assert.Equal(t, Of(0), result)
})
t.Run("Curried function with Right value", func(t *testing.T) {
handler := ChainLeft(func(err error) Result[int] {
return Left[int](errors.New("should not be called"))
})
result := handler(Right(42))
assert.Equal(t, Of(42), result)
})
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
// Create error transformer
wrapError := ChainLeft(func(err error) Result[string] {
return Left[string](fmt.Errorf("Error: %w", err))
})
result := F.Pipe1(
Left[string](errors.New("failed")),
wrapError,
)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Contains(t, err.Error(), "Error:")
assert.Contains(t, err.Error(), "failed")
})
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
// First handler: convert error to string representation
handler1 := ChainLeft(func(err error) Result[int] {
return Left[int](errors.New(err.Error()))
})
// Second handler: add prefix to error
handler2 := ChainLeft(func(err error) Result[int] {
return Left[int](fmt.Errorf("Handled: %w", err))
})
result := F.Pipe2(
Left[int](errors.New("original")),
handler1,
handler2,
)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Contains(t, err.Error(), "Handled:")
assert.Contains(t, err.Error(), "original")
})
t.Run("Error recovery in pipeline", func(t *testing.T) {
// Handler that recovers from specific errors
recoverFromTimeout := ChainLeft(func(err error) Result[int] {
if err.Error() == "timeout" {
return Right(0) // recovered value
}
return Left[int](err) // propagate other errors
})
// Test with timeout error
result1 := F.Pipe1(
Left[int](errors.New("timeout")),
recoverFromTimeout,
)
assert.Equal(t, Of(0), result1)
// Test with other error
result2 := F.Pipe1(
Left[int](errors.New("other error")),
recoverFromTimeout,
)
assert.True(t, IsLeft(result2))
})
t.Run("ChainLeft with Map combination", func(t *testing.T) {
// Combine ChainLeft with Map to handle both channels
errorHandler := ChainLeft(func(err error) Result[int] {
return Left[int](fmt.Errorf("Error: %w", err))
})
valueMapper := Map(func(n int) string {
return fmt.Sprintf("Value: %d", n)
})
// Test with Left
result1 := F.Pipe2(
Left[int](errors.New("fail")),
errorHandler,
valueMapper,
)
assert.True(t, IsLeft(result1))
// Test with Right
result2 := F.Pipe2(
Right(42),
errorHandler,
valueMapper,
)
assert.Equal(t, Of("Value: 42"), result2)
})
t.Run("Reusable error handlers", func(t *testing.T) {
// Create a library of reusable error handlers
recoverNotFound := ChainLeft(func(err error) Result[string] {
if err.Error() == "not found" {
return Right("default")
}
return Left[string](err)
})
recoverTimeout := ChainLeft(func(err error) Result[string] {
if err.Error() == "timeout" {
return Right("retry")
}
return Left[string](err)
})
// Apply handlers in sequence
result := F.Pipe2(
Left[string](errors.New("not found")),
recoverNotFound,
recoverTimeout,
)
assert.Equal(t, Of("default"), result)
})
t.Run("Error transformation pipeline", func(t *testing.T) {
// Build a pipeline that transforms errors step by step
addContext := ChainLeft(func(err error) Result[int] {
return Left[int](fmt.Errorf("context: %w", err))
})
addTimestamp := ChainLeft(func(err error) Result[int] {
return Left[int](fmt.Errorf("[2024-01-01] %w", err))
})
result := F.Pipe2(
Left[int](errors.New("base error")),
addContext,
addTimestamp,
)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Contains(t, err.Error(), "[2024-01-01]")
assert.Contains(t, err.Error(), "context:")
assert.Contains(t, err.Error(), "base error")
})
t.Run("Conditional recovery based on error content", func(t *testing.T) {
// Recover from errors matching specific patterns
smartRecover := ChainLeft(func(err error) Result[int] {
msg := err.Error()
if msg == "not found" {
return Right(0)
}
if msg == "timeout" {
return Right(-1)
}
if msg == "unauthorized" {
return Right(-2)
}
return Left[int](err)
})
assert.Equal(t, Of(0), smartRecover(Left[int](errors.New("not found"))))
assert.Equal(t, Of(-1), smartRecover(Left[int](errors.New("timeout"))))
assert.Equal(t, Of(-2), smartRecover(Left[int](errors.New("unauthorized"))))
assert.True(t, IsLeft(smartRecover(Left[int](errors.New("unknown")))))
})
}

262
v2/result/filterable.go Normal file
View File

@@ -0,0 +1,262 @@
// 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 result provides filterable operations for Result types.
//
// This package implements the Fantasy Land Filterable specification:
// https://github.com/fantasyland/fantasy-land#filterable
//
// Since Result[A] is an alias for Either[error, A], these functions are
// thin wrappers around the corresponding either package functions, specialized
// for the common case where the error type is Go's built-in error interface.
package result
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
// Partition separates a [Result] value into a [Pair] based on a predicate function.
// It returns a function that takes a Result and produces a Pair of Result values,
// where the first element contains values that fail the predicate and the second
// contains values that pass the predicate.
//
// This function implements the Filterable specification's partition operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error (Left), both elements of the resulting Pair will be the same error
// - If the input is Ok (Right) and the predicate returns true, the result is (Err(empty), Ok(value))
// - If the input is Ok (Right) and the predicate returns false, the result is (Ok(value), Err(empty))
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default error to use when creating error Results for partitioning
//
// Returns:
//
// A function that takes a Result[A] and returns a Pair where:
// - First element: Result values that fail the predicate (or original error)
// - Second element: Result values that pass the predicate (or original error)
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// N "github.com/IBM/fp-go/v2/number"
// P "github.com/IBM/fp-go/v2/pair"
// "errors"
// )
//
// // Partition positive and non-positive numbers
// isPositive := N.MoreThan(0)
// partition := R.Partition(isPositive, errors.New("not positive"))
//
// // Ok value that passes predicate
// result1 := partition(R.Of(5))
// // result1 = Pair(Err("not positive"), Ok(5))
//
// // Ok value that fails predicate
// result2 := partition(R.Of(-3))
// // result2 = Pair(Ok(-3), Err("not positive"))
//
// // Error passes through unchanged in both positions
// result3 := partition(R.Error[int](errors.New("original error")))
// // result3 = Pair(Err("original error"), Err("original error"))
//
//go:inline
func Partition[A any](p Predicate[A], empty error) func(Result[A]) Pair[Result[A], Result[A]] {
return either.Partition(p, empty)
}
// Filter creates a filtering operation for [Result] values based on a predicate function.
// It returns a function that takes a Result and produces a Result, where Ok values
// that fail the predicate are converted to error Results with the provided error.
//
// This function implements the Filterable specification's filter operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error, it passes through unchanged
// - If the input is Ok and the predicate returns true, the Ok value passes through unchanged
// - If the input is Ok and the predicate returns false, it's converted to Err(empty)
//
// Parameters:
// - p: A predicate function that tests values of type A
// - empty: The default error to use when filtering out Ok values that fail the predicate
//
// Returns:
//
// An Operator function that takes a Result[A] and returns a Result[A] where:
// - Error values pass through unchanged
// - Ok values that pass the predicate remain as Ok
// - Ok values that fail the predicate become Err(empty)
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// N "github.com/IBM/fp-go/v2/number"
// "errors"
// )
//
// // Filter to keep only positive numbers
// isPositive := N.MoreThan(0)
// filterPositive := R.Filter(isPositive, errors.New("not positive"))
//
// // Ok value that passes predicate - remains Ok
// result1 := filterPositive(R.Of(5))
// // result1 = Ok(5)
//
// // Ok value that fails predicate - becomes Err
// result2 := filterPositive(R.Of(-3))
// // result2 = Err("not positive")
//
// // Error passes through unchanged
// result3 := filterPositive(R.Error[int](errors.New("original error")))
// // result3 = Err("original error")
//
// // Chaining filters
// isEven := func(n int) bool { return n%2 == 0 }
// filterEven := R.Filter(isEven, errors.New("not even"))
//
// result4 := filterEven(filterPositive(R.Of(4)))
// // result4 = Ok(4) - passes both filters
//
// result5 := filterEven(filterPositive(R.Of(3)))
// // result5 = Err("not even") - passes first, fails second
//
//go:inline
func Filter[A any](p Predicate[A], empty error) Operator[A, A] {
return either.Filter(p, empty)
}
// FilterMap combines filtering and mapping operations for [Result] values using an [Option]-returning function.
// It returns a function that takes a Result[A] and produces a Result[B], where Ok values
// are transformed by applying the function f. If f returns Some(B), the result is Ok(B). If f returns
// None, the result is Err(empty).
//
// This function implements the Filterable specification's filterMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error, it passes through with its error value preserved
// - If the input is Ok and f returns Some(B), the result is Ok(B)
// - If the input is Ok and f returns None, the result is Err(empty)
//
// Parameters:
// - f: An Option Kleisli function that transforms values of type A to Option[B]
// - empty: The default error to use when f returns None
//
// Returns:
//
// An Operator function that takes a Result[A] and returns a Result[B] where:
// - Error values pass through with error preserved
// - Ok values are transformed by f: Some(B) becomes Ok(B), None becomes Err(empty)
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// O "github.com/IBM/fp-go/v2/option"
// "errors"
// "strconv"
// )
//
// // Parse string to int, filtering out invalid values
// parseInt := func(s string) O.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
// filterMapInt := R.FilterMap(parseInt, errors.New("invalid number"))
//
// // Valid number string - transforms to Ok(int)
// result1 := filterMapInt(R.Of("42"))
// // result1 = Ok(42)
//
// // Invalid number string - becomes Err
// result2 := filterMapInt(R.Of("abc"))
// // result2 = Err("invalid number")
//
// // Error passes through with error preserved
// result3 := filterMapInt(R.Error[string](errors.New("original error")))
// // result3 = Err("original error")
//
//go:inline
func FilterMap[A, B any](f option.Kleisli[A, B], empty error) Operator[A, B] {
return either.FilterMap(f, empty)
}
// PartitionMap separates and transforms a [Result] value into a [Pair] of Result values using a mapping function.
// It returns a function that takes a Result[A] and produces a Pair of Result values, where the mapping
// function f transforms the Ok value into Either[B, C]. The result is partitioned based on whether f
// produces a Left or Right value.
//
// This function implements the Filterable specification's partitionMap operation:
// https://github.com/fantasyland/fantasy-land#filterable
//
// The behavior is as follows:
// - If the input is an error, both elements of the resulting Pair will be errors with the original error
// - If the input is Ok and f returns Left(B), the result is (Ok(B), Err(empty))
// - If the input is Ok and f returns Right(C), the result is (Err(empty), Ok(C))
//
// Parameters:
// - f: A Kleisli function that transforms values of type A to Either[B, C]
// - empty: The default error to use when creating error Results for partitioning
//
// Returns:
//
// A function that takes a Result[A] and returns a Pair[Result[B], Result[C]] where:
// - If input is error: (Err(original_error), Err(original_error))
// - If f returns Left(B): (Ok(B), Err(empty))
// - If f returns Right(C): (Err(empty), Ok(C))
//
// Example:
//
// import (
// R "github.com/IBM/fp-go/v2/result"
// E "github.com/IBM/fp-go/v2/either"
// P "github.com/IBM/fp-go/v2/pair"
// "errors"
// "strconv"
// )
//
// // Classify and transform numbers: negative -> error message, positive -> squared value
// classifyNumber := func(n int) E.Either[string, int] {
// if n < 0 {
// return E.Left[int]("negative: " + strconv.Itoa(n))
// }
// return E.Right[string](n * n)
// }
// partitionMap := R.PartitionMap(classifyNumber, errors.New("not classified"))
//
// // Positive number - goes to right side as squared value
// result1 := partitionMap(R.Of(5))
// // result1 = Pair(Err("not classified"), Ok(25))
//
// // Negative number - goes to left side with error message
// result2 := partitionMap(R.Of(-3))
// // result2 = Pair(Ok("negative: -3"), Err("not classified"))
//
// // Original error - appears in both positions
// result3 := partitionMap(R.Error[int](errors.New("original error")))
// // result3 = Pair(Err("original error"), Err("original error"))
//
//go:inline
func PartitionMap[A, B, C any](f either.Kleisli[B, A, C], empty error) func(Result[A]) Pair[Result[B], Result[C]] {
return either.PartitionMap(f, empty)
}

View File

@@ -0,0 +1,689 @@
// 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 result
import (
"errors"
"math"
"strconv"
"testing"
E "github.com/IBM/fp-go/v2/either"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
func TestPartition(t *testing.T) {
t.Run("Ok value that passes predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(5)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsRight(right), "right should be Ok")
rightVal, _ := Unwrap(right)
assert.Equal(t, 5, rightVal)
})
t.Run("Ok value that fails predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(-3)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsRight(left), "left should be Ok (failed predicate)")
assert.True(t, IsLeft(right), "right should be error")
leftVal, _ := Unwrap(left)
assert.Equal(t, -3, leftVal)
})
t.Run("Ok value at boundary (zero)", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(0)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsRight(left), "left should be Ok (zero fails predicate)")
assert.True(t, IsLeft(right), "right should be error")
leftVal, _ := Unwrap(left)
assert.Equal(t, 0, leftVal)
})
t.Run("Error passes through unchanged", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
originalError := errors.New("original error")
input := Left[int](originalError)
// Act
result := partition(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsLeft(right), "right should be error")
_, leftErr := Unwrap(left)
_, rightErr := Unwrap(right)
assert.Equal(t, originalError, leftErr)
assert.Equal(t, originalError, rightErr)
})
t.Run("String predicate - even length strings", func(t *testing.T) {
// Arrange
isEvenLength := func(s string) bool { return len(s)%2 == 0 }
partition := Partition(isEvenLength, errors.New("odd length"))
// Act & Assert - passes predicate
result1 := partition(Of("test"))
left1, right1 := P.Unpack(result1)
assert.True(t, IsLeft(left1))
assert.True(t, IsRight(right1))
rightVal1, _ := Unwrap(right1)
assert.Equal(t, "test", rightVal1)
// Act & Assert - fails predicate
result2 := partition(Of("hello"))
left2, right2 := P.Unpack(result2)
assert.True(t, IsRight(left2))
assert.True(t, IsLeft(right2))
leftVal2, _ := Unwrap(left2)
assert.Equal(t, "hello", leftVal2)
})
t.Run("Complex type predicate - struct field check", func(t *testing.T) {
// Arrange
type Person struct {
Name string
Age int
}
isAdult := func(p Person) bool { return p.Age >= 18 }
partition := Partition(isAdult, errors.New("minor"))
// Act & Assert - adult passes
adult := Person{Name: "Alice", Age: 25}
result1 := partition(Of(adult))
left1, right1 := P.Unpack(result1)
assert.True(t, IsLeft(left1))
assert.True(t, IsRight(right1))
rightVal1, _ := Unwrap(right1)
assert.Equal(t, adult, rightVal1)
// Act & Assert - minor fails
minor := Person{Name: "Bob", Age: 15}
result2 := partition(Of(minor))
left2, right2 := P.Unpack(result2)
assert.True(t, IsRight(left2))
assert.True(t, IsLeft(right2))
leftVal2, _ := Unwrap(left2)
assert.Equal(t, minor, leftVal2)
})
}
func TestFilter(t *testing.T) {
t.Run("Ok value that passes predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(5)
// Act
result := filter(input)
// Assert
assert.True(t, IsRight(result), "result should be Ok")
val, _ := Unwrap(result)
assert.Equal(t, 5, val)
})
t.Run("Ok value that fails predicate", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(-3)
// Act
result := filter(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, "not positive", err.Error())
})
t.Run("Ok value at boundary (zero)", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(0)
// Act
result := filter(input)
// Assert
assert.True(t, IsLeft(result), "zero should fail predicate")
_, err := Unwrap(result)
assert.Equal(t, "not positive", err.Error())
})
t.Run("Error passes through unchanged", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
originalError := errors.New("original error")
input := Left[int](originalError)
// Act
result := filter(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, originalError, err)
})
t.Run("Chaining multiple filters", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
isEven := func(n int) bool { return n%2 == 0 }
filterPositive := Filter(isPositive, errors.New("not positive"))
filterEven := Filter(isEven, errors.New("not even"))
// Act & Assert - passes both filters
result1 := filterEven(filterPositive(Of(4)))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, 4, val1)
// Act & Assert - passes first, fails second
result2 := filterEven(filterPositive(Of(3)))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "not even", err2.Error())
// Act & Assert - fails first filter
result3 := filterEven(filterPositive(Of(-2)))
assert.True(t, IsLeft(result3))
_, err3 := Unwrap(result3)
assert.Equal(t, "not positive", err3.Error())
// Act & Assert - error passes through both
originalErr := errors.New("original")
result4 := filterEven(filterPositive(Left[int](originalErr)))
assert.True(t, IsLeft(result4))
_, err4 := Unwrap(result4)
assert.Equal(t, originalErr, err4)
})
t.Run("Filter preserves error", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("default error"))
// Act - error with different message
originalError := errors.New("server error")
result := filter(Left[int](originalError))
// Assert - original error preserved
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Equal(t, originalError, err)
})
}
func TestFilterMap(t *testing.T) {
t.Run("Ok value with Some result", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid number"))
input := Of("42")
// Act
result := filterMap(input)
// Assert
assert.True(t, IsRight(result), "result should be Ok")
val, _ := Unwrap(result)
assert.Equal(t, 42, val)
})
t.Run("Ok value with None result", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid number"))
input := Of("abc")
// Act
result := filterMap(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, "invalid number", err.Error())
})
t.Run("Error passes through", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid number"))
originalError := errors.New("original error")
input := Left[string](originalError)
// Act
result := filterMap(input)
// Assert
assert.True(t, IsLeft(result), "result should be error")
_, err := Unwrap(result)
assert.Equal(t, originalError, err)
})
t.Run("Extract optional field from struct", func(t *testing.T) {
// Arrange
type Person struct {
Name string
Email O.Option[string]
}
extractEmail := func(p Person) O.Option[string] { return p.Email }
filterMap := FilterMap(extractEmail, errors.New("no email"))
// Act & Assert - has email
result1 := filterMap(Of(Person{Name: "Alice", Email: O.Some("alice@example.com")}))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, "alice@example.com", val1)
// Act & Assert - no email
result2 := filterMap(Of(Person{Name: "Bob", Email: O.None[string]()}))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "no email", err2.Error())
})
t.Run("Transform and filter numbers", func(t *testing.T) {
// Arrange
sqrtIfPositive := func(n int) O.Option[float64] {
if n >= 0 {
return O.Some(math.Sqrt(float64(n)))
}
return O.None[float64]()
}
filterMap := FilterMap(sqrtIfPositive, errors.New("negative number"))
// Act & Assert - positive number
result1 := filterMap(Of(16))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, 4.0, val1)
// Act & Assert - negative number
result2 := filterMap(Of(-4))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "negative number", err2.Error())
// Act & Assert - zero
result3 := filterMap(Of(0))
assert.True(t, IsRight(result3))
val3, _ := Unwrap(result3)
assert.Equal(t, 0.0, val3)
})
t.Run("Chain multiple FilterMap operations", func(t *testing.T) {
// Arrange
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
doubleIfEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
filterMap1 := FilterMap(parseInt, errors.New("invalid number"))
filterMap2 := FilterMap(doubleIfEven, errors.New("not even"))
// Act & Assert - valid even number
result1 := filterMap2(filterMap1(Of("4")))
assert.True(t, IsRight(result1))
val1, _ := Unwrap(result1)
assert.Equal(t, 8, val1)
// Act & Assert - valid odd number
result2 := filterMap2(filterMap1(Of("3")))
assert.True(t, IsLeft(result2))
_, err2 := Unwrap(result2)
assert.Equal(t, "not even", err2.Error())
// Act & Assert - invalid number
result3 := filterMap2(filterMap1(Of("abc")))
assert.True(t, IsLeft(result3))
_, err3 := Unwrap(result3)
assert.Equal(t, "invalid number", err3.Error())
})
}
func TestPartitionMap(t *testing.T) {
t.Run("Ok value that maps to Left", func(t *testing.T) {
// Arrange
classifyNumber := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative: " + strconv.Itoa(n))
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
input := Of(-3)
// Act
result := partitionMap(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsRight(left), "left should be Ok (contains error from f)")
assert.True(t, IsLeft(right), "right should be error")
leftVal, _ := Unwrap(left)
assert.Equal(t, "negative: -3", leftVal)
})
t.Run("Ok value that maps to Right", func(t *testing.T) {
// Arrange
classifyNumber := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative: " + strconv.Itoa(n))
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
input := Of(5)
// Act
result := partitionMap(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsRight(right), "right should be Ok (contains value from f)")
rightVal, _ := Unwrap(right)
assert.Equal(t, 25, rightVal)
})
t.Run("Error passes through to both sides", func(t *testing.T) {
// Arrange
classifyNumber := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative")
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classifyNumber, errors.New("not classified"))
originalError := errors.New("original error")
input := Left[int](originalError)
// Act
result := partitionMap(input)
left, right := P.Unpack(result)
// Assert
assert.True(t, IsLeft(left), "left should be error")
assert.True(t, IsLeft(right), "right should be error")
_, leftErr := Unwrap(left)
_, rightErr := Unwrap(right)
assert.Equal(t, originalError, leftErr)
assert.Equal(t, originalError, rightErr)
})
t.Run("Validate and transform user input", func(t *testing.T) {
// Arrange
type ValidationError struct {
Field string
Message string
}
type User struct {
Name string
Age int
}
validateUser := func(input map[string]string) E.Either[ValidationError, User] {
name, hasName := input["name"]
ageStr, hasAge := input["age"]
if !hasName {
return E.Left[User](ValidationError{"name", "missing"})
}
if !hasAge {
return E.Left[User](ValidationError{"age", "missing"})
}
age, err := strconv.Atoi(ageStr)
if err != nil {
return E.Left[User](ValidationError{"age", "invalid"})
}
return E.Right[ValidationError](User{name, age})
}
partitionMap := PartitionMap(validateUser, errors.New("not processed"))
// Act & Assert - valid input
validInput := map[string]string{"name": "Alice", "age": "30"}
result1 := partitionMap(Of(validInput))
left1, right1 := P.Unpack(result1)
assert.True(t, IsLeft(left1))
assert.True(t, IsRight(right1))
rightVal1, _ := Unwrap(right1)
assert.Equal(t, User{"Alice", 30}, rightVal1)
// Act & Assert - invalid input (missing age)
invalidInput := map[string]string{"name": "Bob"}
result2 := partitionMap(Of(invalidInput))
left2, right2 := P.Unpack(result2)
assert.True(t, IsRight(left2))
assert.True(t, IsLeft(right2))
leftVal2, _ := Unwrap(left2)
assert.Equal(t, ValidationError{"age", "missing"}, leftVal2)
})
t.Run("Classify strings by length", func(t *testing.T) {
// Arrange
classifyString := func(s string) E.Either[string, int] {
if len(s) < 5 {
return E.Left[int]("too short: " + s)
}
return E.Right[string](len(s))
}
partitionMap := PartitionMap(classifyString, errors.New("not classified"))
// Act & Assert - short string
result1 := partitionMap(Of("hi"))
left1, right1 := P.Unpack(result1)
assert.True(t, IsRight(left1))
assert.True(t, IsLeft(right1))
leftVal1, _ := Unwrap(left1)
assert.Equal(t, "too short: hi", leftVal1)
// Act & Assert - long string
result2 := partitionMap(Of("hello world"))
left2, right2 := P.Unpack(result2)
assert.True(t, IsLeft(left2))
assert.True(t, IsRight(right2))
rightVal2, _ := Unwrap(right2)
assert.Equal(t, 11, rightVal2)
})
}
// Benchmark tests
func BenchmarkPartition(b *testing.B) {
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partition(input)
}
}
func BenchmarkPartitionError(b *testing.B) {
isPositive := N.MoreThan(0)
partition := Partition(isPositive, errors.New("not positive"))
input := Left[int](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partition(input)
}
}
func BenchmarkFilter(b *testing.B) {
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filter(input)
}
}
func BenchmarkFilterError(b *testing.B) {
isPositive := N.MoreThan(0)
filter := Filter(isPositive, errors.New("not positive"))
input := Left[int](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filter(input)
}
}
func BenchmarkFilterChained(b *testing.B) {
isPositive := N.MoreThan(0)
isEven := func(n int) bool { return n%2 == 0 }
filterPositive := Filter(isPositive, errors.New("not positive"))
filterEven := Filter(isEven, errors.New("not even"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filterEven(filterPositive(input))
}
}
func BenchmarkFilterMap(b *testing.B) {
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid"))
input := Of("42")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filterMap(input)
}
}
func BenchmarkFilterMapError(b *testing.B) {
parseInt := func(s string) O.Option[int] {
if n, err := strconv.Atoi(s); err == nil {
return O.Some(n)
}
return O.None[int]()
}
filterMap := FilterMap(parseInt, errors.New("invalid"))
input := Left[string](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = filterMap(input)
}
}
func BenchmarkPartitionMap(b *testing.B) {
classify := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative")
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classify, errors.New("not classified"))
input := Of(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partitionMap(input)
}
}
func BenchmarkPartitionMapError(b *testing.B) {
classify := func(n int) E.Either[string, int] {
if n < 0 {
return E.Left[int]("negative")
}
return E.Right[string](n * n)
}
partitionMap := PartitionMap(classify, errors.New("not classified"))
input := Left[int](errors.New("error"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = partitionMap(input)
}
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
)
@@ -61,4 +62,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]
Pair[L, R any] = pair.Pair[L, R]
)