1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-07 23:03:15 +02:00

Compare commits

...

11 Commits

Author SHA1 Message Date
Dr. Carsten Leue
02d0be9dad fix: add traversal for sequences
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-14 14:12:44 +01:00
Dr. Carsten Leue
2c1d8196b4 fix: support go iterators and cleanup types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-14 12:56:12 +01:00
Dr. Carsten Leue
17eb8ae66f fix: add Chain...Left methods
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 16:51:15 +01:00
Dr. Carsten Leue
b70e481e7d fix: some minor improvements
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 12:56:51 +01:00
Dr. Carsten Leue
3c3bb7c166 fix: improve lens implementation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 12:15:52 +01:00
Dr. Carsten Leue
d3007cbbfa fix: improve lens generator
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:39:18 +01:00
Dr. Carsten Leue
5aa0e1ea2e fix: handle non comparable types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:35:56 +01:00
Dr. Carsten Leue
d586428cb0 fix: examples
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:05:57 +01:00
Dr. Carsten Leue
d2dbce6e8b fix: improve lens handling
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 18:23:57 +01:00
Dr. Carsten Leue
6f7ec0768d fix: improve lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 17:28:20 +01:00
Dr. Carsten Leue
ca813b673c fix: better tests and doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 16:24:12 +01:00
115 changed files with 11468 additions and 1125 deletions

View File

@@ -17,11 +17,10 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
)
@@ -50,16 +49,16 @@ func Replicate[A any](n int, a A) []A {
// This is the monadic version of Map that takes the array as the first parameter.
//
//go:inline
func MonadMap[A, B any](as []A, f func(a A) B) []B {
func MonadMap[A, B any](as []A, f func(A) B) []B {
return G.MonadMap[[]A, []B](as, f)
}
// MonadMapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is useful when you need to access elements by reference without copying.
func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
func MonadMapRef[A, B any](as []A, f func(*A) B) []B {
count := len(as)
bs := make([]B, count)
for i := count - 1; i >= 0; i-- {
for i := range count {
bs[i] = f(&as[i])
}
return bs
@@ -68,7 +67,7 @@ func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
// MapWithIndex applies a function to each element and its index in an array, returning a new array with the results.
//
//go:inline
func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
func MapWithIndex[A, B any](f func(int, A) B) Operator[A, B] {
return G.MapWithIndex[[]A, []B](f)
}
@@ -81,35 +80,35 @@ func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
// result := double([]int{1, 2, 3}) // [2, 4, 6]
//
//go:inline
func Map[A, B any](f func(a A) B) func([]A) []B {
func Map[A, B any](f func(A) B) Operator[A, B] {
return G.Map[[]A, []B](f)
}
// MapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is the curried version that returns a function.
func MapRef[A, B any](f func(a *A) B) func([]A) []B {
func MapRef[A, B any](f func(*A) B) Operator[A, B] {
return F.Bind2nd(MonadMapRef[A, B], f)
}
func filterRef[A any](fa []A, pred func(a *A) bool) []A {
var result []A
func filterRef[A any](fa []A, pred func(*A) bool) []A {
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, a)
var result []A = make([]A, 0, count)
for i := range count {
a := &fa[i]
if pred(a) {
result = append(result, *a)
}
}
return result
}
func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
var result []B
func filterMapRef[A, B any](fa []A, pred func(*A) bool, f func(*A) B) []B {
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, f(&a))
var result []B = make([]B, 0, count)
for i := range count {
a := &fa[i]
if pred(a) {
result = append(result, f(a))
}
}
return result
@@ -118,19 +117,19 @@ func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
// Filter returns a new array with all elements from the original array that match a predicate
//
//go:inline
func Filter[A any](pred func(A) bool) EM.Endomorphism[[]A] {
func Filter[A any](pred func(A) bool) Operator[A, A] {
return G.Filter[[]A](pred)
}
// FilterWithIndex returns a new array with all elements from the original array that match a predicate
//
//go:inline
func FilterWithIndex[A any](pred func(int, A) bool) EM.Endomorphism[[]A] {
func FilterWithIndex[A any](pred func(int, A) bool) Operator[A, A] {
return G.FilterWithIndex[[]A](pred)
}
// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers.
func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
func FilterRef[A any](pred func(*A) bool) Operator[A, A] {
return F.Bind2nd(filterRef[A], pred)
}
@@ -138,7 +137,7 @@ func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
// This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
func MonadFilterMap[A, B any](fa []A, f option.Kleisli[A, B]) []B {
return G.MonadFilterMap[[]A, []B](fa, f)
}
@@ -146,33 +145,33 @@ func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
// keeping only the Some values. This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) O.Option[B]) []B {
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) Option[B]) []B {
return G.MonadFilterMapWithIndex[[]A, []B](fa, f)
}
// FilterMap maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
// FilterMap maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMap[A, B any](f func(A) O.Option[B]) func([]A) []B {
func FilterMap[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return G.FilterMap[[]A, []B](f)
}
// FilterMapWithIndex maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
// FilterMapWithIndex maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMapWithIndex[A, B any](f func(int, A) O.Option[B]) func([]A) []B {
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 [O.Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// 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.
//
//go:inline
func FilterChain[A, B any](f func(A) O.Option[[]B]) func([]A) []B {
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.FilterChain[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B {
func FilterMapRef[A, B any](pred func(a *A) bool, f func(*A) B) Operator[A, B] {
return func(fa []A) []B {
return filterMapRef(fa, pred, f)
}
@@ -180,8 +179,7 @@ func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B
func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
current := initial
count := len(fa)
for i := 0; i < count; i++ {
for i := range len(fa) {
current = f(current, &fa[i])
}
return current
@@ -277,7 +275,7 @@ func Of[A any](a A) []A {
// This is the monadic version that takes the array as the first parameter (also known as FlatMap).
//
//go:inline
func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
func MonadChain[A, B any](fa []A, f Kleisli[A, B]) []B {
return G.MonadChain(fa, f)
}
@@ -290,7 +288,7 @@ func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
// result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3]
//
//go:inline
func Chain[A, B any](f func(A) []B) func([]A) []B {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return G.Chain[[]A](f)
}
@@ -306,7 +304,7 @@ func MonadAp[B, A any](fab []func(A) B, fa []A) []B {
// This is the curried version.
//
//go:inline
func Ap[B, A any](fa []A) func([]func(A) B) []B {
func Ap[B, A any](fa []A) Operator[func(A) B, B] {
return G.Ap[[]B, []func(A) B](fa)
}
@@ -328,7 +326,7 @@ func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A)
// Returns None if the array is empty.
//
//go:inline
func Tail[A any](as []A) O.Option[[]A] {
func Tail[A any](as []A) Option[[]A] {
return G.Tail(as)
}
@@ -336,7 +334,7 @@ func Tail[A any](as []A) O.Option[[]A] {
// Returns None if the array is empty.
//
//go:inline
func Head[A any](as []A) O.Option[A] {
func Head[A any](as []A) Option[A] {
return G.Head(as)
}
@@ -344,7 +342,7 @@ func Head[A any](as []A) O.Option[A] {
// Returns None if the array is empty.
//
//go:inline
func First[A any](as []A) O.Option[A] {
func First[A any](as []A) Option[A] {
return G.First(as)
}
@@ -352,12 +350,12 @@ func First[A any](as []A) O.Option[A] {
// Returns None if the array is empty.
//
//go:inline
func Last[A any](as []A) O.Option[A] {
func Last[A any](as []A) Option[A] {
return G.Last(as)
}
// PrependAll inserts a separator before each element of an array.
func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
func PrependAll[A any](middle A) Operator[A, A] {
return func(as []A) []A {
count := len(as)
dst := count * 2
@@ -377,7 +375,7 @@ func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
// Example:
//
// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3]
func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
func Intersperse[A any](middle A) Operator[A, A] {
prepend := PrependAll(middle)
return func(as []A) []A {
if IsEmpty(as) {
@@ -406,7 +404,7 @@ func Flatten[A any](mma [][]A) []A {
}
// Slice extracts a subarray from index low (inclusive) to high (exclusive).
func Slice[A any](low, high int) func(as []A) []A {
func Slice[A any](low, high int) Operator[A, A] {
return array.Slice[[]A](low, high)
}
@@ -414,7 +412,7 @@ func Slice[A any](low, high int) func(as []A) []A {
// Returns None if the index is out of bounds.
//
//go:inline
func Lookup[A any](idx int) func([]A) O.Option[A] {
func Lookup[A any](idx int) func([]A) Option[A] {
return G.Lookup[[]A](idx)
}
@@ -422,7 +420,7 @@ func Lookup[A any](idx int) func([]A) O.Option[A] {
// If the index is out of bounds, the element is appended.
//
//go:inline
func UpsertAt[A any](a A) EM.Endomorphism[[]A] {
func UpsertAt[A any](a A) Operator[A, A] {
return G.UpsertAt[[]A](a)
}
@@ -468,7 +466,7 @@ func ConstNil[A any]() []A {
// SliceRight extracts a subarray from the specified start index to the end.
//
//go:inline
func SliceRight[A any](start int) EM.Endomorphism[[]A] {
func SliceRight[A any](start int) Operator[A, A] {
return G.SliceRight[[]A](start)
}
@@ -482,7 +480,7 @@ func Copy[A any](b []A) []A {
// Clone creates a deep copy of the array using the provided endomorphism to clone the values
//
//go:inline
func Clone[A any](f func(A) A) func(as []A) []A {
func Clone[A any](f func(A) A) Operator[A, A] {
return G.Clone[[]A](f)
}
@@ -510,8 +508,8 @@ func Fold[A any](m M.Monoid[A]) func([]A) A {
// Push adds an element to the end of an array (alias for Append).
//
//go:inline
func Push[A any](a A) EM.Endomorphism[[]A] {
return G.Push[EM.Endomorphism[[]A]](a)
func Push[A any](a A) Operator[A, A] {
return G.Push[Operator[A, A]](a)
}
// MonadFlap applies a value to an array of functions, producing an array of results.
@@ -526,13 +524,13 @@ func MonadFlap[B, A any](fab []func(A) B, a A) []B {
// This is the curried version.
//
//go:inline
func Flap[B, A any](a A) func([]func(A) B) []B {
func Flap[B, A any](a A) Operator[func(A) B, B] {
return G.Flap[func(A) B, []func(A) B, []B](a)
}
// Prepend adds an element to the beginning of an array, returning a new array.
//
//go:inline
func Prepend[A any](head A) EM.Endomorphism[[]A] {
return G.Prepend[EM.Endomorphism[[]A]](head)
func Prepend[A any](head A) Operator[A, A] {
return G.Prepend[Operator[A, A]](head)
}

View File

@@ -56,8 +56,8 @@ func Do[S any](
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) []T,
) func([]S1) []S2 {
f Kleisli[S1, T],
) Operator[S1, S2] {
return G.Bind[[]S1, []S2](setter, f)
}
@@ -79,7 +79,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.Let[[]S1, []S2](setter, f)
}
@@ -101,7 +101,7 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.LetTo[[]S1, []S2](setter, b)
}
@@ -120,7 +120,7 @@ func LetTo[S1, S2, T any](
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) func([]T) []S1 {
) Operator[T, S1] {
return G.BindTo[[]S1, []T](setter)
}
@@ -143,6 +143,6 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa []T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.ApS[[]S1, []S2](setter, fa)
}

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/option"
)
// FindFirst finds the first element which satisfies a predicate function.
@@ -30,7 +30,7 @@ import (
// result2 := findGreaterThan3([]int{1, 2, 3}) // None
//
//go:inline
func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
func FindFirst[A any](pred func(A) bool) option.Kleisli[[]A, A] {
return G.FindFirst[[]A](pred)
}
@@ -45,7 +45,7 @@ func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
// result := findEvenAtEvenIndex([]int{1, 3, 4, 5}) // Some(4)
//
//go:inline
func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
func FindFirstWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
return G.FindFirstWithIndex[[]A](pred)
}
@@ -65,7 +65,7 @@ func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
// result := parseFirst([]string{"a", "42", "b"}) // Some(42)
//
//go:inline
func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
func FindFirstMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
return G.FindFirstMap[[]A](sel)
}
@@ -73,7 +73,7 @@ func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
// The selector receives both the index and the element.
//
//go:inline
func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
func FindFirstMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
return G.FindFirstMapWithIndex[[]A](sel)
}
@@ -86,7 +86,7 @@ func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.O
// result := findGreaterThan3([]int{1, 4, 2, 5}) // Some(5)
//
//go:inline
func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
func FindLast[A any](pred func(A) bool) option.Kleisli[[]A, A] {
return G.FindLast[[]A](pred)
}
@@ -94,7 +94,7 @@ func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
// Returns Some(element) if found, None if no element matches.
//
//go:inline
func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
func FindLastWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
return G.FindLastWithIndex[[]A](pred)
}
@@ -102,7 +102,7 @@ func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
// This combines finding and mapping in a single operation, searching from the end.
//
//go:inline
func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
func FindLastMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
return G.FindLastMap[[]A](sel)
}
@@ -110,6 +110,6 @@ func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
// The selector receives both the index and the element, searching from the end.
//
//go:inline
func FindLastMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
func FindLastMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
return G.FindLastMapWithIndex[[]A](sel)
}

View File

@@ -82,7 +82,7 @@ func MakeBy[AS ~[]A, F ~func(int) A, A any](n int, f F) AS {
}
// run the generator function across the input
as := make(AS, n)
for i := n - 1; i >= 0; i-- {
for i := range n {
as[i] = f(i)
}
return as
@@ -165,10 +165,9 @@ func Size[GA ~[]A, A any](as GA) int {
func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for _, a := range fa {
O.Map(func(b B) B {
if b, ok := O.Unwrap(f(a)); ok {
result = append(result, b)
return b
})(f(a))
}
}
return result
}
@@ -176,10 +175,9 @@ func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
func filterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(int, A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for i, a := range fa {
O.Map(func(b B) B {
if b, ok := O.Unwrap(f(i, a)); ok {
result = append(result, b)
return b
})(f(i, a))
}
}
return result
}

View File

@@ -42,8 +42,7 @@ func FindFirst[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) O.Option[
func FindFirstMapWithIndex[AS ~[]A, PRED ~func(int, A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
none := O.None[B]()
return func(as AS) O.Option[B] {
count := len(as)
for i := 0; i < count; i++ {
for i := range len(as) {
out := pred(i, as[i])
if O.IsSome(out) {
return out

View File

@@ -26,7 +26,7 @@ import (
func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, fb BS, f FCT) CS {
l := N.Min(len(fa), len(fb))
res := make(CS, l)
for i := l - 1; i >= 0; i-- {
for i := range l {
res[i] = f(fa[i], fb[i])
}
return res
@@ -43,7 +43,7 @@ func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS,
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := l - 1; i >= 0; i-- {
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2

View File

@@ -86,7 +86,7 @@ func Sequence[A, HKTA, HKTRA, HKTFRA any](
// option.Some(3),
// }
// result2 := array.ArrayOption[int]()(opts2) // None
func ArrayOption[A any]() func([]O.Option[A]) O.Option[[]A] {
func ArrayOption[A any]() func([]Option[A]) Option[[]A] {
return Sequence(
O.Of[[]A],
O.MonadMap[[]A, func(A) []A],

View File

@@ -32,7 +32,7 @@ import (
// // Result: [1, 1, 2, 3, 4, 5, 6, 9]
//
//go:inline
func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
func Sort[T any](ord O.Ord[T]) Operator[T, T] {
return G.Sort[[]T](ord)
}
@@ -62,7 +62,7 @@ func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
// // Result: [{"Bob", 25}, {"Alice", 30}, {"Charlie", 35}]
//
//go:inline
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) Operator[T, T] {
return G.SortByKey[[]T](ord, f)
}
@@ -93,6 +93,6 @@ func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
// // Result: [{"Jones", "Bob"}, {"Smith", "Alice"}, {"Smith", "John"}]
//
//go:inline
func SortBy[T any](ord []O.Ord[T]) func(ma []T) []T {
func SortBy[T any](ord []O.Ord[T]) Operator[T, T] {
return G.SortBy[[]T](ord)
}

9
v2/array/types.go Normal file
View File

@@ -0,0 +1,9 @@
package array
import "github.com/IBM/fp-go/v2/option"
type (
Kleisli[A, B any] = func(A) []B
Operator[A, B any] = Kleisli[[]A, B]
Option[A any] = option.Option[A]
)

View File

@@ -46,6 +46,6 @@ func StrictUniq[A comparable](as []A) []A {
// // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}]
//
//go:inline
func Uniq[A any, K comparable](f func(A) K) func(as []A) []A {
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
return G.Uniq[[]A](f)
}

View File

@@ -15,14 +15,163 @@
package bytes
// Empty returns an empty byte slice.
//
// This function returns the identity element for the byte slice Monoid,
// which is an empty byte slice. It's useful as a starting point for
// building byte slices or as a default value.
//
// Returns:
// - An empty byte slice ([]byte{})
//
// Properties:
// - Empty() is the identity element for Monoid.Concat
// - Monoid.Concat(Empty(), x) == x
// - Monoid.Concat(x, Empty()) == x
//
// Example - Basic usage:
//
// empty := Empty()
// fmt.Println(len(empty)) // 0
//
// Example - As identity element:
//
// data := []byte("hello")
// result1 := Monoid.Concat(Empty(), data) // []byte("hello")
// result2 := Monoid.Concat(data, Empty()) // []byte("hello")
//
// Example - Building byte slices:
//
// // Start with empty and build up
// buffer := Empty()
// buffer = Monoid.Concat(buffer, []byte("Hello"))
// buffer = Monoid.Concat(buffer, []byte(" "))
// buffer = Monoid.Concat(buffer, []byte("World"))
// // buffer: []byte("Hello World")
//
// See also:
// - Monoid.Empty(): Alternative way to get empty byte slice
// - ConcatAll(): For concatenating multiple byte slices
func Empty() []byte {
return Monoid.Empty()
}
// ToString converts a byte slice to a string.
//
// This function performs a direct conversion from []byte to string.
// The conversion creates a new string with a copy of the byte data.
//
// Parameters:
// - a: The byte slice to convert
//
// Returns:
// - A string containing the same data as the byte slice
//
// Performance Note:
//
// This conversion allocates a new string. For performance-critical code
// that needs to avoid allocations, consider using unsafe.String (Go 1.20+)
// or working directly with byte slices.
//
// Example - Basic conversion:
//
// bytes := []byte("hello")
// str := ToString(bytes)
// fmt.Println(str) // "hello"
//
// Example - Converting binary data:
//
// // ASCII codes for "Hello"
// data := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
// str := ToString(data)
// fmt.Println(str) // "Hello"
//
// Example - Empty byte slice:
//
// empty := Empty()
// str := ToString(empty)
// fmt.Println(str == "") // true
//
// Example - UTF-8 encoded text:
//
// utf8Bytes := []byte("Hello, 世界")
// str := ToString(utf8Bytes)
// fmt.Println(str) // "Hello, 世界"
//
// Example - Round-trip conversion:
//
// original := "test string"
// bytes := []byte(original)
// result := ToString(bytes)
// fmt.Println(original == result) // true
//
// See also:
// - []byte(string): For converting string to byte slice
// - Size(): For getting the length of a byte slice
func ToString(a []byte) string {
return string(a)
}
// Size returns the number of bytes in a byte slice.
//
// This function returns the length of the byte slice, which is the number
// of bytes it contains. This is equivalent to len(as) but provided as a
// named function for use in functional composition.
//
// Parameters:
// - as: The byte slice to measure
//
// Returns:
// - The number of bytes in the slice
//
// Example - Basic usage:
//
// data := []byte("hello")
// size := Size(data)
// fmt.Println(size) // 5
//
// Example - Empty slice:
//
// empty := Empty()
// size := Size(empty)
// fmt.Println(size) // 0
//
// Example - Binary data:
//
// binary := []byte{0x01, 0x02, 0x03, 0x04}
// size := Size(binary)
// fmt.Println(size) // 4
//
// Example - UTF-8 encoded text:
//
// // Note: Size returns byte count, not character count
// utf8 := []byte("Hello, 世界")
// byteCount := Size(utf8)
// fmt.Println(byteCount) // 13 (not 9 characters)
//
// Example - Using in functional composition:
//
// import "github.com/IBM/fp-go/v2/array"
//
// slices := [][]byte{
// []byte("a"),
// []byte("bb"),
// []byte("ccc"),
// }
//
// // Map to get sizes
// sizes := array.Map(Size)(slices)
// // sizes: []int{1, 2, 3}
//
// Example - Checking if slice is empty:
//
// data := []byte("test")
// isEmpty := Size(data) == 0
// fmt.Println(isEmpty) // false
//
// See also:
// - len(): Built-in function for getting slice length
// - ToString(): For converting byte slice to string
func Size(as []byte) int {
return len(as)
}

View File

@@ -187,6 +187,299 @@ func TestOrd(t *testing.T) {
})
}
// TestOrdProperties tests mathematical properties of Ord
func TestOrdProperties(t *testing.T) {
t.Run("reflexivity: x == x", func(t *testing.T) {
testCases := [][]byte{
[]byte{},
[]byte("a"),
[]byte("test"),
[]byte{0x01, 0x02, 0x03},
}
for _, tc := range testCases {
assert.Equal(t, 0, Ord.Compare(tc, tc),
"Compare(%v, %v) should be 0", tc, tc)
assert.True(t, Ord.Equals(tc, tc),
"Equals(%v, %v) should be true", tc, tc)
}
})
t.Run("antisymmetry: if x <= y and y <= x then x == y", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abc")},
{[]byte{}, []byte{}},
{[]byte{0x01}, []byte{0x01}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
if cmp1 <= 0 && cmp2 <= 0 {
assert.True(t, Ord.Equals(tc.a, tc.b),
"If %v <= %v and %v <= %v, they should be equal", tc.a, tc.b, tc.b, tc.a)
}
}
})
t.Run("transitivity: if x <= y and y <= z then x <= z", func(t *testing.T) {
x := []byte("a")
y := []byte("b")
z := []byte("c")
cmpXY := Ord.Compare(x, y)
cmpYZ := Ord.Compare(y, z)
cmpXZ := Ord.Compare(x, z)
if cmpXY <= 0 && cmpYZ <= 0 {
assert.True(t, cmpXZ <= 0,
"If %v <= %v and %v <= %v, then %v <= %v", x, y, y, z, x, z)
}
})
t.Run("totality: either x <= y or y <= x", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abd")},
{[]byte("xyz"), []byte("abc")},
{[]byte{}, []byte("a")},
{[]byte{0x01}, []byte{0x02}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
assert.True(t, cmp1 <= 0 || cmp2 <= 0,
"Either %v <= %v or %v <= %v must be true", tc.a, tc.b, tc.b, tc.a)
}
})
}
// TestEdgeCases tests edge cases and boundary conditions
func TestEdgeCases(t *testing.T) {
t.Run("very large byte slices", func(t *testing.T) {
large := make([]byte, 1000000)
for i := range large {
large[i] = byte(i % 256)
}
size := Size(large)
assert.Equal(t, 1000000, size)
str := ToString(large)
assert.Equal(t, 1000000, len(str))
})
t.Run("concatenating many slices", func(t *testing.T) {
slices := make([][]byte, 100)
for i := range slices {
slices[i] = []byte{byte(i)}
}
result := ConcatAll(slices...)
assert.Equal(t, 100, Size(result))
})
t.Run("null bytes in slice", func(t *testing.T) {
data := []byte{0x00, 0x01, 0x00, 0x02}
size := Size(data)
assert.Equal(t, 4, size)
str := ToString(data)
assert.Equal(t, 4, len(str))
})
t.Run("comparing slices with null bytes", func(t *testing.T) {
a := []byte{0x00, 0x01}
b := []byte{0x00, 0x02}
assert.Equal(t, -1, Ord.Compare(a, b))
})
}
// TestMonoidConcatPerformance tests concatenation performance characteristics
func TestMonoidConcatPerformance(t *testing.T) {
t.Run("ConcatAll vs repeated Concat", func(t *testing.T) {
slices := [][]byte{
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
}
// Using ConcatAll
result1 := ConcatAll(slices...)
// Using repeated Concat
result2 := Monoid.Empty()
for _, s := range slices {
result2 = Monoid.Concat(result2, s)
}
assert.Equal(t, result1, result2)
assert.Equal(t, []byte("abcde"), result1)
})
}
// TestRoundTrip tests round-trip conversions
func TestRoundTrip(t *testing.T) {
t.Run("string to bytes to string", func(t *testing.T) {
original := "Hello, World! 世界"
bytes := []byte(original)
result := ToString(bytes)
assert.Equal(t, original, result)
})
t.Run("bytes to string to bytes", func(t *testing.T) {
original := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
str := ToString(original)
result := []byte(str)
assert.Equal(t, original, result)
})
}
// TestConcatAllVariadic tests ConcatAll with various argument counts
func TestConcatAllVariadic(t *testing.T) {
t.Run("zero arguments", func(t *testing.T) {
result := ConcatAll()
assert.Equal(t, []byte{}, result)
})
t.Run("one argument", func(t *testing.T) {
result := ConcatAll([]byte("test"))
assert.Equal(t, []byte("test"), result)
})
t.Run("two arguments", func(t *testing.T) {
result := ConcatAll([]byte("hello"), []byte("world"))
assert.Equal(t, []byte("helloworld"), result)
})
t.Run("many arguments", func(t *testing.T) {
result := ConcatAll(
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
[]byte("f"),
[]byte("g"),
[]byte("h"),
[]byte("i"),
[]byte("j"),
)
assert.Equal(t, []byte("abcdefghij"), result)
})
}
// Benchmark tests
func BenchmarkToString(b *testing.B) {
data := []byte("Hello, World!")
b.Run("small", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ToString(data)
}
})
b.Run("large", func(b *testing.B) {
large := make([]byte, 10000)
for i := range large {
large[i] = byte(i % 256)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToString(large)
}
})
}
func BenchmarkSize(b *testing.B) {
data := []byte("Hello, World!")
for i := 0; i < b.N; i++ {
_ = Size(data)
}
}
func BenchmarkMonoidConcat(b *testing.B) {
a := []byte("Hello")
c := []byte(" World")
b.Run("small slices", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Monoid.Concat(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Monoid.Concat(large1, large2)
}
})
}
func BenchmarkConcatAll(b *testing.B) {
slices := [][]byte{
[]byte("Hello"),
[]byte(" "),
[]byte("World"),
[]byte("!"),
}
b.Run("few slices", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ConcatAll(slices...)
}
})
b.Run("many slices", func(b *testing.B) {
many := make([][]byte, 100)
for i := range many {
many[i] = []byte{byte(i)}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ConcatAll(many...)
}
})
}
func BenchmarkOrdCompare(b *testing.B) {
a := []byte("abc")
c := []byte("abd")
b.Run("equal", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Ord.Compare(a, a)
}
})
b.Run("different", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Ord.Compare(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
large2[9999] = 1
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = Ord.Compare(large1, large2)
}
})
}
// Example tests
func ExampleEmpty() {
empty := Empty()
@@ -219,3 +512,17 @@ func ExampleConcatAll() {
// Output:
}
func ExampleMonoid_concat() {
result := Monoid.Concat([]byte("Hello"), []byte(" World"))
println(string(result)) // Hello World
// Output:
}
func ExampleOrd_compare() {
cmp := Ord.Compare([]byte("abc"), []byte("abd"))
println(cmp) // -1 (abc < abd)
// Output:
}

4
v2/bytes/coverage.out Normal file
View File

@@ -0,0 +1,4 @@
mode: set
github.com/IBM/fp-go/v2/bytes/bytes.go:55.21,57.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:111.32,113.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:175.26,177.2 1 1

View File

@@ -23,12 +23,219 @@ import (
)
var (
// monoid for byte arrays
// Monoid is the Monoid instance for byte slices.
//
// This Monoid combines byte slices through concatenation, with an empty
// byte slice as the identity element. It satisfies the monoid laws:
//
// Identity laws:
// - Monoid.Concat(Monoid.Empty(), x) == x (left identity)
// - Monoid.Concat(x, Monoid.Empty()) == x (right identity)
//
// Associativity law:
// - Monoid.Concat(Monoid.Concat(a, b), c) == Monoid.Concat(a, Monoid.Concat(b, c))
//
// Operations:
// - Empty(): Returns an empty byte slice []byte{}
// - Concat(a, b []byte): Concatenates two byte slices
//
// Example - Basic concatenation:
//
// result := Monoid.Concat([]byte("Hello"), []byte(" World"))
// // result: []byte("Hello World")
//
// Example - Identity element:
//
// empty := Monoid.Empty()
// data := []byte("test")
// result1 := Monoid.Concat(empty, data) // []byte("test")
// result2 := Monoid.Concat(data, empty) // []byte("test")
//
// Example - Building byte buffers:
//
// buffer := Monoid.Empty()
// buffer = Monoid.Concat(buffer, []byte("Line 1\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 2\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 3\n"))
//
// Example - Associativity:
//
// a := []byte("a")
// b := []byte("b")
// c := []byte("c")
// left := Monoid.Concat(Monoid.Concat(a, b), c) // []byte("abc")
// right := Monoid.Concat(a, Monoid.Concat(b, c)) // []byte("abc")
// // left == right
//
// See also:
// - ConcatAll: For concatenating multiple byte slices at once
// - Empty(): Convenience function for getting empty byte slice
Monoid = A.Monoid[byte]()
// ConcatAll concatenates all bytes
// ConcatAll efficiently concatenates multiple byte slices into a single slice.
//
// This function takes a variadic number of byte slices and combines them
// into a single byte slice. It pre-allocates the exact amount of memory
// needed, making it more efficient than repeated concatenation.
//
// Parameters:
// - slices: Zero or more byte slices to concatenate
//
// Returns:
// - A new byte slice containing all input slices concatenated in order
//
// Performance:
//
// ConcatAll is more efficient than using Monoid.Concat repeatedly because
// it calculates the total size upfront and allocates memory once, avoiding
// multiple allocations and copies.
//
// Example - Basic usage:
//
// result := ConcatAll(
// []byte("Hello"),
// []byte(" "),
// []byte("World"),
// )
// // result: []byte("Hello World")
//
// Example - Empty input:
//
// result := ConcatAll()
// // result: []byte{}
//
// Example - Single slice:
//
// result := ConcatAll([]byte("test"))
// // result: []byte("test")
//
// Example - Building protocol messages:
//
// import "encoding/binary"
//
// header := []byte{0x01, 0x02}
// length := make([]byte, 4)
// binary.BigEndian.PutUint32(length, 100)
// payload := []byte("data")
// footer := []byte{0xFF}
//
// message := ConcatAll(header, length, payload, footer)
//
// Example - With empty slices:
//
// result := ConcatAll(
// []byte("a"),
// []byte{},
// []byte("b"),
// []byte{},
// []byte("c"),
// )
// // result: []byte("abc")
//
// Example - Building CSV line:
//
// fields := [][]byte{
// []byte("John"),
// []byte("Doe"),
// []byte("30"),
// }
// separator := []byte(",")
//
// // Interleave fields with separators
// parts := [][]byte{
// fields[0], separator,
// fields[1], separator,
// fields[2],
// }
// line := ConcatAll(parts...)
// // line: []byte("John,Doe,30")
//
// See also:
// - Monoid.Concat: For concatenating exactly two byte slices
// - bytes.Join: Standard library function for joining with separator
ConcatAll = A.ArrayConcatAll[byte]
// Ord implements the default ordering on bytes
// Ord is the Ord instance for byte slices providing lexicographic ordering.
//
// This Ord instance compares byte slices lexicographically (dictionary order),
// comparing bytes from left to right until a difference is found or one slice
// ends. It uses the standard library's bytes.Compare and bytes.Equal functions.
//
// Comparison rules:
// - Compares byte-by-byte from left to right
// - First differing byte determines the order
// - Shorter slice is less than longer slice if all bytes match
// - Empty slice is less than any non-empty slice
//
// Operations:
// - Compare(a, b []byte) int: Returns -1 if a < b, 0 if a == b, 1 if a > b
// - Equals(a, b []byte) bool: Returns true if slices are equal
//
// Example - Basic comparison:
//
// cmp := Ord.Compare([]byte("abc"), []byte("abd"))
// // cmp: -1 (abc < abd)
//
// cmp = Ord.Compare([]byte("xyz"), []byte("abc"))
// // cmp: 1 (xyz > abc)
//
// cmp = Ord.Compare([]byte("test"), []byte("test"))
// // cmp: 0 (equal)
//
// Example - Length differences:
//
// cmp := Ord.Compare([]byte("ab"), []byte("abc"))
// // cmp: -1 (shorter is less)
//
// cmp = Ord.Compare([]byte("abc"), []byte("ab"))
// // cmp: 1 (longer is greater)
//
// Example - Empty slices:
//
// cmp := Ord.Compare([]byte{}, []byte("a"))
// // cmp: -1 (empty is less)
//
// cmp = Ord.Compare([]byte{}, []byte{})
// // cmp: 0 (both empty)
//
// Example - Equality check:
//
// equal := Ord.Equals([]byte("test"), []byte("test"))
// // equal: true
//
// equal = Ord.Equals([]byte("test"), []byte("Test"))
// // equal: false (case-sensitive)
//
// Example - Sorting byte slices:
//
// import "github.com/IBM/fp-go/v2/array"
//
// data := [][]byte{
// []byte("zebra"),
// []byte("apple"),
// []byte("mango"),
// }
//
// sorted := array.Sort(Ord)(data)
// // sorted: [[]byte("apple"), []byte("mango"), []byte("zebra")]
//
// Example - Binary data comparison:
//
// cmp := Ord.Compare([]byte{0x01, 0x02}, []byte{0x01, 0x03})
// // cmp: -1 (0x02 < 0x03)
//
// Example - Finding minimum:
//
// import O "github.com/IBM/fp-go/v2/ord"
//
// a := []byte("xyz")
// b := []byte("abc")
// min := O.Min(Ord)(a, b)
// // min: []byte("abc")
//
// See also:
// - bytes.Compare: Standard library comparison function
// - bytes.Equal: Standard library equality function
// - array.Sort: For sorting slices using an Ord instance
Ord = O.MakeOrd(bytes.Compare, bytes.Equal)
)

View File

@@ -53,17 +53,20 @@ var (
// structInfo holds information about a struct that needs lens generation
type structInfo struct {
Name string
Fields []fieldInfo
Imports map[string]string // package path -> alias
Name string
TypeParams string // e.g., "[T any]" or "[K comparable, V any]" - for type declarations
TypeParamNames string // e.g., "[T]" or "[K, V]" - for type usage in function signatures
Fields []fieldInfo
Imports map[string]string // package path -> alias
}
// fieldInfo holds information about a struct field
type fieldInfo struct {
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
IsComparable bool // true if the type is comparable (can use ==)
}
// templateData holds data for template rendering
@@ -74,64 +77,95 @@ type templateData struct {
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
type {{.Name}}Lenses struct {
type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}}
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
}
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
type {{.Name}}RefLenses struct {
type {{.Name}}RefLenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}}
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
}
`
const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
func Make{{.Name}}Lenses() {{.Name}}Lenses {
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
{{- if .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
lens{{.Name}} := L.MakeLens(
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
{{- end}}
return {{.Name}}Lenses{
return {{.Name}}Lenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
),
{{.Name}}: lens{{.Name}},
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O: lens{{.Name}}O,
{{- end}}
{{- end}}
}
}
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
{{- if .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
{{- end}}
{{- end}}
return {{.Name}}RefLenses{
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- if .IsComparable}}
lens{{.Name}} := L.MakeLensStrict(
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- else}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
),
lens{{.Name}} := L.MakeLensRef(
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- end}}
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
{{- end}}
return {{.Name}}RefLenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}}
{{.Name}}: lens{{.Name}},
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O: lens{{.Name}}O,
{{- end}}
{{- end}}
}
@@ -257,6 +291,259 @@ func isPointerType(expr ast.Expr) bool {
return ok
}
// isComparableType checks if a type expression represents a comparable type.
// Comparable types in Go include:
// - Basic types (bool, numeric types, string)
// - Pointer types
// - Channel types
// - Interface types
// - Structs where all fields are comparable
// - Arrays where the element type is comparable
//
// Non-comparable types include:
// - Slices
// - Maps
// - Functions
//
// typeParams is a map of type parameter names to their constraints (e.g., "T" -> "any", "K" -> "comparable")
func isComparableType(expr ast.Expr, typeParams map[string]string) bool {
switch t := expr.(type) {
case *ast.Ident:
// Check if this is a type parameter
if constraint, isTypeParam := typeParams[t.Name]; isTypeParam {
// Type parameter - check its constraint
return constraint == "comparable"
}
// Basic types and named types
// We assume named types are comparable unless they're known non-comparable types
name := t.Name
// Known non-comparable built-in types
if name == "error" {
// error is an interface, which is comparable
return true
}
// Most basic types and named types are comparable
// We can't determine if a custom type is comparable without type checking,
// so we assume it is (conservative approach)
return true
case *ast.StarExpr:
// Pointer types are always comparable
return true
case *ast.ArrayType:
// Arrays are comparable if their element type is comparable
if t.Len == nil {
// This is a slice (no length), slices are not comparable
return false
}
// Fixed-size array, check element type
return isComparableType(t.Elt, typeParams)
case *ast.MapType:
// Maps are not comparable
return false
case *ast.FuncType:
// Functions are not comparable
return false
case *ast.InterfaceType:
// Interface types are comparable
return true
case *ast.StructType:
// Structs are comparable if all fields are comparable
// We can't easily determine this without full type information,
// so we conservatively return false for struct literals
return false
case *ast.SelectorExpr:
// Qualified identifier (e.g., pkg.Type)
// We can't determine comparability without type information
// Check for known non-comparable types from standard library
if ident, ok := t.X.(*ast.Ident); ok {
pkgName := ident.Name
typeName := t.Sel.Name
// Check for known non-comparable types
if pkgName == "context" && typeName == "Context" {
// context.Context is an interface, which is comparable
return true
}
// For other qualified types, we assume they're comparable
// This is a conservative approach
}
return true
case *ast.IndexExpr, *ast.IndexListExpr:
// Generic types - we can't determine comparability without type information
// For common generic types, we can make educated guesses
var baseExpr ast.Expr
if idx, ok := t.(*ast.IndexExpr); ok {
baseExpr = idx.X
} else if idxList, ok := t.(*ast.IndexListExpr); ok {
baseExpr = idxList.X
}
if sel, ok := baseExpr.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok {
pkgName := ident.Name
typeName := sel.Sel.Name
// Check for known non-comparable generic types
if pkgName == "option" && typeName == "Option" {
// Option types are not comparable (they contain a slice internally)
return false
}
if pkgName == "either" && typeName == "Either" {
// Either types are not comparable
return false
}
}
}
// For other generic types, conservatively assume not comparable
log.Printf("Not comparable type: %v\n", t)
return false
case *ast.ChanType:
// Channel types are comparable
return true
default:
// Unknown type, conservatively assume not comparable
return false
}
}
// embeddedFieldResult holds both the field info and its AST type for import extraction
type embeddedFieldResult struct {
fieldInfo fieldInfo
fieldType ast.Expr
}
// extractEmbeddedFields extracts fields from an embedded struct type
// It returns a slice of embeddedFieldResult for all exported fields in the embedded struct
// typeParamsMap contains the type parameters of the parent struct (for checking comparability)
func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, file *ast.File, typeParamsMap map[string]string) []embeddedFieldResult {
var results []embeddedFieldResult
// Get the type name of the embedded field
var typeName string
var typeIdent *ast.Ident
switch t := embedType.(type) {
case *ast.Ident:
// Direct embedded type: type MyStruct struct { EmbeddedType }
typeName = t.Name
typeIdent = t
case *ast.StarExpr:
// Pointer embedded type: type MyStruct struct { *EmbeddedType }
if ident, ok := t.X.(*ast.Ident); ok {
typeName = ident.Name
typeIdent = ident
}
case *ast.SelectorExpr:
// Qualified embedded type: type MyStruct struct { pkg.EmbeddedType }
// We can't easily resolve this without full type information
// For now, skip these
return results
}
if typeName == "" || typeIdent == nil {
return results
}
// Find the struct definition in the same file
var embeddedStructType *ast.StructType
ast.Inspect(file, func(n ast.Node) bool {
if ts, ok := n.(*ast.TypeSpec); ok {
if ts.Name.Name == typeName {
if st, ok := ts.Type.(*ast.StructType); ok {
embeddedStructType = st
return false
}
}
}
return true
})
if embeddedStructType == nil {
// Struct not found in this file, might be from another package
return results
}
// Extract fields from the embedded struct
for _, field := range embeddedStructType.Fields.List {
// Skip embedded fields within embedded structs (for now, to avoid infinite recursion)
if len(field.Names) == 0 {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
isOptional := false
baseType := fieldTypeName
// Check if field is optional
if isPointerType(field.Type) {
isOptional = true
baseType = strings.TrimPrefix(fieldTypeName, "*")
} else if hasOmitEmpty(field.Tag) {
isOptional = true
}
// Check if the type is comparable
isComparable := isComparableType(field.Type, typeParamsMap)
results = append(results, embeddedFieldResult{
fieldInfo: fieldInfo{
Name: name.Name,
TypeName: fieldTypeName,
BaseType: baseType,
IsOptional: isOptional,
IsComparable: isComparable,
},
fieldType: field.Type,
})
}
}
}
return results
}
// extractTypeParams extracts type parameters from a type spec
// Returns two strings: full params like "[T any]" and names only like "[T]"
func extractTypeParams(typeSpec *ast.TypeSpec) (string, string) {
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
return "", ""
}
var params []string
var names []string
for _, field := range typeSpec.TypeParams.List {
for _, name := range field.Names {
constraint := getTypeName(field.Type)
params = append(params, name.Name+" "+constraint)
names = append(names, name.Name)
}
}
fullParams := "[" + strings.Join(params, ", ") + "]"
nameParams := "[" + strings.Join(names, ", ") + "]"
return fullParams, nameParams
}
// buildTypeParamsMap creates a map of type parameter names to their constraints
// e.g., for "type Box[T any, K comparable]", returns {"T": "any", "K": "comparable"}
func buildTypeParamsMap(typeSpec *ast.TypeSpec) map[string]string {
typeParamsMap := make(map[string]string)
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
return typeParamsMap
}
for _, field := range typeSpec.TypeParams.List {
constraint := getTypeName(field.Type)
for _, name := range field.Names {
typeParamsMap[name.Name] = constraint
}
}
return typeParamsMap
}
// parseFile parses a Go file and extracts structs with lens annotations
func parseFile(filename string) ([]structInfo, string, error) {
fset := token.NewFileSet()
@@ -320,9 +607,27 @@ func parseFile(filename string) ([]structInfo, string, error) {
var fields []fieldInfo
structImports := make(map[string]string)
// Build type parameters map for this struct
typeParamsMap := buildTypeParamsMap(typeSpec)
for _, field := range structType.Fields.List {
if len(field.Names) == 0 {
// Embedded field, skip for now
// Embedded field - promote its fields
embeddedResults := extractEmbeddedFields(field.Type, fileImports, node, typeParamsMap)
for _, embResult := range embeddedResults {
// Extract imports from embedded field's type
fieldImports := make(map[string]string)
extractImports(embResult.fieldType, fieldImports)
// Resolve package names to full import paths
for pkgName := range fieldImports {
if importPath, ok := fileImports[pkgName]; ok {
structImports[importPath] = pkgName
}
}
fields = append(fields, embResult.fieldInfo)
}
continue
}
for _, name := range field.Names {
@@ -331,6 +636,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
typeName := getTypeName(field.Type)
isOptional := false
baseType := typeName
isComparable := false
// Check if field is optional:
// 1. Pointer types are always optional
@@ -344,6 +650,11 @@ func parseFile(filename string) ([]structInfo, string, error) {
isOptional = true
}
// Check if the type is comparable (for non-optional fields)
// For optional fields, we don't need to check since they use LensO
isComparable = isComparableType(field.Type, typeParamsMap)
// log.Printf("field %s, type: %v, isComparable: %b\n", name, field.Type, isComparable)
// Extract imports from this field's type
fieldImports := make(map[string]string)
extractImports(field.Type, fieldImports)
@@ -356,20 +667,24 @@ func parseFile(filename string) ([]structInfo, string, error) {
}
fields = append(fields, fieldInfo{
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
IsComparable: isComparable,
})
}
}
}
if len(fields) > 0 {
typeParams, typeParamNames := extractTypeParams(typeSpec)
structs = append(structs, structInfo{
Name: typeSpec.Name.Name,
Fields: fields,
Imports: structImports,
Name: typeSpec.Name.Name,
TypeParams: typeParams,
TypeParamNames: typeParamNames,
Fields: fields,
Imports: structImports,
})
}
@@ -469,8 +784,8 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
// Standard fp-go imports always needed
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\tI \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// Add additional imports collected from field types
for importPath, alias := range allImports {

View File

@@ -168,6 +168,91 @@ func TestIsPointerType(t *testing.T) {
}
}
func TestIsComparableType(t *testing.T) {
tests := []struct {
name string
code string
expected bool
}{
{
name: "basic type - string",
code: "type T struct { F string }",
expected: true,
},
{
name: "basic type - int",
code: "type T struct { F int }",
expected: true,
},
{
name: "basic type - bool",
code: "type T struct { F bool }",
expected: true,
},
{
name: "pointer type",
code: "type T struct { F *string }",
expected: true,
},
{
name: "slice type - not comparable",
code: "type T struct { F []string }",
expected: false,
},
{
name: "map type - not comparable",
code: "type T struct { F map[string]int }",
expected: false,
},
{
name: "array type - comparable if element is",
code: "type T struct { F [5]int }",
expected: true,
},
{
name: "interface type",
code: "type T struct { F interface{} }",
expected: true,
},
{
name: "channel type",
code: "type T struct { F chan int }",
expected: true,
},
{
name: "function type - not comparable",
code: "type T struct { F func() }",
expected: false,
},
{
name: "struct literal - conservatively not comparable",
code: "type T struct { F struct{ X int } }",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := isComparableType(fieldType, map[string]string{})
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasOmitEmpty(t *testing.T) {
tests := []struct {
name string
@@ -337,6 +422,167 @@ type Config struct {
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
}
func TestParseFileWithComparableTypes(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type TypeTest struct {
Name string
Age int
Pointer *string
Slice []string
Map map[string]int
Channel chan int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check TypeTest struct
typeTest := structs[0]
assert.Equal(t, "TypeTest", typeTest.Name)
assert.Len(t, typeTest.Fields, 6)
// Name - string is comparable
assert.Equal(t, "Name", typeTest.Fields[0].Name)
assert.Equal(t, "string", typeTest.Fields[0].TypeName)
assert.False(t, typeTest.Fields[0].IsOptional)
assert.True(t, typeTest.Fields[0].IsComparable, "string should be comparable")
// Age - int is comparable
assert.Equal(t, "Age", typeTest.Fields[1].Name)
assert.Equal(t, "int", typeTest.Fields[1].TypeName)
assert.False(t, typeTest.Fields[1].IsOptional)
assert.True(t, typeTest.Fields[1].IsComparable, "int should be comparable")
// Pointer - pointer is optional, IsComparable not checked for optional fields
assert.Equal(t, "Pointer", typeTest.Fields[2].Name)
assert.Equal(t, "*string", typeTest.Fields[2].TypeName)
assert.True(t, typeTest.Fields[2].IsOptional)
// Slice - not comparable
assert.Equal(t, "Slice", typeTest.Fields[3].Name)
assert.Equal(t, "[]string", typeTest.Fields[3].TypeName)
assert.False(t, typeTest.Fields[3].IsOptional)
assert.False(t, typeTest.Fields[3].IsComparable, "slice should not be comparable")
// Map - not comparable
assert.Equal(t, "Map", typeTest.Fields[4].Name)
assert.Equal(t, "map[string]int", typeTest.Fields[4].TypeName)
assert.False(t, typeTest.Fields[4].IsOptional)
assert.False(t, typeTest.Fields[4].IsComparable, "map should not be comparable")
// Channel - comparable (note: getTypeName returns "any" for channel types, but isComparableType correctly identifies them)
assert.Equal(t, "Channel", typeTest.Fields[5].Name)
assert.Equal(t, "any", typeTest.Fields[5].TypeName) // getTypeName doesn't handle chan types specifically
assert.False(t, typeTest.Fields[5].IsOptional)
assert.True(t, typeTest.Fields[5].IsComparable, "channel should be comparable")
}
func TestLensRefTemplatesWithComparable(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Age", TypeName: "int", IsOptional: false, IsComparable: true},
{Name: "Data", TypeName: "[]byte", IsOptional: false, IsComparable: false},
{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: false},
},
}
// Test constructor template for RefLenses
var constructorBuf bytes.Buffer
err := constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
// Check that MakeLensStrict is used for comparable types in RefLenses
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
// Name field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(",
"comparable field Name should use MakeLensStrict in RefLenses")
// Age field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(",
"comparable field Age should use MakeLensStrict in RefLenses")
// Data field - not comparable, should use MakeLensRef
assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(",
"non-comparable field Data should use MakeLensRef in RefLenses")
}
func TestGenerateLensHelpersWithComparable(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type TestStruct struct {
Name string
Count int
Data []byte
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content in RefLenses
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
// Name and Count are comparable, should use MakeLensStrict
assert.Contains(t, contentStr, "L.MakeLensStrict",
"comparable fields should use MakeLensStrict in RefLenses")
// Data is not comparable (slice), should use MakeLensRef
assert.Contains(t, contentStr, "L.MakeLensRef",
"non-comparable fields should use MakeLensRef in RefLenses")
// Verify the pattern appears for Name field (comparable)
namePattern := "lensName := L.MakeLensStrict("
assert.Contains(t, contentStr, namePattern,
"Name field should use MakeLensStrict")
// Verify the pattern appears for Data field (not comparable)
dataPattern := "lensData := L.MakeLensRef("
assert.Contains(t, contentStr, dataPattern,
"Data field should use MakeLensRef")
}
func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
@@ -373,11 +619,11 @@ type TestStruct struct {
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLens")
assert.Contains(t, contentStr, "MakeTestStructLens")
assert.Contains(t, contentStr, "TestStructLenses")
assert.Contains(t, contentStr, "MakeTestStructLenses")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
assert.Contains(t, contentStr, "I.FromZero")
assert.Contains(t, contentStr, "IO.FromZero")
}
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
@@ -411,8 +657,8 @@ func TestLensTemplates(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "*int", IsOptional: true},
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "*int", IsOptional: true, IsComparable: true},
},
}
@@ -424,7 +670,9 @@ func TestLensTemplates(t *testing.T) {
structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]")
assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]")
// Test constructor template
var constructorBuf bytes.Buffer
@@ -434,19 +682,21 @@ func TestLensTemplates(t *testing.T) {
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
assert.Contains(t, constructorStr, "return TestStructLenses{")
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
assert.Contains(t, constructorStr, "I.FromZero")
assert.Contains(t, constructorStr, "Name: lensName,")
assert.Contains(t, constructorStr, "NameO: lensNameO,")
assert.Contains(t, constructorStr, "Value: lensValue,")
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
assert.Contains(t, constructorStr, "IO.FromZero")
}
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
s := structInfo{
Name: "ConfigStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "string", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: true}, // pointer
},
}
@@ -458,9 +708,13 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Pointer LO.LensO[ConfigStruct, *string]")
assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]")
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]")
assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]")
// Test constructor template
var constructorBuf bytes.Buffer
@@ -469,9 +723,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()")
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()")
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()")
assert.Contains(t, constructorStr, "IO.FromZero[string]()")
assert.Contains(t, constructorStr, "IO.FromZero[int]()")
assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
}
func TestLensCommandFlags(t *testing.T) {
@@ -480,7 +734,7 @@ func TestLensCommandFlags(t *testing.T) {
assert.Equal(t, "lens", cmd.Name)
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")
// Check flags
assert.Len(t, cmd.Flags, 3)
@@ -501,3 +755,330 @@ func TestLensCommandFlags(t *testing.T) {
assert.True(t, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag")
}
func TestParseFileWithEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Base struct {
ID int
Name string
}
// fp-go:Lens
type Extended struct {
Base
Extra string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Extended struct
extended := structs[0]
assert.Equal(t, "Extended", extended.Name)
assert.Len(t, extended.Fields, 3, "Should have 3 fields: ID, Name (from Base), and Extra")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range extended.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["ID"], "Should have promoted ID field from Base")
assert.True(t, fieldNames["Name"], "Should have promoted Name field from Base")
assert.True(t, fieldNames["Extra"], "Should have Extra field")
}
func TestGenerateLensHelpersWithEmbeddedStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// Base struct to be embedded
type Address struct {
Street string
City string
}
// fp-go:Lens
type Person struct {
Address
Name string
Age int
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "PersonLenses")
assert.Contains(t, contentStr, "MakePersonLenses")
// Check that embedded fields are included
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")
// Check that optional lenses are also generated for embedded fields
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
}
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Metadata struct {
CreatedAt string
UpdatedAt string
}
// fp-go:Lens
type Document struct {
*Metadata
Title string
Content string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Document struct
doc := structs[0]
assert.Equal(t, "Document", doc.Name)
assert.Len(t, doc.Fields, 4, "Should have 4 fields: CreatedAt, UpdatedAt (from *Metadata), Title, and Content")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range doc.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["CreatedAt"], "Should have promoted CreatedAt field from *Metadata")
assert.True(t, fieldNames["UpdatedAt"], "Should have promoted UpdatedAt field from *Metadata")
assert.True(t, fieldNames["Title"], "Should have Title field")
assert.True(t, fieldNames["Content"], "Should have Content field")
}
func TestParseFileWithGenericStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Container[T any] struct {
Value T
Count int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Container struct
container := structs[0]
assert.Equal(t, "Container", container.Name)
assert.Equal(t, "[T any]", container.TypeParams, "Should have type parameter [T any]")
assert.Len(t, container.Fields, 2)
assert.Equal(t, "Value", container.Fields[0].Name)
assert.Equal(t, "T", container.Fields[0].TypeName)
assert.Equal(t, "Count", container.Fields[1].Name)
assert.Equal(t, "int", container.Fields[1].TypeName)
}
func TestParseFileWithMultipleTypeParams(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Pair[K comparable, V any] struct {
Key K
Value V
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Pair struct
pair := structs[0]
assert.Equal(t, "Pair", pair.Name)
assert.Equal(t, "[K comparable, V any]", pair.TypeParams, "Should have type parameters [K comparable, V any]")
assert.Len(t, pair.Fields, 2)
assert.Equal(t, "Key", pair.Fields[0].Name)
assert.Equal(t, "K", pair.Fields[0].TypeName)
assert.Equal(t, "Value", pair.Fields[1].Name)
assert.Equal(t, "V", pair.Fields[1].TypeName)
}
func TestGenerateLensHelpersWithGenericStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type Box[T any] struct {
Content T
Label string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type BoxLenses[T any] struct", "Should have generic BoxLenses type")
assert.Contains(t, contentStr, "type BoxRefLenses[T any] struct", "Should have generic BoxRefLenses type")
assert.Contains(t, contentStr, "func MakeBoxLenses[T any]() BoxLenses[T]", "Should have generic constructor")
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
// Check that fields use the generic type parameter
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")
// Check optional lenses - only for comparable types
// T any is not comparable, so ContentO should NOT be generated
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
// string is comparable, so LabelO should be generated
assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens")
}
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type ComparableBox[T comparable] struct {
Key T
Value string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type ComparableBoxLenses[T comparable] struct", "Should have generic ComparableBoxLenses type")
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict")
// Check that Value field (string, always comparable) also uses MakeLensStrict
assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict")
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable")
}

11
v2/constant/monoid.go Normal file
View File

@@ -0,0 +1,11 @@
package constant
import (
"github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a [M.Monoid] that returns a constant value in all operations
func Monoid[A any](a A) M.Monoid[A] {
return M.MakeMonoid(function.Constant2[A, A](a), a)
}

View File

@@ -53,12 +53,12 @@ import (
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
RIOEH "github.com/IBM/fp-go/v2/context/readerioresult/http"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/http/builder"
H "github.com/IBM/fp-go/v2/http/headers"
LZ "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
// Requester converts an http/builder.Builder into a ReaderIOResult that produces HTTP requests.
@@ -143,10 +143,10 @@ func Requester(builder *R.Builder) RIOEH.Requester {
return F.Pipe5(
builder.GetBody(),
O.Fold(LZ.Of(E.Of[error](withoutBody)), E.Map[error](withBody)),
E.Ap[func(string) RIOE.ReaderIOResult[*http.Request]](builder.GetTargetURL()),
E.Flap[error, RIOE.ReaderIOResult[*http.Request]](builder.GetMethod()),
E.GetOrElse(RIOE.Left[*http.Request]),
O.Fold(LZ.Of(result.Of(withoutBody)), result.Map(withBody)),
result.Ap[RIOE.Kleisli[string, *http.Request]](builder.GetTargetURL()),
result.Flap[RIOE.ReaderIOResult[*http.Request]](builder.GetMethod()),
result.GetOrElse(RIOE.Left[*http.Request]),
RIOE.Map(func(req *http.Request) *http.Request {
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
return req

View File

@@ -180,6 +180,11 @@ func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIORe
return RIOR.MonadChainFirst(ma, f)
}
//go:inline
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTap(ma, f)
}
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
// This is the curried version of [MonadChainFirst].
//
@@ -193,6 +198,11 @@ func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirst(f)
}
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.Tap(f)
}
// Of creates a [ReaderIOResult] that always succeeds with the given value.
// This is the same as [Right] and represents the monadic return operation.
//
@@ -403,6 +413,11 @@ func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B])
return RIOR.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
return RIOR.MonadTapEitherK(ma, f)
}
// ChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
// This is the curried version of [MonadChainFirstEitherK].
//
@@ -416,6 +431,11 @@ func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.ChainFirstEitherK[context.Context](f)
}
//go:inline
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.TapEitherK[context.Context](f)
}
// ChainOptionK chains a function that returns an [Option] into a [ReaderIOResult] computation.
// If the Option is None, the provided error function is called.
//
@@ -538,6 +558,11 @@ func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderI
return RIOR.MonadChainFirstIOK(ma, f)
}
//go:inline
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
return RIOR.MonadTapIOK(ma, f)
}
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// This is the curried version of [MonadChainFirstIOK].
//
@@ -551,6 +576,11 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.ChainFirstIOK[context.Context](f)
}
//go:inline
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.TapIOK[context.Context](f)
}
// ChainIOEitherK chains a function that returns an [IOResult] into a [ReaderIOResult] computation.
// This is useful for integrating IOResult-returning functions into ReaderIOResult workflows.
//
@@ -782,11 +812,21 @@ func MonadChainFirstReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[con
return RIOR.MonadChainFirstReaderK(ma, f)
}
//go:inline
func MonadTapReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderK(ma, f)
}
//go:inline
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderK(f)
}
//go:inline
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderK(f)
}
//go:inline
func MonadChainReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderResultK(ma, f)
@@ -802,11 +842,21 @@ func MonadChainFirstReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult
return RIOR.MonadChainFirstReaderResultK(ma, f)
}
//go:inline
func MonadTapReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderResultK(ma, f)
}
//go:inline
func ChainFirstReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderResultK(f)
}
//go:inline
func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapReaderResultK(f)
}
//go:inline
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderIOK(ma, f)
@@ -822,11 +872,21 @@ func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli
return RIOR.MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(f)
}
//go:inline
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderIOK(f)
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
@@ -837,7 +897,64 @@ func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kl
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type. If the input is a Right value, it passes through unchanged.
//
//go:inline
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
return RIOR.MonadChainLeft(fa, f)
}
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
return RIOR.ChainLeft(f)
}
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
// but always returns the original Left error regardless of what f returns (Left or Right).
// If the input is a Right value, it passes through unchanged without calling f.
//
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
//
//go:inline
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstLeft(ma, f)
}
//go:inline
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadTapLeft(ma, f)
}
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
//
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
// ensuring the error path is preserved.
//
//go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.ChainFirstLeft[A](f)
}
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](f)
}

View File

@@ -284,3 +284,160 @@ func TestWithResourceErrorInRelease(t *testing.T) {
assert.Equal(t, 0, countRelease)
assert.Equal(t, E.Left[int](err), res)
}
func TestMonadChainFirstLeft(t *testing.T) {
ctx := context.Background()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
sideEffectCalled := false
originalErr := fmt.Errorf("original error")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
sideEffectCalled = true
return Left[int](fmt.Errorf("new error")) // This error is ignored
},
)
actualResult := result(ctx)()
assert.True(t, sideEffectCalled)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var capturedError error
originalErr := fmt.Errorf("validation failed")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
capturedError = e
return Right[int](999) // This Right value is ignored
},
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, capturedError)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
sideEffectCalled := false
result := MonadChainFirstLeft(
Right[int](42),
func(e error) ReaderIOResult[int] {
sideEffectCalled = true
return Left[int](fmt.Errorf("should not be called"))
},
)
assert.False(t, sideEffectCalled)
assert.Equal(t, E.Right[error](42), result(ctx)())
})
// Test that side effects are executed but original error is always preserved
t.Run("Side effects executed but original error preserved", func(t *testing.T) {
effectCount := 0
originalErr := fmt.Errorf("original error")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
effectCount++
// Try to return Right, but original Left should still be returned
return Right[int](999)
},
)
actualResult := result(ctx)()
assert.Equal(t, 1, effectCount)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
}
func TestChainFirstLeft(t *testing.T) {
ctx := context.Background()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
var captured error
originalErr := fmt.Errorf("test error")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
captured = e
return Left[int](fmt.Errorf("ignored error"))
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, captured)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var captured error
originalErr := fmt.Errorf("test error")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
captured = e
return Right[int](42) // This Right is ignored
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, captured)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
called := false
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
called = true
return Right[int](0)
})
result := F.Pipe1(
Right[int](100),
chainFn,
)
assert.False(t, called)
assert.Equal(t, E.Right[error](100), result(ctx)())
})
// Test that original error is always preserved regardless of what f returns
t.Run("Original error always preserved", func(t *testing.T) {
originalErr := fmt.Errorf("original")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
// Try to return Right, but original Left should still be returned
return Right[int](999)
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
assert.Equal(t, E.Left[int](originalErr), result(ctx)())
})
// Test logging with Left preservation
t.Run("Logging with Left preservation", func(t *testing.T) {
errorLog := []string{}
originalErr := fmt.Errorf("step1")
logError := ChainFirstLeft[string](func(e error) ReaderIOResult[string] {
errorLog = append(errorLog, "Logged: "+e.Error())
return Left[string](fmt.Errorf("log entry")) // This is ignored
})
result := F.Pipe2(
Left[string](originalErr),
logError,
ChainLeft(func(e error) ReaderIOResult[string] {
return Left[string](fmt.Errorf("step2"))
}),
)
actualResult := result(ctx)()
assert.Equal(t, []string{"Logged: step1"}, errorLog)
assert.Equal(t, E.Left[string](fmt.Errorf("step2")), actualResult)
})
}

View File

@@ -304,3 +304,85 @@ func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
func Chain[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChain, f)
}
// Flatten collapses a nested endomorphism into a single endomorphism.
//
// Given an endomorphism that transforms endomorphisms (Endomorphism[Endomorphism[A]]),
// Flatten produces a simple endomorphism by applying the outer transformation to the
// identity function. This is the monadic join operation for the Endomorphism monad.
//
// The function applies the nested endomorphism to Identity[A] to extract the inner
// endomorphism, effectively "flattening" the two layers into one.
//
// Type Parameters:
// - A: The type being transformed by the endomorphisms
//
// Parameters:
// - mma: A nested endomorphism that transforms endomorphisms
//
// Returns:
// - An endomorphism that applies the transformation directly to values of type A
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// // An endomorphism that wraps another endomorphism
// addThenDouble := func(endo Endomorphism[Counter]) Endomorphism[Counter] {
// return func(c Counter) Counter {
// c = endo(c) // Apply the input endomorphism
// c.Value = c.Value * 2 // Then double
// return c
// }
// }
//
// flattened := Flatten(addThenDouble)
// result := flattened(Counter{Value: 5}) // Counter{Value: 10}
func Flatten[A any](mma Endomorphism[Endomorphism[A]]) Endomorphism[A] {
return mma(function.Identity[A])
}
// Join performs self-application of a function that produces endomorphisms.
//
// Given a function that takes a value and returns an endomorphism of that same type,
// Join creates an endomorphism that applies the value to itself through the function.
// This operation is also known as the W combinator (warbler) in combinatory logic,
// or diagonal application.
//
// The resulting endomorphism evaluates f(a)(a), applying the same value a to both
// the function f and the resulting endomorphism.
//
// Type Parameters:
// - A: The type being transformed
//
// Parameters:
// - f: A function that takes a value and returns an endomorphism of that type
//
// Returns:
// - An endomorphism that performs self-application: f(a)(a)
//
// Example:
//
// type Point struct {
// X, Y int
// }
//
// // Create an endomorphism based on the input point
// scaleBy := func(p Point) Endomorphism[Point] {
// return func(p2 Point) Point {
// return Point{
// X: p2.X * p.X,
// Y: p2.Y * p.Y,
// }
// }
// }
//
// selfScale := Join(scaleBy)
// result := selfScale(Point{X: 3, Y: 4}) // Point{X: 9, Y: 16}
func Join[A any](f Kleisli[A]) Endomorphism[A] {
return func(a A) A {
return f(a)(a)
}
}

10
v2/endomorphism/from.go Normal file
View File

@@ -0,0 +1,10 @@
package endomorphism
import (
"github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
func FromSemigroup[A any](s S.Semigroup[A]) Kleisli[A] {
return function.Bind2of2(s.Concat)
}

View File

@@ -15,7 +15,84 @@
package eq
// Contramap implements an Equals predicate based on a mapping
// Contramap creates an Eq[B] from an Eq[A] by providing a function that maps B to A.
// This is a contravariant functor operation that allows you to transform equality predicates
// by mapping the input type. It's particularly useful for comparing complex types by
// extracting comparable fields.
//
// The name "contramap" comes from category theory, where it represents a contravariant
// functor. Unlike regular map (covariant), which transforms the output, contramap
// transforms the input in the opposite direction.
//
// Type Parameters:
// - A: The type that has an existing Eq instance
// - B: The type for which we want to create an Eq instance
//
// Parameters:
// - f: A function that extracts or converts a value of type B to type A
//
// Returns:
// - A function that takes an Eq[A] and returns an Eq[B]
//
// The resulting Eq[B] compares two B values by:
// 1. Applying f to both values to get A values
// 2. Using the original Eq[A] to compare those A values
//
// Example - Compare structs by a single field:
//
// type Person struct {
// ID int
// Name string
// Age int
// }
//
// // Compare persons by ID only
// personEqByID := eq.Contramap(func(p Person) int {
// return p.ID
// })(eq.FromStrictEquals[int]())
//
// p1 := Person{ID: 1, Name: "Alice", Age: 30}
// p2 := Person{ID: 1, Name: "Bob", Age: 25}
// assert.True(t, personEqByID.Equals(p1, p2)) // Same ID, different names
//
// Example - Case-insensitive string comparison:
//
// type User struct {
// Username string
// Email string
// }
//
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
//
// userEqByUsername := eq.Contramap(func(u User) string {
// return u.Username
// })(caseInsensitiveEq)
//
// u1 := User{Username: "Alice", Email: "alice@example.com"}
// u2 := User{Username: "ALICE", Email: "different@example.com"}
// assert.True(t, userEqByUsername.Equals(u1, u2)) // Case-insensitive match
//
// Example - Nested field access:
//
// type Address struct {
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Compare persons by city
// personEqByCity := eq.Contramap(func(p Person) string {
// return p.Address.City
// })(eq.FromStrictEquals[string]())
//
// Contramap Law:
// Contramap must satisfy: Contramap(f)(Contramap(g)(eq)) = Contramap(g ∘ f)(eq)
// This means contramapping twice is the same as contramapping with the composed function.
func Contramap[A, B any](f func(b B) A) func(Eq[A]) Eq[B] {
return func(fa Eq[A]) Eq[B] {
equals := fa.Equals

View File

@@ -19,38 +19,188 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// Eq represents an equality type class for type T.
// It provides a way to define custom equality semantics for any type,
// not just those that are comparable with Go's == operator.
//
// Type Parameters:
// - T: The type for which equality is defined
//
// Methods:
// - Equals(x, y T) bool: Returns true if x and y are considered equal
//
// Laws:
// An Eq instance must satisfy the equivalence relation laws:
// 1. Reflexivity: Equals(x, x) = true for all x
// 2. Symmetry: Equals(x, y) = Equals(y, x) for all x, y
// 3. Transitivity: If Equals(x, y) and Equals(y, z), then Equals(x, z)
//
// Example:
//
// // Create an equality predicate for integers
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// // Create a custom equality predicate
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
type Eq[T any] interface {
// Equals returns true if x and y are considered equal according to this equality predicate.
//
// Parameters:
// - x: The first value to compare
// - y: The second value to compare
//
// Returns:
// - true if x and y are equal, false otherwise
Equals(x, y T) bool
}
// eq is the internal implementation of the Eq interface.
// It wraps a comparison function to provide the Eq interface.
type eq[T any] struct {
c func(x, y T) bool
}
// Equals implements the Eq interface by delegating to the wrapped comparison function.
func (e eq[T]) Equals(x, y T) bool {
return e.c(x, y)
}
// strictEq is a helper function that uses Go's built-in == operator for comparison.
// It can only be used with comparable types.
func strictEq[A comparable](a, b A) bool {
return a == b
}
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
// FromStrictEquals constructs an Eq instance using Go's built-in == operator.
// This is the most common way to create an Eq for types that support ==.
//
// Type Parameters:
// - T: Must be a comparable type (supports ==)
//
// Returns:
// - An Eq[T] that uses == for equality comparison
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// stringEq := eq.FromStrictEquals[string]()
// assert.True(t, stringEq.Equals("hello", "hello"))
// assert.False(t, stringEq.Equals("hello", "world"))
//
// Note: For types that are not comparable or require custom equality logic,
// use FromEquals instead.
func FromStrictEquals[T comparable]() Eq[T] {
return FromEquals(strictEq[T])
}
// FromEquals constructs an [EQ.Eq] from the comparison function
// FromEquals constructs an Eq instance from a custom comparison function.
// This allows defining equality for any type, including non-comparable types
// or types that need custom equality semantics.
//
// Type Parameters:
// - T: The type for which equality is being defined (can be any type)
//
// Parameters:
// - c: A function that takes two values of type T and returns true if they are equal
//
// Returns:
// - An Eq[T] that uses the provided function for equality comparison
//
// Example:
//
// // Case-insensitive string equality
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
//
// // Approximate float equality
// approxEq := eq.FromEquals(func(a, b float64) bool {
// return math.Abs(a-b) < 0.0001
// })
// assert.True(t, approxEq.Equals(1.0, 1.00009))
//
// // Custom struct equality (compare by specific fields)
// type Person struct { ID int; Name string }
// personEq := eq.FromEquals(func(a, b Person) bool {
// return a.ID == b.ID // Compare only by ID
// })
//
// Note: The provided function should satisfy the equivalence relation laws
// (reflexivity, symmetry, transitivity) for correct behavior.
func FromEquals[T any](c func(x, y T) bool) Eq[T] {
return eq[T]{c: c}
}
// Empty returns the equals predicate that is always true
// Empty returns an Eq instance that always returns true for any comparison.
// This is the identity element for the Eq Monoid and is useful when you need
// an equality predicate that accepts everything.
//
// Type Parameters:
// - T: The type for which the always-true equality is defined
//
// Returns:
// - An Eq[T] where Equals(x, y) always returns true
//
// Example:
//
// alwaysTrue := eq.Empty[int]()
// assert.True(t, alwaysTrue.Equals(1, 2))
// assert.True(t, alwaysTrue.Equals(42, 100))
//
// // Useful as identity in monoid operations
// monoid := eq.Monoid[string]()
// combined := monoid.Concat(eq.FromStrictEquals[string](), monoid.Empty())
// // combined behaves the same as FromStrictEquals
//
// Use cases:
// - As the identity element in Monoid operations
// - When you need a placeholder equality that accepts everything
// - In generic code that requires an Eq but doesn't need actual comparison
func Empty[T any]() Eq[T] {
return FromEquals(F.Constant2[T, T](true))
}
// Equals returns a predicate to test if one value equals the other under an equals predicate
// Equals returns a curried equality checking function.
// This is useful for partial application and functional composition.
//
// Type Parameters:
// - T: The type being compared
//
// Parameters:
// - eq: The Eq instance to use for comparison
//
// Returns:
// - A function that takes a value and returns another function that checks equality with that value
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// equals42 := eq.Equals(intEq)(42)
//
// assert.True(t, equals42(42))
// assert.False(t, equals42(43))
//
// // Use in higher-order functions
// numbers := []int{40, 41, 42, 43, 44}
// filtered := array.Filter(equals42)(numbers)
// // filtered = [42]
//
// // Partial application
// equalsFunc := eq.Equals(intEq)
// equals10 := equalsFunc(10)
// equals20 := equalsFunc(20)
//
// This is particularly useful when working with functional programming patterns
// like map, filter, and other higher-order functions.
func Equals[T any](eq Eq[T]) func(T) func(T) bool {
return func(other T) func(T) bool {
return F.Bind2nd(eq.Equals, other)

View File

@@ -20,6 +20,65 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Semigroup returns a Semigroup instance for Eq[A].
// A Semigroup provides a way to combine two values of the same type.
// For Eq, the combination uses logical AND - two values are equal only if
// they are equal according to BOTH equality predicates.
//
// Type Parameters:
// - A: The type for which equality predicates are being combined
//
// Returns:
// - A Semigroup[Eq[A]] that combines equality predicates with logical AND
//
// The Concat operation satisfies:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Example - Combine multiple equality checks:
//
// type User struct {
// Username string
// Email string
// }
//
// usernameEq := eq.Contramap(func(u User) string {
// return u.Username
// })(eq.FromStrictEquals[string]())
//
// emailEq := eq.Contramap(func(u User) string {
// return u.Email
// })(eq.FromStrictEquals[string]())
//
// // Users are equal only if BOTH username AND email match
// userEq := eq.Semigroup[User]().Concat(usernameEq, emailEq)
//
// u1 := User{Username: "alice", Email: "alice@example.com"}
// u2 := User{Username: "alice", Email: "alice@example.com"}
// u3 := User{Username: "alice", Email: "different@example.com"}
//
// assert.True(t, userEq.Equals(u1, u2)) // Both match
// assert.False(t, userEq.Equals(u1, u3)) // Email differs
//
// Example - Combine multiple field checks:
//
// type Product struct {
// ID int
// Name string
// Price float64
// }
//
// idEq := eq.Contramap(func(p Product) int { return p.ID })(eq.FromStrictEquals[int]())
// nameEq := eq.Contramap(func(p Product) string { return p.Name })(eq.FromStrictEquals[string]())
// priceEq := eq.Contramap(func(p Product) float64 { return p.Price })(eq.FromStrictEquals[float64]())
//
// sg := eq.Semigroup[Product]()
// // All three fields must match
// productEq := sg.Concat(sg.Concat(idEq, nameEq), priceEq)
//
// Use cases:
// - Combining multiple field comparisons for struct equality
// - Building complex equality predicates from simpler ones
// - Ensuring all conditions are met (logical AND of predicates)
func Semigroup[A any]() S.Semigroup[Eq[A]] {
return S.MakeSemigroup(func(x, y Eq[A]) Eq[A] {
return FromEquals(func(a, b A) bool {
@@ -28,6 +87,67 @@ func Semigroup[A any]() S.Semigroup[Eq[A]] {
})
}
// Monoid returns a Monoid instance for Eq[A].
// A Monoid extends Semigroup with an identity element (Empty).
// For Eq, the identity is an equality predicate that always returns true.
//
// Type Parameters:
// - A: The type for which the equality monoid is defined
//
// Returns:
// - A Monoid[Eq[A]] with:
// - Concat: Combines equality predicates with logical AND (from Semigroup)
// - Empty: An equality predicate that always returns true (identity element)
//
// Monoid Laws:
// 1. Left Identity: Concat(Empty(), x) = x
// 2. Right Identity: Concat(x, Empty()) = x
// 3. Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Example - Using the identity element:
//
// monoid := eq.Monoid[int]()
// intEq := eq.FromStrictEquals[int]()
//
// // Empty is the identity - combining with it doesn't change behavior
// leftIdentity := monoid.Concat(monoid.Empty(), intEq)
// rightIdentity := monoid.Concat(intEq, monoid.Empty())
//
// assert.True(t, leftIdentity.Equals(42, 42))
// assert.False(t, leftIdentity.Equals(42, 43))
// assert.True(t, rightIdentity.Equals(42, 42))
// assert.False(t, rightIdentity.Equals(42, 43))
//
// Example - Empty always returns true:
//
// monoid := eq.Monoid[string]()
// alwaysTrue := monoid.Empty()
//
// assert.True(t, alwaysTrue.Equals("hello", "world"))
// assert.True(t, alwaysTrue.Equals("same", "same"))
// assert.True(t, alwaysTrue.Equals("", "anything"))
//
// Example - Building complex equality with fold:
//
// type Person struct {
// FirstName string
// LastName string
// Age int
// }
//
// firstNameEq := eq.Contramap(func(p Person) string { return p.FirstName })(eq.FromStrictEquals[string]())
// lastNameEq := eq.Contramap(func(p Person) string { return p.LastName })(eq.FromStrictEquals[string]())
// ageEq := eq.Contramap(func(p Person) int { return p.Age })(eq.FromStrictEquals[int]())
//
// monoid := eq.Monoid[Person]()
// // Combine all predicates - all fields must match
// personEq := monoid.Concat(monoid.Concat(firstNameEq, lastNameEq), ageEq)
//
// Use cases:
// - Providing a neutral element for equality combinations
// - Generic algorithms that require a Monoid instance
// - Folding multiple equality predicates into one
// - Default "accept everything" equality predicate
func Monoid[A any]() M.Monoid[Eq[A]] {
return M.MakeMonoid(Semigroup[A]().Concat, Empty[A]())
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,105 @@
package function
// Flip reverses the order of parameters of a curried function
// Flip reverses the order of parameters of a curried function.
//
// Given a curried function f that takes T1 then T2 and returns R,
// Flip returns a new curried function that takes T2 then T1 and returns R.
// This is useful when you have a curried function but need to apply its
// arguments in a different order.
//
// Mathematical notation:
// - Given: f: T1 → T2 → R
// - Returns: g: T2 → T1 → R where g(t2)(t1) = f(t1)(t2)
//
// Type Parameters:
// - T1: The type of the first parameter (becomes second after flip)
// - T2: The type of the second parameter (becomes first after flip)
// - R: The return type
//
// Parameters:
// - f: A curried function taking T1 then T2 and returning R
//
// Returns:
// - A new curried function taking T2 then T1 and returning R
//
// Relationship to Swap:
//
// Flip is the curried version of Swap. While Swap works with binary functions,
// Flip works with curried functions:
// - Swap: func(T1, T2) R → func(T2, T1) R
// - Flip: func(T1) func(T2) R → func(T2) func(T1) R
//
// Example - Basic usage:
//
// // Create a curried division function
// divide := Curry2(func(a, b float64) float64 { return a / b })
// // divide(10)(2) = 5.0 (10 / 2)
//
// // Flip the parameter order
// divideFlipped := Flip(divide)
// // divideFlipped(10)(2) = 0.2 (2 / 10)
//
// Example - String formatting:
//
// // Curried string formatter: format(template)(value)
// format := Curry2(func(template, value string) string {
// return fmt.Sprintf(template, value)
// })
//
// // Normal order: template first, then value
// result1 := format("Hello, %s!")("World") // "Hello, World!"
//
// // Flipped order: value first, then template
// formatFlipped := Flip(format)
// result2 := formatFlipped("Hello, %s!")("World") // "Hello, World!"
//
// // Useful for partial application in different order
// greetWorld := format("Hello, %s!")
// greetWorld("Alice") // "Hello, Alice!"
//
// formatAlice := formatFlipped("Alice")
// formatAlice("Hello, %s!") // "Hello, Alice!"
//
// Example - Practical use case with map operations:
//
// // Curried map lookup: getFrom(map)(key)
// getFrom := Curry2(func(m map[string]int, key string) int {
// return m[key]
// })
//
// data := map[string]int{"a": 1, "b": 2, "c": 3}
//
// // Create a getter for this specific map
// getValue := getFrom(data)
// getValue("a") // 1
//
// // Flip to create key-first version: get(key)(map)
// get := Flip(getFrom)
// getA := get("a")
// getA(data) // 1
//
// Example - Combining with other functional patterns:
//
// // Curried append: append(slice)(element)
// appendTo := Curry2(func(slice []int, elem int) []int {
// return append(slice, elem)
// })
//
// // Flip to get: prepend(element)(slice)
// prepend := Flip(appendTo)
//
// nums := []int{1, 2, 3}
// add4 := appendTo(nums)
// result1 := add4(4) // [1, 2, 3, 4]
//
// prependZero := prepend(0)
// result2 := prependZero(nums) // [1, 2, 3, 0]
//
// See also:
// - Swap: For flipping parameters of non-curried binary functions
// - Curry2: For converting binary functions to curried form
// - Uncurry2: For converting curried functions back to binary form
func Flip[T1, T2, R any](f func(T1) func(T2) R) func(T2) func(T1) R {
return func(t2 T2) func(T1) R {
return func(t1 T1) R {

View File

@@ -22,15 +22,265 @@ import (
"github.com/stretchr/testify/assert"
)
// TestFlip tests the Flip function with various scenarios
func TestFlip(t *testing.T) {
t.Run("flips string concatenation", func(t *testing.T) {
// Create a curried function that formats strings
format := Curry2(func(a, b string) string {
return fmt.Sprintf("%s:%s", a, b)
})
x := Curry2(func(a, b string) string {
return fmt.Sprintf("%s:%s", a, b)
// Original order: a then b
assert.Equal(t, "a:b", format("a")("b"))
assert.Equal(t, "hello:world", format("hello")("world"))
// Flipped order: b then a
flipped := Flip(format)
assert.Equal(t, "b:a", flipped("a")("b"))
assert.Equal(t, "world:hello", flipped("hello")("world"))
})
assert.Equal(t, "a:b", x("a")("b"))
t.Run("flips numeric operations", func(t *testing.T) {
// Curried subtraction: subtract(a)(b) = a - b
subtract := Curry2(func(a, b int) int {
return a - b
})
y := Flip(x)
// Original: 10 - 3 = 7
assert.Equal(t, 7, subtract(10)(3))
assert.Equal(t, "b:a", y("a")("b"))
// Flipped: 3 - 10 = -7
flipped := Flip(subtract)
assert.Equal(t, -7, flipped(10)(3))
})
t.Run("flips division", func(t *testing.T) {
// Curried division: divide(a)(b) = a / b
divide := Curry2(func(a, b float64) float64 {
return a / b
})
// Original: 10 / 2 = 5.0
assert.Equal(t, 5.0, divide(10)(2))
// Flipped: 2 / 10 = 0.2
flipped := Flip(divide)
assert.Equal(t, 0.2, flipped(10)(2))
})
t.Run("flips with partial application", func(t *testing.T) {
// Curried append-like operation
prepend := Curry2(func(prefix, text string) string {
return prefix + text
})
// Create specialized functions with original order
addHello := prepend("Hello, ")
assert.Equal(t, "Hello, World", addHello("World"))
assert.Equal(t, "Hello, Go", addHello("Go"))
// Flip and create specialized functions with reversed order
flipped := Flip(prepend)
addToWorld := flipped("World")
assert.Equal(t, "Hello, World", addToWorld("Hello, "))
assert.Equal(t, "Goodbye, World", addToWorld("Goodbye, "))
})
t.Run("flips with different types", func(t *testing.T) {
// Curried function with different input types
repeat := Curry2(func(s string, n int) string {
result := ""
for i := 0; i < n; i++ {
result += s
}
return result
})
// Original: repeat("x")(3) = "xxx"
assert.Equal(t, "xxx", repeat("x")(3))
assert.Equal(t, "abab", repeat("ab")(2))
// Flipped: repeat(3)("x") = "xxx"
flipped := Flip(repeat)
assert.Equal(t, "xxx", flipped(3)("x"))
assert.Equal(t, "abab", flipped(2)("ab"))
})
t.Run("double flip returns to original", func(t *testing.T) {
// Flipping twice should return to original behavior
original := Curry2(func(a, b string) string {
return a + "-" + b
})
flipped := Flip(original)
doubleFlipped := Flip(flipped)
// Original and double-flipped should behave the same
assert.Equal(t, original("a")("b"), doubleFlipped("a")("b"))
assert.Equal(t, "a-b", doubleFlipped("a")("b"))
})
t.Run("flips with complex types", func(t *testing.T) {
type Person struct {
Name string
Age int
}
// Curried function creating a person
makePerson := Curry2(func(name string, age int) Person {
return Person{Name: name, Age: age}
})
// Original order: name then age
alice := makePerson("Alice")(30)
assert.Equal(t, "Alice", alice.Name)
assert.Equal(t, 30, alice.Age)
// Flipped order: age then name
flipped := Flip(makePerson)
bob := flipped(25)("Bob")
assert.Equal(t, "Bob", bob.Name)
assert.Equal(t, 25, bob.Age)
})
t.Run("flips map operations", func(t *testing.T) {
// Curried map getter: get(map)(key)
get := Curry2(func(m map[string]int, key string) int {
return m[key]
})
data := map[string]int{"a": 1, "b": 2, "c": 3}
// Original: provide map first, then key
getValue := get(data)
assert.Equal(t, 1, getValue("a"))
assert.Equal(t, 2, getValue("b"))
// Flipped: provide key first, then map
flipped := Flip(get)
getA := flipped("a")
assert.Equal(t, 1, getA(data))
data2 := map[string]int{"a": 10, "b": 20}
assert.Equal(t, 10, getA(data2))
})
t.Run("flips boolean operations", func(t *testing.T) {
// Curried logical operation
implies := Curry2(func(a, b bool) bool {
return !a || b
})
// Test truth table for implication
assert.True(t, implies(true)(true)) // T → T = T
assert.False(t, implies(true)(false)) // T → F = F
assert.True(t, implies(false)(true)) // F → T = T
assert.True(t, implies(false)(false)) // F → F = T
// Flipped version (reverse implication)
flipped := Flip(implies)
assert.True(t, flipped(true)(true)) // T ← T = T
assert.True(t, flipped(true)(false)) // T ← F = T
assert.False(t, flipped(false)(true)) // F ← T = F
assert.True(t, flipped(false)(false)) // F ← F = T
})
t.Run("flips with slice operations", func(t *testing.T) {
// Curried slice append
appendTo := Curry2(func(slice []int, elem int) []int {
return append(slice, elem)
})
nums := []int{1, 2, 3}
// Original: provide slice first, then element
add4 := appendTo(nums)
result1 := add4(4)
assert.Equal(t, []int{1, 2, 3, 4}, result1)
// Flipped: provide element first, then slice
flipped := Flip(appendTo)
appendFive := flipped(5)
result2 := appendFive(nums)
assert.Equal(t, []int{1, 2, 3, 5}, result2)
})
}
// TestFlipProperties tests mathematical properties of Flip
func TestFlipProperties(t *testing.T) {
t.Run("flip is involutive (flip . flip = id)", func(t *testing.T) {
// Flipping twice should give back the original function behavior
original := Curry2(func(a, b int) int {
return a*10 + b
})
flipped := Flip(original)
doubleFlipped := Flip(flipped)
// Test with multiple inputs
testCases := []struct{ a, b int }{
{1, 2},
{5, 7},
{0, 0},
{-1, 3},
}
for _, tc := range testCases {
assert.Equal(t,
original(tc.a)(tc.b),
doubleFlipped(tc.a)(tc.b),
"flip(flip(f)) should equal f for inputs (%d, %d)", tc.a, tc.b)
}
})
t.Run("flip preserves function composition", func(t *testing.T) {
// If we have f: A → B → C and g: C → D
// then g ∘ f(a)(b) = g(f(a)(b))
// and g ∘ flip(f)(b)(a) = g(flip(f)(b)(a))
f := Curry2(func(a, b int) int {
return a + b
})
g := func(n int) int {
return n * 2
}
flippedF := Flip(f)
// Compose g with f
composed1 := func(a, b int) int {
return g(f(a)(b))
}
// Compose g with flipped f
composed2 := func(a, b int) int {
return g(flippedF(b)(a))
}
// Both should give the same result
assert.Equal(t, composed1(3, 5), composed2(3, 5))
assert.Equal(t, 16, composed1(3, 5)) // (3 + 5) * 2 = 16
})
}
// BenchmarkFlip benchmarks the Flip function
func BenchmarkFlip(b *testing.B) {
add := Curry2(func(a, b int) int {
return a + b
})
flipped := Flip(add)
b.Run("original", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = add(i)(i + 1)
}
})
b.Run("flipped", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = flipped(i)(i + 1)
}
})
}

View File

@@ -80,7 +80,6 @@ import (
A "github.com/IBM/fp-go/v2/array"
B "github.com/IBM/fp-go/v2/bytes"
E "github.com/IBM/fp-go/v2/either"
ENDO "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
C "github.com/IBM/fp-go/v2/http/content"
@@ -91,16 +90,17 @@ import (
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
)
type (
Builder struct {
method O.Option[string]
method Option[string]
url string
headers http.Header
body O.Option[E.Either[error, []byte]]
body Option[Result[[]byte]]
query url.Values
}
@@ -117,19 +117,19 @@ var (
// Monoid is the [M.Monoid] for the [Endomorphism]
Monoid = ENDO.Monoid[*Builder]()
// Url is a [L.Lens] for the URL
// Url is a [Lens] for the URL
//
// Deprecated: use [URL] instead
Url = L.MakeLensRef((*Builder).GetURL, (*Builder).SetURL)
// URL is a [L.Lens] for the URL
// URL is a [Lens] for the URL
URL = L.MakeLensRef((*Builder).GetURL, (*Builder).SetURL)
// Method is a [L.Lens] for the HTTP method
// Method is a [Lens] for the HTTP method
Method = L.MakeLensRef((*Builder).GetMethod, (*Builder).SetMethod)
// Body is a [L.Lens] for the request body
// Body is a [Lens] for the request body
Body = L.MakeLensRef((*Builder).GetBody, (*Builder).SetBody)
// Headers is a [L.Lens] for the complete set of request headers
// Headers is a [Lens] for the complete set of request headers
Headers = L.MakeLensRef((*Builder).GetHeaders, (*Builder).SetHeaders)
// Query is a [L.Lens] for the set of query parameters
// Query is a [Lens] for the set of query parameters
Query = L.MakeLensRef((*Builder).GetQuery, (*Builder).SetQuery)
rawQuery = L.MakeLensRef(getRawQuery, setRawQuery)
@@ -139,11 +139,11 @@ var (
setHeader = F.Bind2of3((*Builder).SetHeader)
noHeader = O.None[string]()
noBody = O.None[E.Either[error, []byte]]()
noBody = O.None[Result[[]byte]]()
noQueryArg = O.None[string]()
parseURL = E.Eitherize1(url.Parse)
parseQuery = E.Eitherize1(url.ParseQuery)
parseURL = result.Eitherize1(url.Parse)
parseQuery = result.Eitherize1(url.ParseQuery)
// WithQuery creates a [Endomorphism] for a complete set of query parameters
WithQuery = Query.Set
@@ -159,12 +159,12 @@ var (
WithHeaders = Headers.Set
// WithBody creates a [Endomorphism] for a request body
WithBody = F.Flow2(
O.Of[E.Either[error, []byte]],
O.Of[Result[[]byte]],
Body.Set,
)
// WithBytes creates a [Endomorphism] for a request body using bytes
WithBytes = F.Flow2(
E.Of[error, []byte],
result.Of[[]byte],
WithBody,
)
// WithContentType adds the [H.ContentType] header
@@ -202,7 +202,7 @@ var (
)
// bodyAsBytes returns a []byte with a fallback to the empty array
bodyAsBytes = O.Fold(B.Empty, E.Fold(F.Ignore1of1[error](B.Empty), F.Identity[[]byte]))
bodyAsBytes = O.Fold(B.Empty, result.Fold(F.Ignore1of1[error](B.Empty), F.Identity[[]byte]))
)
func setRawQuery(u *url.URL, raw string) *url.URL {
@@ -223,35 +223,35 @@ func (builder *Builder) clone() *Builder {
// GetTargetUrl constructs a full URL with query parameters on top of the provided URL string
//
// Deprecated: use [GetTargetURL] instead
func (builder *Builder) GetTargetUrl() E.Either[error, string] {
func (builder *Builder) GetTargetUrl() Result[string] {
return builder.GetTargetURL()
}
// GetTargetURL constructs a full URL with query parameters on top of the provided URL string
func (builder *Builder) GetTargetURL() E.Either[error, string] {
func (builder *Builder) GetTargetURL() Result[string] {
// construct the final URL
return F.Pipe3(
builder,
Url.Get,
parseURL,
E.Chain(F.Flow4(
result.Chain(F.Flow4(
T.Replicate2[*url.URL],
T.Map2(
F.Flow2(
F.Curry2(setRawQuery),
E.Of[error, func(string) *url.URL],
result.Of[func(string) *url.URL],
),
F.Flow3(
rawQuery.Get,
parseQuery,
E.Map[error](F.Flow2(
result.Map(F.Flow2(
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
(url.Values).Encode,
)),
),
),
T.Tupled2(E.MonadAp[*url.URL, error, string]),
E.Map[error]((*url.URL).String),
T.Tupled2(result.MonadAp[*url.URL, string]),
result.Map((*url.URL).String),
)),
)
}
@@ -285,7 +285,7 @@ func (builder *Builder) SetQuery(query url.Values) *Builder {
return builder
}
func (builder *Builder) GetBody() O.Option[E.Either[error, []byte]] {
func (builder *Builder) GetBody() Option[Result[[]byte]] {
return builder.body
}
@@ -310,7 +310,7 @@ func (builder *Builder) SetHeaders(headers http.Header) *Builder {
return builder
}
func (builder *Builder) SetBody(body O.Option[E.Either[error, []byte]]) *Builder {
func (builder *Builder) SetBody(body Option[Result[[]byte]]) *Builder {
builder.body = body
return builder
}
@@ -325,7 +325,7 @@ func (builder *Builder) DelHeader(name string) *Builder {
return builder
}
func (builder *Builder) GetHeader(name string) O.Option[string] {
func (builder *Builder) GetHeader(name string) Option[string] {
return F.Pipe2(
name,
builder.headers.Get,
@@ -342,8 +342,8 @@ func (builder *Builder) GetHash() string {
return MakeHash(builder)
}
// Header returns a [L.Lens] for a single header
func Header(name string) L.Lens[*Builder, O.Option[string]] {
// Header returns a [Lens] for a single header
func Header(name string) Lens[*Builder, Option[string]] {
get := getHeader(name)
set := F.Bind1of2(setHeader(name))
del := F.Flow2(
@@ -351,7 +351,7 @@ func Header(name string) L.Lens[*Builder, O.Option[string]] {
LZ.Map(delHeader(name)),
)
return L.MakeLens(get, func(b *Builder, value O.Option[string]) *Builder {
return L.MakeLens(get, func(b *Builder, value Option[string]) *Builder {
cpy := b.clone()
return F.Pipe1(
value,
@@ -392,8 +392,8 @@ func WithJSON[T any](data T) Endomorphism {
)
}
// QueryArg is a [L.Lens] for the first value of a query argument
func QueryArg(name string) L.Lens[*Builder, O.Option[string]] {
// QueryArg is a [Lens] for the first value of a query argument
func QueryArg(name string) Lens[*Builder, Option[string]] {
return F.Pipe1(
Query,
L.Compose[*Builder](FM.AtValue(name)),

13
v2/http/builder/type.go Normal file
View File

@@ -0,0 +1,13 @@
package builder
import (
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
Lens[S, T any] = lens.Lens[S, T]
)

View File

@@ -77,8 +77,7 @@ func IsNonNil[GA ~[]A, A any](as GA) bool {
func Reduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B {
current := initial
count := len(fa)
for i := 0; i < count; i++ {
for i := range len(fa) {
current = f(current, fa[i])
}
return current
@@ -86,8 +85,7 @@ func Reduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B {
func ReduceWithIndex[GA ~[]A, A, B any](fa GA, f func(int, B, A) B, initial B) B {
current := initial
count := len(fa)
for i := 0; i < count; i++ {
for i := range len(fa) {
current = f(i, current, fa[i])
}
return current

View File

@@ -163,3 +163,18 @@ func MonadMapLeft[E, A, B, HKTFA, HKTFB any](fmap func(HKTFA, func(ET.Either[E,
func MapLeft[E, A, B, HKTFA, HKTFB any](fmap func(func(ET.Either[E, A]) ET.Either[B, A]) func(HKTFA) HKTFB, f func(E) B) func(HKTFA) HKTFB {
return FC.Map(fmap, ET.MapLeft[A, E, B], f)
}
func MonadChainLeft[EA, A, EB, HKTFA, HKTFB any](
fchain func(HKTFA, func(ET.Either[EA, A]) HKTFB) HKTFB,
fof func(ET.Either[EB, A]) HKTFB,
fa HKTFA,
f func(EA) HKTFB) HKTFB {
return fchain(fa, ET.Fold(f, F.Flow2(ET.Right[EB, A], fof)))
}
func ChainLeft[EA, A, EB, HKTFA, HKTFB any](
fchain func(func(ET.Either[EA, A]) HKTFB) func(HKTFA) HKTFB,
fof func(ET.Either[EB, A]) HKTFB,
f func(EA) HKTFB) func(HKTFA) HKTFB {
return fchain(ET.Fold(f, F.Flow2(ET.Right[EB, A], fof)))
}

61
v2/internal/iter/iter.go Normal file
View File

@@ -0,0 +1,61 @@
package iter
import (
F "github.com/IBM/fp-go/v2/function"
)
func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(int, B, A) B, initial B) B {
current := initial
var i int
for a := range fa {
current = f(i, current, a)
i += 1
}
return current
}
func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B, initial B) B {
current := initial
for a := range fa {
current = f(current, a)
}
return current
}
// Concat concatenates two sequences, yielding all elements from left followed by all elements from right.
func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
return func(yield func(T) bool) {
for t := range left {
if !yield(t) {
return
}
}
for t := range right {
if !yield(t) {
return
}
}
}
}
func Of[GA ~func(yield func(A) bool), A any](a A) GA {
return func(yield func(A) bool) {
yield(a)
}
}
func MonadAppend[GA ~func(yield func(A) bool), A any](f GA, tail A) GA {
return Concat(f, Of[GA](tail))
}
func Append[GA ~func(yield func(A) bool), A any](tail A) func(GA) GA {
return F.Bind2nd(Concat[GA], Of[GA](tail))
}
func Prepend[GA ~func(yield func(A) bool), A any](head A) func(GA) GA {
return F.Bind1st(Concat[GA], Of[GA](head))
}
func Empty[GA ~func(yield func(A) bool), A any]() GA {
return func(_ func(A) bool) {}
}

View File

@@ -0,0 +1,152 @@
// 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 (
F "github.com/IBM/fp-go/v2/function"
)
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta GA,
f func(A) HKTB) HKTRB {
return MonadTraverseReduce(fof, fmap, fap, ta, f, MonadAppend[GB, B], Empty[GB]())
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
func MonadTraverseWithIndex[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta GA,
f func(int, A) HKTB) HKTRB {
return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, f, MonadAppend[GB, B], Empty[GB]())
}
func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
f func(A) HKTB) func(GA) HKTRB {
return func(ma GA) HKTRB {
return MonadTraverse(fof, fmap, fap, ma, f)
}
}
func TraverseWithIndex[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
f func(int, A) HKTB) func(GA) HKTRB {
return func(ma GA) HKTRB {
return MonadTraverseWithIndex(fof, fmap, fap, ma, f)
}
}
func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta GA,
transform func(A) HKTB,
reduce func(GB, B) GB,
initial GB,
) HKTRB {
mmap := fmap(F.Curry2(reduce))
return MonadReduce(ta, func(r HKTRB, a A) HKTRB {
return F.Pipe2(
r,
mmap,
fap(transform(a)),
)
}, fof(initial))
}
func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta GA,
transform func(int, A) HKTB,
reduce func(GB, B) GB,
initial GB,
) HKTRB {
mmap := fmap(F.Curry2(reduce))
return MonadReduceWithIndex(ta, func(idx int, r HKTRB, a A) HKTRB {
return F.Pipe2(
r,
mmap,
fap(transform(idx, a)),
)
}, fof(initial))
}
func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
transform func(A) HKTB,
reduce func(GB, B) GB,
initial GB,
) func(GA) HKTRB {
return func(ta GA) HKTRB {
return MonadTraverseReduce(fof, fmap, fap, ta, transform, reduce, initial)
}
}
func TraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
transform func(int, A) HKTB,
reduce func(GB, B) GB,
initial GB,
) func(GA) HKTRB {
return func(ta GA) HKTRB {
return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, transform, reduce, initial)
}
}

View File

@@ -0,0 +1,9 @@
package iter
import (
I "iter"
)
type (
Seq[A any] = I.Seq[A]
)

View File

@@ -50,7 +50,10 @@ func MonadChain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](fcha
})
}
func Chain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](fchain func(func(A) HKTB) func(HKTA) HKTB, f func(A) GEB) func(GEA) GEB {
func Chain[GEA ~func(E) HKTA, GEB ~func(E) HKTB, A, E, HKTA, HKTB any](
fchain func(func(A) HKTB) func(HKTA) HKTB,
f func(A) GEB,
) func(GEA) GEB {
return func(ma GEA) GEB {
return R.MakeReader(func(r E) HKTB {
return fchain(func(a A) HKTB {

View File

@@ -18,6 +18,7 @@ package io
import (
F "github.com/IBM/fp-go/v2/function"
INTA "github.com/IBM/fp-go/v2/internal/array"
INTI "github.com/IBM/fp-go/v2/internal/iter"
INTR "github.com/IBM/fp-go/v2/internal/record"
)
@@ -60,6 +61,16 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
)
}
func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
return INTI.Traverse[Seq[A]](
Of[Seq[B]],
Map[Seq[B], func(B) Seq[B]],
Ap[Seq[B], B],
f,
)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index.
// Executes in parallel by default.
//

7
v2/io/types.go Normal file
View File

@@ -0,0 +1,7 @@
package io
import "iter"
type (
Seq[T any] = iter.Seq[T]
)

View File

@@ -17,7 +17,7 @@ func Eitherize0[F ~func() (R, error), R any](f F) func() IOEither[error, R] {
// Uneitherize0 converts a function with 1 parameters returning a tuple into a function with 0 parameters returning a [IOEither[error, R]]
func Uneitherize0[F ~func() IOEither[error, R], R any](f F) func() (R, error) {
return G.Uneitherize0[IOEither[error, R]](f)
return G.Uneitherize0(f)
}
// Eitherize1 converts a function with 2 parameters returning a tuple into a function with 1 parameters returning a [IOEither[error, R]]
@@ -27,7 +27,7 @@ func Eitherize1[F ~func(T1) (R, error), T1, R any](f F) func(T1) IOEither[error,
// Uneitherize1 converts a function with 2 parameters returning a tuple into a function with 1 parameters returning a [IOEither[error, R]]
func Uneitherize1[F ~func(T1) IOEither[error, R], T1, R any](f F) func(T1) (R, error) {
return G.Uneitherize1[IOEither[error, R]](f)
return G.Uneitherize1(f)
}
// SequenceT1 converts 1 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple1[T1]]]
@@ -124,7 +124,7 @@ func Eitherize2[F ~func(T1, T2) (R, error), T1, T2, R any](f F) func(T1, T2) IOE
// Uneitherize2 converts a function with 3 parameters returning a tuple into a function with 2 parameters returning a [IOEither[error, R]]
func Uneitherize2[F ~func(T1, T2) IOEither[error, R], T1, T2, R any](f F) func(T1, T2) (R, error) {
return G.Uneitherize2[IOEither[error, R]](f)
return G.Uneitherize2(f)
}
// SequenceT2 converts 2 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple2[T1, T2]]]
@@ -239,7 +239,7 @@ func Eitherize3[F ~func(T1, T2, T3) (R, error), T1, T2, T3, R any](f F) func(T1,
// Uneitherize3 converts a function with 4 parameters returning a tuple into a function with 3 parameters returning a [IOEither[error, R]]
func Uneitherize3[F ~func(T1, T2, T3) IOEither[error, R], T1, T2, T3, R any](f F) func(T1, T2, T3) (R, error) {
return G.Uneitherize3[IOEither[error, R]](f)
return G.Uneitherize3(f)
}
// SequenceT3 converts 3 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple3[T1, T2, T3]]]
@@ -372,7 +372,7 @@ func Eitherize4[F ~func(T1, T2, T3, T4) (R, error), T1, T2, T3, T4, R any](f F)
// Uneitherize4 converts a function with 5 parameters returning a tuple into a function with 4 parameters returning a [IOEither[error, R]]
func Uneitherize4[F ~func(T1, T2, T3, T4) IOEither[error, R], T1, T2, T3, T4, R any](f F) func(T1, T2, T3, T4) (R, error) {
return G.Uneitherize4[IOEither[error, R]](f)
return G.Uneitherize4(f)
}
// SequenceT4 converts 4 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple4[T1, T2, T3, T4]]]
@@ -523,7 +523,7 @@ func Eitherize5[F ~func(T1, T2, T3, T4, T5) (R, error), T1, T2, T3, T4, T5, R an
// Uneitherize5 converts a function with 6 parameters returning a tuple into a function with 5 parameters returning a [IOEither[error, R]]
func Uneitherize5[F ~func(T1, T2, T3, T4, T5) IOEither[error, R], T1, T2, T3, T4, T5, R any](f F) func(T1, T2, T3, T4, T5) (R, error) {
return G.Uneitherize5[IOEither[error, R]](f)
return G.Uneitherize5(f)
}
// SequenceT5 converts 5 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple5[T1, T2, T3, T4, T5]]]
@@ -692,7 +692,7 @@ func Eitherize6[F ~func(T1, T2, T3, T4, T5, T6) (R, error), T1, T2, T3, T4, T5,
// Uneitherize6 converts a function with 7 parameters returning a tuple into a function with 6 parameters returning a [IOEither[error, R]]
func Uneitherize6[F ~func(T1, T2, T3, T4, T5, T6) IOEither[error, R], T1, T2, T3, T4, T5, T6, R any](f F) func(T1, T2, T3, T4, T5, T6) (R, error) {
return G.Uneitherize6[IOEither[error, R]](f)
return G.Uneitherize6(f)
}
// SequenceT6 converts 6 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple6[T1, T2, T3, T4, T5, T6]]]
@@ -1084,7 +1084,7 @@ func Eitherize8[F ~func(T1, T2, T3, T4, T5, T6, T7, T8) (R, error), T1, T2, T3,
// Uneitherize8 converts a function with 9 parameters returning a tuple into a function with 8 parameters returning a [IOEither[error, R]]
func Uneitherize8[F ~func(T1, T2, T3, T4, T5, T6, T7, T8) IOEither[error, R], T1, T2, T3, T4, T5, T6, T7, T8, R any](f F) func(T1, T2, T3, T4, T5, T6, T7, T8) (R, error) {
return G.Uneitherize8[IOEither[error, R]](f)
return G.Uneitherize8(f)
}
// SequenceT8 converts 8 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple8[T1, T2, T3, T4, T5, T6, T7, T8]]]
@@ -1307,7 +1307,7 @@ func Eitherize9[F ~func(T1, T2, T3, T4, T5, T6, T7, T8, T9) (R, error), T1, T2,
// Uneitherize9 converts a function with 10 parameters returning a tuple into a function with 9 parameters returning a [IOEither[error, R]]
func Uneitherize9[F ~func(T1, T2, T3, T4, T5, T6, T7, T8, T9) IOEither[error, R], T1, T2, T3, T4, T5, T6, T7, T8, T9, R any](f F) func(T1, T2, T3, T4, T5, T6, T7, T8, T9) (R, error) {
return G.Uneitherize9[IOEither[error, R]](f)
return G.Uneitherize9(f)
}
// SequenceT9 converts 9 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple9[T1, T2, T3, T4, T5, T6, T7, T8, T9]]]
@@ -1548,7 +1548,7 @@ func Eitherize10[F ~func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) (R, error), T1
// Uneitherize10 converts a function with 11 parameters returning a tuple into a function with 10 parameters returning a [IOEither[error, R]]
func Uneitherize10[F ~func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) IOEither[error, R], T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R any](f F) func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) (R, error) {
return G.Uneitherize10[IOEither[error, R]](f)
return G.Uneitherize10(f)
}
// SequenceT10 converts 10 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]]]

View File

@@ -92,7 +92,7 @@ func ChainOptionK[A, B, E any](onNone func() E) func(func(A) O.Option[B]) Operat
)
}
func MonadChainIOK[E, A, B any](ma IOEither[E, A], f func(A) IO[B]) IOEither[E, B] {
func MonadChainIOK[E, A, B any](ma IOEither[E, A], f io.Kleisli[A, B]) IOEither[E, B] {
return fromio.MonadChainIOK(
MonadChain[E, A, B],
FromIO[E, B],
@@ -101,7 +101,7 @@ func MonadChainIOK[E, A, B any](ma IOEither[E, A], f func(A) IO[B]) IOEither[E,
)
}
func ChainIOK[E, A, B any](f func(A) IO[B]) Operator[E, A, B] {
func ChainIOK[E, A, B any](f io.Kleisli[A, B]) Operator[E, A, B] {
return fromio.ChainIOK(
Chain[E, A, B],
FromIO[E, B],
@@ -147,7 +147,7 @@ func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B] {
return eithert.Chain(io.Chain[Either[E, A], Either[E, B]], io.Of[Either[E, B]], f)
}
func MonadChainEitherK[E, A, B any](ma IOEither[E, A], f func(A) Either[E, B]) IOEither[E, B] {
func MonadChainEitherK[E, A, B any](ma IOEither[E, A], f either.Kleisli[E, A, B]) IOEither[E, B] {
return fromeither.MonadChainEitherK(
MonadChain[E, A, B],
FromEither[E, B],
@@ -156,7 +156,7 @@ func MonadChainEitherK[E, A, B any](ma IOEither[E, A], f func(A) Either[E, B]) I
)
}
func ChainEitherK[E, A, B any](f func(A) Either[E, B]) Operator[E, A, B] {
func ChainEitherK[E, A, B any](f either.Kleisli[E, A, B]) Operator[E, A, B] {
return fromeither.ChainEitherK(
Chain[E, A, B],
FromEither[E, B],
@@ -255,7 +255,7 @@ func BiMap[E1, E2, A, B any](f func(E1) E2, g func(A) B) func(IOEither[E1, A]) I
}
// Fold converts an IOEither into an IO
func Fold[E, A, B any](onLeft func(E) IO[B], onRight func(A) IO[B]) func(IOEither[E, A]) IO[B] {
func Fold[E, A, B any](onLeft func(E) IO[B], onRight io.Kleisli[A, B]) func(IOEither[E, A]) IO[B] {
return eithert.MatchE(io.MonadChain[Either[E, A], B], onLeft, onRight)
}
@@ -284,7 +284,12 @@ func MonadChainFirst[E, A, B any](ma IOEither[E, A], f Kleisli[E, A, B]) IOEithe
)
}
// ChainFirst runs the [IOEither] monad returned by the function but returns the result of the original monad
//go:inline
func MonadTap[E, A, B any](ma IOEither[E, A], f Kleisli[E, A, B]) IOEither[E, A] {
return MonadChainFirst(ma, f)
}
//go:inline
func ChainFirst[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, A] {
return chain.ChainFirst(
Chain[E, A, A],
@@ -293,7 +298,12 @@ func ChainFirst[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, A] {
)
}
func MonadChainFirstEitherK[A, E, B any](ma IOEither[E, A], f func(A) Either[E, B]) IOEither[E, A] {
//go:inline
func Tap[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, A] {
return ChainFirst(f)
}
func MonadChainFirstEitherK[A, E, B any](ma IOEither[E, A], f either.Kleisli[E, A, B]) IOEither[E, A] {
return fromeither.MonadChainFirstEitherK(
MonadChain[E, A, A],
MonadMap[E, B, A],
@@ -303,7 +313,7 @@ func MonadChainFirstEitherK[A, E, B any](ma IOEither[E, A], f func(A) Either[E,
)
}
func ChainFirstEitherK[A, E, B any](f func(A) Either[E, B]) Operator[E, A, A] {
func ChainFirstEitherK[A, E, B any](f either.Kleisli[E, A, B]) Operator[E, A, A] {
return fromeither.ChainFirstEitherK(
Chain[E, A, A],
Map[E, B, A],
@@ -313,7 +323,7 @@ func ChainFirstEitherK[A, E, B any](f func(A) Either[E, B]) Operator[E, A, A] {
}
// MonadChainFirstIOK runs [IO] the monad returned by the function but returns the result of the original monad
func MonadChainFirstIOK[E, A, B any](ma IOEither[E, A], f func(A) IO[B]) IOEither[E, A] {
func MonadChainFirstIOK[E, A, B any](ma IOEither[E, A], f io.Kleisli[A, B]) IOEither[E, A] {
return fromio.MonadChainFirstIOK(
MonadChain[E, A, A],
MonadMap[E, B, A],
@@ -324,7 +334,7 @@ func MonadChainFirstIOK[E, A, B any](ma IOEither[E, A], f func(A) IO[B]) IOEithe
}
// ChainFirstIOK runs the [IO] monad returned by the function but returns the result of the original monad
func ChainFirstIOK[E, A, B any](f func(A) IO[B]) func(IOEither[E, A]) IOEither[E, A] {
func ChainFirstIOK[E, A, B any](f io.Kleisli[A, B]) Operator[E, A, A] {
return fromio.ChainFirstIOK(
Chain[E, A, A],
Map[E, B, A],
@@ -333,7 +343,27 @@ func ChainFirstIOK[E, A, B any](f func(A) IO[B]) func(IOEither[E, A]) IOEither[E
)
}
func MonadFold[E, A, B any](ma IOEither[E, A], onLeft func(E) IO[B], onRight func(A) IO[B]) IO[B] {
//go:inline
func MonadTapEitherK[A, E, B any](ma IOEither[E, A], f either.Kleisli[E, A, B]) IOEither[E, A] {
return MonadChainFirstEitherK(ma, f)
}
//go:inline
func TapEitherK[A, E, B any](f either.Kleisli[E, A, B]) Operator[E, A, A] {
return ChainFirstEitherK(f)
}
// MonadChainFirstIOK runs [IO] the monad returned by the function but returns the result of the original monad
func MonadTapIOK[E, A, B any](ma IOEither[E, A], f io.Kleisli[A, B]) IOEither[E, A] {
return MonadChainFirstIOK(ma, f)
}
// ChainFirstIOK runs the [IO] monad returned by the function but returns the result of the original monad
func TapIOK[E, A, B any](f io.Kleisli[A, B]) Operator[E, A, A] {
return ChainFirstIOK[E](f)
}
func MonadFold[E, A, B any](ma IOEither[E, A], onLeft func(E) IO[B], onRight io.Kleisli[A, B]) IO[B] {
return eithert.FoldE(io.MonadChain[Either[E, A], B], ma, onLeft, onRight)
}
@@ -408,3 +438,149 @@ func Delay[E, A any](delay time.Duration) Operator[E, A, A] {
func After[E, A any](timestamp time.Time) Operator[E, A, A] {
return io.After[Either[E, A]](timestamp)
}
// MonadChainLeft chains a computation on the left (error) side of an [IOEither].
// 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.
//
// This is useful for error recovery or error transformation scenarios where you want to handle
// errors by performing another computation that may also fail.
//
// Parameters:
// - fa: The input [IOEither] that may contain an error of type EA
// - f: A function that takes an error of type EA and returns an [IOEither] with error type EB
//
// Returns:
// - An [IOEither] with the potentially transformed error type EB
//
// Example:
//
// // Recover from a specific error by trying an alternative computation
// result := MonadChainLeft(
// Left[int]("network error"),
// func(err string) IOEither[string, int] {
// if err == "network error" {
// return Right[string](42) // recover with default value
// }
// return Left[int]("unrecoverable: " + err)
// },
// )
func MonadChainLeft[EA, EB, A any](fa IOEither[EA, A], f Kleisli[EB, EA, A]) IOEither[EB, A] {
return eithert.MonadChainLeft(
io.MonadChain[Either[EA, A], Either[EB, A]],
io.MonadOf[Either[EB, A]],
fa,
f,
)
}
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of an [IOEither].
//
// This is particularly useful in functional composition pipelines where you want to handle
// errors by performing another computation that may also fail.
//
// Parameters:
// - f: A function that takes an error of type EA and returns an [IOEither] with error type EB
//
// Returns:
// - A function that transforms an [IOEither] with error type EA to one with error type EB
//
// Example:
//
// // Create a reusable error handler
// recoverFromNetworkError := ChainLeft(func(err string) IOEither[string, int] {
// if strings.Contains(err, "network") {
// return Right[string](0) // return default value
// }
// return Left[int](err) // propagate other errors
// })
//
// result := F.Pipe1(
// Left[int]("network timeout"),
// recoverFromNetworkError,
// )
func ChainLeft[EA, EB, A any](f Kleisli[EB, EA, A]) func(IOEither[EA, A]) IOEither[EB, A] {
return eithert.ChainLeft(
io.Chain[Either[EA, A], Either[EB, A]],
io.Of[Either[EB, A]],
f,
)
}
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
// but always returns the original Left error regardless of what f returns (Left or Right).
// If the input is a Right value, it passes through unchanged without calling f.
//
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
//
// Parameters:
// - ma: The input [IOEither] that may contain an error of type EA
// - f: A function that takes an error of type EA and returns an [IOEither] (typically for side effects)
//
// Returns:
// - An [IOEither] with the original error preserved if input was Left, or the original Right value
//
// Example:
//
// // Log errors but always preserve the original error
// result := MonadChainFirstLeft(
// Left[int]("database error"),
// func(err string) IOEither[string, int] {
// return FromIO[string](func() int {
// log.Printf("Error occurred: %s", err)
// return 0
// })
// },
// )
// // result will always be Left("database error"), even though f returns Right
func MonadChainFirstLeft[A, EA, EB, B any](ma IOEither[EA, A], f Kleisli[EB, EA, B]) IOEither[EA, A] {
return MonadChainLeft(ma, function.Flow2(f, Fold(function.Constant1[EB](ma), function.Constant1[B](ma))))
}
//go:inline
func MonadTapLeft[A, EA, EB, B any](ma IOEither[EA, A], f Kleisli[EB, EA, B]) IOEither[EA, A] {
return MonadChainFirstLeft(ma, f)
}
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
//
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
// ensuring the error path is preserved.
//
// Parameters:
// - f: A function that takes an error of type EA and returns an [IOEither] (typically for side effects)
//
// Returns:
// - An [Operator] that performs the side effect but always returns the original error if input was Left
//
// Example:
//
// // Create a reusable error logger
// logError := ChainFirstLeft(func(err string) IOEither[any, int] {
// return FromIO[any](func() int {
// log.Printf("Error: %s", err)
// return 0
// })
// })
//
// result := F.Pipe1(
// Left[int]("validation failed"),
// logError, // logs the error
// )
// // result is always Left("validation failed"), even though f returns Right
func ChainFirstLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
return ChainLeft(func(e EA) IOEither[EA, A] {
ma := Left[A](e)
return MonadFold(f(e), function.Constant1[EB](ma), function.Constant1[B](ma))
})
}
//go:inline
func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
return ChainFirstLeft[A](f)
}

View File

@@ -23,7 +23,6 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/IBM/fp-go/v2/io"
I "github.com/IBM/fp-go/v2/io"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
@@ -69,7 +68,7 @@ func TestFromOption(t *testing.T) {
}
func TestChainIOK(t *testing.T) {
f := ChainIOK[string](func(n int) I.IO[string] {
f := ChainIOK[string](func(n int) IO[string] {
return func() string {
return fmt.Sprintf("%d", n)
}
@@ -106,8 +105,8 @@ func TestChainFirst(t *testing.T) {
}
func TestChainFirstIOK(t *testing.T) {
f := func(a string) I.IO[int] {
return I.Of(len(a))
f := func(a string) IO[int] {
return io.Of(len(a))
}
good := Of[string]("foo")
ch := ChainFirstIOK[string](f)
@@ -134,3 +133,271 @@ func TestApSecond(t *testing.T) {
assert.Equal(t, E.Of[error]("b"), x())
}
func TestMonadChainLeft(t *testing.T) {
// Test with Left value - should apply the function
t.Run("Left value applies function", func(t *testing.T) {
result := MonadChainLeft(
Left[int]("error1"),
func(e string) IOEither[string, int] {
return Left[int]("transformed: " + e)
},
)
assert.Equal(t, E.Left[int]("transformed: error1"), result())
})
// Test with Left value - function returns Right (error recovery)
t.Run("Left value recovers to Right", func(t *testing.T) {
result := MonadChainLeft(
Left[int]("recoverable"),
func(e string) IOEither[string, int] {
if e == "recoverable" {
return Right[string](42)
}
return Left[int](e)
},
)
assert.Equal(t, E.Right[string](42), result())
})
// Test with Right value - should pass through unchanged
t.Run("Right value passes through", func(t *testing.T) {
result := MonadChainLeft(
Right[string](100),
func(e string) IOEither[string, int] {
return Left[int]("should not be called")
},
)
assert.Equal(t, E.Right[string](100), result())
})
// Test error type transformation
t.Run("Error type transformation", func(t *testing.T) {
result := MonadChainLeft(
Left[int]("404"),
func(e string) IOEither[int, int] {
return Left[int](404)
},
)
assert.Equal(t, E.Left[int](404), result())
})
}
func TestChainLeft(t *testing.T) {
// Test with Left value - should apply the function
t.Run("Left value applies function", func(t *testing.T) {
chainFn := ChainLeft(func(e string) IOEither[string, int] {
return Left[int]("chained: " + e)
})
result := F.Pipe1(
Left[int]("original"),
chainFn,
)
assert.Equal(t, E.Left[int]("chained: original"), result())
})
// Test with Left value - function returns Right (error recovery)
t.Run("Left value recovers to Right", func(t *testing.T) {
chainFn := ChainLeft(func(e string) IOEither[string, int] {
if e == "network error" {
return Right[string](0) // default value
}
return Left[int](e)
})
result := F.Pipe1(
Left[int]("network error"),
chainFn,
)
assert.Equal(t, E.Right[string](0), result())
})
// Test with Right value - should pass through unchanged
t.Run("Right value passes through", func(t *testing.T) {
chainFn := ChainLeft(func(e string) IOEither[string, int] {
return Left[int]("should not be called")
})
result := F.Pipe1(
Right[string](42),
chainFn,
)
assert.Equal(t, E.Right[string](42), result())
})
// Test composition with other operations
t.Run("Composition with Map", func(t *testing.T) {
result := F.Pipe2(
Left[int]("error"),
ChainLeft(func(e string) IOEither[string, int] {
return Left[int]("handled: " + e)
}),
Map[string](utils.Double),
)
assert.Equal(t, E.Left[int]("handled: error"), result())
})
}
func TestMonadChainFirstLeft(t *testing.T) {
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
sideEffectCalled := false
result := MonadChainFirstLeft(
Left[int]("original error"),
func(e string) IOEither[string, int] {
sideEffectCalled = true
return Left[int]("new error") // This error is ignored, original is returned
},
)
actualResult := result()
assert.True(t, sideEffectCalled)
assert.Equal(t, E.Left[int]("original error"), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var capturedError string
result := MonadChainFirstLeft(
Left[int]("validation failed"),
func(e string) IOEither[string, int] {
capturedError = e
return Right[string](999) // This Right value is ignored, original Left is returned
},
)
actualResult := result()
assert.Equal(t, "validation failed", capturedError)
assert.Equal(t, E.Left[int]("validation failed"), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
sideEffectCalled := false
result := MonadChainFirstLeft(
Right[string](42),
func(e string) IOEither[string, int] {
sideEffectCalled = true
return Left[int]("should not be called")
},
)
assert.False(t, sideEffectCalled)
assert.Equal(t, E.Right[string](42), result())
})
// Test that side effects are executed but original error is always preserved
t.Run("Side effects executed but original error preserved", func(t *testing.T) {
effectCount := 0
result := MonadChainFirstLeft(
Left[int]("original error"),
func(e string) IOEither[string, int] {
effectCount++
// Try to return Right, but original Left should still be returned
return Right[string](999)
},
)
actualResult := result()
assert.Equal(t, 1, effectCount)
assert.Equal(t, E.Left[int]("original error"), actualResult)
})
}
func TestChainFirstLeft(t *testing.T) {
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
var captured string
chainFn := ChainFirstLeft[int](func(e string) IOEither[string, int] {
captured = e
return Left[int]("ignored error")
})
result := F.Pipe1(
Left[int]("test error"),
chainFn,
)
actualResult := result()
assert.Equal(t, "test error", captured)
assert.Equal(t, E.Left[int]("test error"), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var captured string
chainFn := ChainFirstLeft[int](func(e string) IOEither[string, int] {
captured = e
return Right[string](42) // This Right is ignored, original Left is returned
})
result := F.Pipe1(
Left[int]("test error"),
chainFn,
)
actualResult := result()
assert.Equal(t, "test error", captured)
assert.Equal(t, E.Left[int]("test error"), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
called := false
chainFn := ChainFirstLeft[int](func(e string) IOEither[string, int] {
called = true
return Right[string](0)
})
result := F.Pipe1(
Right[string](100),
chainFn,
)
assert.False(t, called)
assert.Equal(t, E.Right[string](100), result())
})
// Test that original error is always preserved regardless of what f returns
t.Run("Original error always preserved", func(t *testing.T) {
chainFn := ChainFirstLeft[int](func(e string) IOEither[string, int] {
// Try to return Right, but original Left should still be returned
return Right[string](999)
})
result := F.Pipe1(
Left[int]("original"),
chainFn,
)
assert.Equal(t, E.Left[int]("original"), result())
})
// Test with IO side effects - original Left is always preserved
t.Run("IO side effects with Left preservation", func(t *testing.T) {
effectCount := 0
chainFn := ChainFirstLeft[int](func(e string) IOEither[string, int] {
return FromIO[string](func() int {
effectCount++
return 0
})
})
// Even though FromIO wraps in Right, the original Left is preserved
result := F.Pipe1(
Left[int]("error"),
chainFn,
)
assert.Equal(t, E.Left[int]("error"), result())
assert.Equal(t, 1, effectCount)
})
// Test logging with Left preservation
t.Run("Logging with Left preservation", func(t *testing.T) {
errorLog := []string{}
logError := ChainFirstLeft[string](func(e string) IOEither[string, string] {
errorLog = append(errorLog, "Logged: "+e)
return Left[string]("log entry") // This is ignored, original is preserved
})
result := F.Pipe2(
Left[string]("step1"),
logError,
ChainLeft(func(e string) IOEither[string, string] {
return Left[string]("step2")
}),
)
actualResult := result()
assert.Equal(t, []string{"Logged: step1"}, errorLog)
assert.Equal(t, E.Left[string]("step2"), actualResult)
})
}

View File

@@ -18,9 +18,11 @@ package ioresult
import (
"time"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
//go:inline
@@ -79,12 +81,12 @@ func ChainOptionK[A, B any](onNone func() error) func(func(A) O.Option[B]) Opera
}
//go:inline
func MonadChainIOK[A, B any](ma IOResult[A], f func(A) IO[B]) IOResult[B] {
func MonadChainIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[B] {
return ioeither.MonadChainIOK(ma, f)
}
//go:inline
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
return ioeither.ChainIOK[error](f)
}
@@ -138,22 +140,22 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
}
//go:inline
func MonadChainEitherK[A, B any](ma IOResult[A], f func(A) Result[B]) IOResult[B] {
func MonadChainEitherK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[B] {
return ioeither.MonadChainEitherK(ma, f)
}
//go:inline
func MonadChainResultK[A, B any](ma IOResult[A], f func(A) Result[B]) IOResult[B] {
func MonadChainResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[B] {
return ioeither.MonadChainEitherK(ma, f)
}
//go:inline
func ChainEitherK[A, B any](f func(A) Result[B]) Operator[A, B] {
func ChainEitherK[A, B any](f result.Kleisli[A, B]) Operator[A, B] {
return ioeither.ChainEitherK(f)
}
//go:inline
func ChainResultK[A, B any](f func(A) Result[B]) Operator[A, B] {
func ChainResultK[A, B any](f result.Kleisli[A, B]) Operator[A, B] {
return ioeither.ChainEitherK(f)
}
@@ -238,7 +240,7 @@ func BiMap[E, A, B any](f func(error) E, g func(A) B) func(IOResult[A]) ioeither
// Fold converts an IOResult into an IO
//
//go:inline
func Fold[A, B any](onLeft func(error) IO[B], onRight func(A) IO[B]) func(IOResult[A]) IO[B] {
func Fold[A, B any](onLeft func(error) IO[B], onRight io.Kleisli[A, B]) func(IOResult[A]) IO[B] {
return ioeither.Fold(onLeft, onRight)
}
@@ -270,6 +272,11 @@ func MonadChainFirst[A, B any](ma IOResult[A], f Kleisli[A, B]) IOResult[A] {
return ioeither.MonadChainFirst(ma, f)
}
//go:inline
func MonadTap[A, B any](ma IOResult[A], f Kleisli[A, B]) IOResult[A] {
return ioeither.MonadTap(ma, f)
}
// ChainFirst runs the [IOResult] monad returned by the function but returns the result of the original monad
//
//go:inline
@@ -278,36 +285,65 @@ func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
}
//go:inline
func MonadChainFirstEitherK[A, B any](ma IOResult[A], f func(A) Result[B]) IOResult[A] {
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return ioeither.Tap(f)
}
//go:inline
func MonadChainFirstEitherK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
return ioeither.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadChainFirstResultK[A, B any](ma IOResult[A], f func(A) Result[B]) IOResult[A] {
func MonadTapEitherK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
return ioeither.MonadTapEitherK(ma, f)
}
//go:inline
func MonadChainFirstResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
return ioeither.MonadChainFirstEitherK(ma, f)
}
//go:inline
func ChainFirstEitherK[A, B any](f func(A) Result[B]) Operator[A, A] {
func MonadTapResultK[A, B any](ma IOResult[A], f result.Kleisli[A, B]) IOResult[A] {
return ioeither.MonadTapEitherK(ma, f)
}
//go:inline
func ChainFirstEitherK[A, B any](f result.Kleisli[A, B]) Operator[A, A] {
return ioeither.ChainFirstEitherK(f)
}
//go:inline
func TapEitherK[A, B any](f result.Kleisli[A, B]) Operator[A, A] {
return ioeither.TapEitherK(f)
}
// MonadChainFirstIOK runs [IO] the monad returned by the function but returns the result of the original monad
//
//go:inline
func MonadChainFirstIOK[A, B any](ma IOResult[A], f func(A) IO[B]) IOResult[A] {
func MonadChainFirstIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[A] {
return ioeither.MonadChainFirstIOK(ma, f)
}
//go:inline
func MonadTapIOK[A, B any](ma IOResult[A], f io.Kleisli[A, B]) IOResult[A] {
return ioeither.MonadTapIOK(ma, f)
}
// ChainFirstIOK runs the [IO] monad returned by the function but returns the result of the original monad
//
//go:inline
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return ioeither.ChainFirstIOK[error](f)
}
func TapIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return ioeither.TapIOK[error](f)
}
//go:inline
func MonadFold[A, B any](ma IOResult[A], onLeft func(error) IO[B], onRight func(A) IO[B]) IO[B] {
func MonadFold[A, B any](ma IOResult[A], onLeft func(error) IO[B], onRight io.Kleisli[A, B]) IO[B] {
return ioeither.MonadFold(ma, onLeft, onRight)
}
@@ -383,3 +419,33 @@ func Delay[A any](delay time.Duration) Operator[A, A] {
func After[A any](timestamp time.Time) Operator[A, A] {
return ioeither.After[error, A](timestamp)
}
//go:inline
func MonadChainLeft[A any](fa IOResult[A], f Kleisli[error, A]) IOResult[A] {
return ioeither.MonadChainLeft(fa, f)
}
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
return ioeither.ChainLeft(f)
}
//go:inline
func MonadChainFirstLeft[A, B any](fa IOResult[A], f Kleisli[error, B]) IOResult[A] {
return ioeither.MonadChainFirstLeft(fa, f)
}
//go:inline
func MonadTapLeft[A, B any](fa IOResult[A], f Kleisli[error, B]) IOResult[A] {
return ioeither.MonadTapLeft(fa, f)
}
//go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return ioeither.ChainFirstLeft[A](f)
}
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return ioeither.TapLeft[A](f)
}

887
v2/iterator/iter/iter.go Normal file
View File

@@ -0,0 +1,887 @@
// 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 provides functional programming utilities for Go 1.23+ iterators.
//
// This package offers a comprehensive set of operations for working with lazy sequences
// using Go's native iter.Seq and iter.Seq2 types. It follows functional programming
// principles and provides monadic operations, transformations, and reductions.
//
// The package supports:
// - Functor operations (Map, MapWithIndex, MapWithKey)
// - Monad operations (Chain, Flatten, Ap)
// - Filtering (Filter, FilterMap, FilterWithIndex, FilterWithKey)
// - Folding and reduction (Reduce, Fold, FoldMap)
// - Sequence construction (Of, From, MakeBy, Replicate)
// - Sequence combination (Zip, Prepend, Append)
//
// All operations are lazy and only execute when the sequence is consumed via iteration.
//
// Example usage:
//
// // Create a sequence and transform it
// seq := From(1, 2, 3, 4, 5)
// doubled := Map(func(x int) int { return x * 2 })(seq)
//
// // Filter and reduce
// evens := Filter(func(x int) bool { return x%2 == 0 })(doubled)
// sum := MonadReduce(evens, func(acc, x int) int { return acc + x }, 0)
// // sum = 20 (2+4+6+8+10 from doubled evens)
package iter
import (
"slices"
I "iter"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
G "github.com/IBM/fp-go/v2/internal/iter"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
)
// Of creates a sequence containing a single element.
//
// Example:
//
// seq := Of(42)
// // yields: 42
//
//go:inline
func Of[A any](a A) Seq[A] {
return G.Of[Seq[A]](a)
}
// Of2 creates a key-value sequence containing a single key-value pair.
//
// Example:
//
// seq := Of2("key", 100)
// // yields: ("key", 100)
func Of2[K, A any](k K, a A) Seq2[K, A] {
return func(yield func(K, A) bool) {
yield(k, a)
}
}
// MonadMap transforms each element in a sequence using the provided function.
// This is the monadic version that takes the sequence as the first parameter.
//
// Example:
//
// seq := From(1, 2, 3)
// result := MonadMap(seq, func(x int) int { return x * 2 })
// // yields: 2, 4, 6
func MonadMap[A, B any](as Seq[A], f func(A) B) Seq[B] {
return func(yield Predicate[B]) {
for a := range as {
if !yield(f(a)) {
return
}
}
}
}
// Map returns a function that transforms each element in a sequence.
// This is the curried version of MonadMap.
//
// Example:
//
// double := Map(func(x int) int { return x * 2 })
// seq := From(1, 2, 3)
// result := double(seq)
// // yields: 2, 4, 6
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return F.Bind2nd(MonadMap[A, B], f)
}
// MonadMapWithIndex transforms each element in a sequence using a function that also receives the element's index.
//
// Example:
//
// seq := From("a", "b", "c")
// result := MonadMapWithIndex(seq, func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// })
// // yields: "0:a", "1:b", "2:c"
func MonadMapWithIndex[A, B any](as Seq[A], f func(int, A) B) Seq[B] {
return func(yield Predicate[B]) {
var i int
for a := range as {
if !yield(f(i, a)) {
return
}
i += 1
}
}
}
// MapWithIndex returns a function that transforms elements with their indices.
// This is the curried version of MonadMapWithIndex.
//
// Example:
//
// addIndex := MapWithIndex(func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// })
// seq := From("a", "b", "c")
// result := addIndex(seq)
// // yields: "0:a", "1:b", "2:c"
//
//go:inline
func MapWithIndex[A, B any](f func(int, A) B) Operator[A, B] {
return F.Bind2nd(MonadMapWithIndex[A, B], f)
}
// MonadMapWithKey transforms values in a key-value sequence using a function that receives both key and value.
//
// Example:
//
// seq := Of2("x", 10)
// result := MonadMapWithKey(seq, func(k string, v int) int { return v * 2 })
// // yields: ("x", 20)
func MonadMapWithKey[K, A, B any](as Seq2[K, A], f func(K, A) B) Seq2[K, B] {
return func(yield func(K, B) bool) {
for k, a := range as {
if !yield(k, f(k, a)) {
return
}
}
}
}
// MapWithKey returns a function that transforms values using their keys.
// This is the curried version of MonadMapWithKey.
//
// Example:
//
// doubleValue := MapWithKey(func(k string, v int) int { return v * 2 })
// seq := Of2("x", 10)
// result := doubleValue(seq)
// // yields: ("x", 20)
//
//go:inline
func MapWithKey[K, A, B any](f func(K, A) B) Operator2[K, A, B] {
return F.Bind2nd(MonadMapWithKey[K, A, B], f)
}
// MonadFilter returns a sequence containing only elements that satisfy the predicate.
//
// Example:
//
// seq := From(1, 2, 3, 4, 5)
// result := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
// // yields: 2, 4
func MonadFilter[A any](as Seq[A], pred func(A) bool) Seq[A] {
return func(yield Predicate[A]) {
for a := range as {
if pred(a) {
if !yield(a) {
return
}
}
}
}
}
// Filter returns a function that filters elements based on a predicate.
// This is the curried version of MonadFilter.
//
// Example:
//
// evens := Filter(func(x int) bool { return x%2 == 0 })
// seq := From(1, 2, 3, 4, 5)
// result := evens(seq)
// // yields: 2, 4
//
//go:inline
func Filter[A any](pred func(A) bool) Operator[A, A] {
return F.Bind2nd(MonadFilter[A], pred)
}
// MonadFilterWithIndex filters elements using a predicate that also receives the element's index.
//
// Example:
//
// seq := From("a", "b", "c", "d")
// result := MonadFilterWithIndex(seq, func(i int, s string) bool { return i%2 == 0 })
// // yields: "a", "c" (elements at even indices)
func MonadFilterWithIndex[A any](as Seq[A], pred func(int, A) bool) Seq[A] {
return func(yield Predicate[A]) {
var i int
for a := range as {
if pred(i, a) {
if !yield(a) {
return
}
}
i++
}
}
}
// FilterWithIndex returns a function that filters elements based on their index and value.
// This is the curried version of MonadFilterWithIndex.
//
// Example:
//
// evenIndices := FilterWithIndex(func(i int, s string) bool { return i%2 == 0 })
// seq := From("a", "b", "c", "d")
// result := evenIndices(seq)
// // yields: "a", "c"
//
//go:inline
func FilterWithIndex[A any](pred func(int, A) bool) Operator[A, A] {
return F.Bind2nd(MonadFilterWithIndex[A], pred)
}
// MonadFilterWithKey filters key-value pairs using a predicate that receives both key and value.
//
// Example:
//
// seq := Of2("x", 10)
// result := MonadFilterWithKey(seq, func(k string, v int) bool { return v > 5 })
// // yields: ("x", 10)
func MonadFilterWithKey[K, A any](as Seq2[K, A], pred func(K, A) bool) Seq2[K, A] {
return func(yield func(K, A) bool) {
for k, a := range as {
if pred(k, a) {
if !yield(k, a) {
return
}
}
}
}
}
// FilterWithKey returns a function that filters key-value pairs based on a predicate.
// This is the curried version of MonadFilterWithKey.
//
// Example:
//
// largeValues := FilterWithKey(func(k string, v int) bool { return v > 5 })
// seq := Of2("x", 10)
// result := largeValues(seq)
// // yields: ("x", 10)
//
//go:inline
func FilterWithKey[K, A any](pred func(K, A) bool) Operator2[K, A, A] {
return F.Bind2nd(MonadFilterWithKey[K, A], pred)
}
// MonadFilterMap applies a function that returns an Option to each element,
// keeping only the Some values and unwrapping them.
//
// Example:
//
// seq := From(1, 2, 3, 4, 5)
// result := MonadFilterMap(seq, func(x int) Option[int] {
// if x%2 == 0 {
// return option.Some(x * 10)
// }
// return option.None[int]()
// })
// // yields: 20, 40
func MonadFilterMap[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
return func(yield Predicate[B]) {
for a := range as {
if b, ok := option.Unwrap(f(a)); ok {
if !yield(b) {
return
}
}
}
}
}
// FilterMap returns a function that filters and maps in one operation.
// This is the curried version of MonadFilterMap.
//
// Example:
//
// evenDoubled := FilterMap(func(x int) Option[int] {
// if x%2 == 0 {
// return option.Some(x * 2)
// }
// return option.None[int]()
// })
// seq := From(1, 2, 3, 4)
// result := evenDoubled(seq)
// // yields: 4, 8
//
//go:inline
func FilterMap[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return F.Bind2nd(MonadFilterMap[A, B], f)
}
// MonadFilterMapWithIndex applies a function with index that returns an Option,
// keeping only the Some values.
//
// Example:
//
// seq := From("a", "b", "c")
// result := MonadFilterMapWithIndex(seq, func(i int, s string) Option[string] {
// if i%2 == 0 {
// return option.Some(fmt.Sprintf("%d:%s", i, s))
// }
// return option.None[string]()
// })
// // yields: "0:a", "2:c"
func MonadFilterMapWithIndex[A, B any](as Seq[A], f func(int, A) Option[B]) Seq[B] {
return func(yield Predicate[B]) {
var i int
for a := range as {
if b, ok := option.Unwrap(f(i, a)); ok {
if !yield(b) {
return
}
}
i++
}
}
}
// FilterMapWithIndex returns a function that filters and maps with index.
// This is the curried version of MonadFilterMapWithIndex.
//
// Example:
//
// evenIndexed := FilterMapWithIndex(func(i int, s string) Option[string] {
// if i%2 == 0 {
// return option.Some(s)
// }
// return option.None[string]()
// })
// seq := From("a", "b", "c", "d")
// result := evenIndexed(seq)
// // yields: "a", "c"
//
//go:inline
func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return F.Bind2nd(MonadFilterMapWithIndex[A, B], f)
}
// MonadFilterMapWithKey applies a function with key that returns an Option to key-value pairs,
// keeping only the Some values.
//
// Example:
//
// seq := Of2("x", 10)
// result := MonadFilterMapWithKey(seq, func(k string, v int) Option[int] {
// if v > 5 {
// return option.Some(v * 2)
// }
// return option.None[int]()
// })
// // yields: ("x", 20)
func MonadFilterMapWithKey[K, A, B any](as Seq2[K, A], f func(K, A) Option[B]) Seq2[K, B] {
return func(yield func(K, B) bool) {
for k, a := range as {
if b, ok := option.Unwrap(f(k, a)); ok {
if !yield(k, b) {
return
}
}
}
}
}
// FilterMapWithKey returns a function that filters and maps key-value pairs.
// This is the curried version of MonadFilterMapWithKey.
//
// Example:
//
// largeDoubled := FilterMapWithKey(func(k string, v int) Option[int] {
// if v > 5 {
// return option.Some(v * 2)
// }
// return option.None[int]()
// })
// seq := Of2("x", 10)
// result := largeDoubled(seq)
// // yields: ("x", 20)
//
//go:inline
func FilterMapWithKey[K, A, B any](f func(K, A) Option[B]) Operator2[K, A, B] {
return F.Bind2nd(MonadFilterMapWithKey[K, A, B], f)
}
// MonadChain applies a function that returns a sequence to each element and flattens the results.
// This is the monadic bind operation (flatMap).
//
// Example:
//
// seq := From(1, 2, 3)
// result := MonadChain(seq, func(x int) Seq[int] {
// return From(x, x*10)
// })
// // yields: 1, 10, 2, 20, 3, 30
func MonadChain[A, B any](as Seq[A], f Kleisli[A, B]) Seq[B] {
return func(yield Predicate[B]) {
for a := range as {
for b := range f(a) {
if !yield(b) {
return
}
}
}
}
}
// Chain returns a function that chains (flatMaps) a sequence transformation.
// This is the curried version of MonadChain.
//
// Example:
//
// duplicate := Chain(func(x int) Seq[int] { return From(x, x) })
// seq := From(1, 2, 3)
// result := duplicate(seq)
// // yields: 1, 1, 2, 2, 3, 3
//
//go:inline
func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
return F.Bind2nd(MonadChain[A, B], f)
}
// Flatten flattens a sequence of sequences into a single sequence.
//
// Example:
//
// nested := From(From(1, 2), From(3, 4), From(5))
// result := Flatten(nested)
// // yields: 1, 2, 3, 4, 5
//
//go:inline
func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
return MonadChain(mma, F.Identity[Seq[A]])
}
// MonadAp applies a sequence of functions to a sequence of values.
// This is the applicative apply operation.
//
// Example:
//
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
// vals := From(5, 3)
// result := MonadAp(fns, vals)
// // yields: 10, 6, 15, 13 (each function applied to each value)
//
//go:inline
func MonadAp[B, A any](fab Seq[func(A) B], fa Seq[A]) Seq[B] {
return MonadChain(fab, F.Bind1st(MonadMap[A, B], fa))
}
// Ap returns a function that applies functions to values.
// This is the curried version of MonadAp.
//
// Example:
//
// applyTo5 := Ap(From(5))
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
// result := applyTo5(fns)
// // yields: 10, 15
//
//go:inline
func Ap[B, A any](fa Seq[A]) Operator[func(A) B, B] {
return F.Bind2nd(MonadAp[B, A], fa)
}
// From creates a sequence from a variadic list of elements.
//
// Example:
//
// seq := From(1, 2, 3, 4, 5)
// // yields: 1, 2, 3, 4, 5
//
//go:inline
func From[A any](data ...A) Seq[A] {
return slices.Values(data)
}
// Empty returns an empty sequence that yields no elements.
//
// Example:
//
// seq := Empty[int]()
// // yields nothing
//
//go:inline
func Empty[A any]() Seq[A] {
return G.Empty[Seq[A]]()
}
// MakeBy creates a sequence of n elements by applying a function to each index.
// Returns an empty sequence if n <= 0.
//
// Example:
//
// seq := MakeBy(5, func(i int) int { return i * i })
// // yields: 0, 1, 4, 9, 16
func MakeBy[A any](n int, f func(int) A) Seq[A] {
// sanity check
if n <= 0 {
return Empty[A]()
}
// run the generator function across the input
return func(yield Predicate[A]) {
for i := range n {
if !yield(f(i)) {
return
}
}
}
}
// Replicate creates a sequence containing n copies of the same element.
//
// Example:
//
// seq := Replicate(3, "hello")
// // yields: "hello", "hello", "hello"
//
//go:inline
func Replicate[A any](n int, a A) Seq[A] {
return MakeBy(n, F.Constant1[int](a))
}
// MonadReduce reduces a sequence to a single value by applying a function to each element
// and an accumulator, starting with an initial value.
//
// Example:
//
// seq := From(1, 2, 3, 4, 5)
// sum := MonadReduce(seq, func(acc, x int) int { return acc + x }, 0)
// // returns: 15
//
//go:inline
func MonadReduce[A, B any](fa Seq[A], f func(B, A) B, initial B) B {
return G.MonadReduce(fa, f, initial)
}
// Reduce returns a function that reduces a sequence to a single value.
// This is the curried version of MonadReduce.
//
// Example:
//
// sum := Reduce(func(acc, x int) int { return acc + x }, 0)
// seq := From(1, 2, 3, 4, 5)
// result := sum(seq)
// // returns: 15
func Reduce[A, B any](f func(B, A) B, initial B) func(Seq[A]) B {
return func(fa Seq[A]) B {
return MonadReduce(fa, f, initial)
}
}
// MonadReduceWithIndex reduces a sequence using a function that also receives the element's index.
//
// Example:
//
// seq := From(10, 20, 30)
// result := MonadReduceWithIndex(seq, func(i, acc, x int) int {
// return acc + (i * x)
// }, 0)
// // returns: 0*10 + 1*20 + 2*30 = 80
//
//go:inline
func MonadReduceWithIndex[A, B any](fa Seq[A], f func(int, B, A) B, initial B) B {
return G.MonadReduceWithIndex(fa, f, initial)
}
// ReduceWithIndex returns a function that reduces with index.
// This is the curried version of MonadReduceWithIndex.
//
// Example:
//
// weightedSum := ReduceWithIndex(func(i, acc, x int) int {
// return acc + (i * x)
// }, 0)
// seq := From(10, 20, 30)
// result := weightedSum(seq)
// // returns: 80
func ReduceWithIndex[A, B any](f func(int, B, A) B, initial B) func(Seq[A]) B {
return func(fa Seq[A]) B {
return MonadReduceWithIndex(fa, f, initial)
}
}
// MonadReduceWithKey reduces a key-value sequence using a function that receives the key.
//
// Example:
//
// seq := Of2("x", 10)
// result := MonadReduceWithKey(seq, func(k string, acc int, v int) int {
// return acc + v
// }, 0)
// // returns: 10
func MonadReduceWithKey[K, A, B any](fa Seq2[K, A], f func(K, B, A) B, initial B) B {
current := initial
for k, a := range fa {
current = f(k, current, a)
}
return current
}
// ReduceWithKey returns a function that reduces key-value pairs.
// This is the curried version of MonadReduceWithKey.
//
// Example:
//
// sumValues := ReduceWithKey(func(k string, acc int, v int) int {
// return acc + v
// }, 0)
// seq := Of2("x", 10)
// result := sumValues(seq)
// // returns: 10
func ReduceWithKey[K, A, B any](f func(K, B, A) B, initial B) func(Seq2[K, A]) B {
return func(fa Seq2[K, A]) B {
return MonadReduceWithKey(fa, f, initial)
}
}
// MonadFold folds a sequence using a monoid's concat operation and empty value.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/number"
// seq := From(1, 2, 3, 4, 5)
// sum := MonadFold(seq, number.MonoidSum[int]())
// // returns: 15
//
//go:inline
func MonadFold[A any](fa Seq[A], m M.Monoid[A]) A {
return MonadReduce(fa, m.Concat, m.Empty())
}
// Fold returns a function that folds a sequence using a monoid.
// This is the curried version of MonadFold.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/number"
// sumAll := Fold(number.MonoidSum[int]())
// seq := From(1, 2, 3, 4, 5)
// result := sumAll(seq)
// // returns: 15
//
//go:inline
func Fold[A any](m M.Monoid[A]) func(Seq[A]) A {
return Reduce(m.Concat, m.Empty())
}
// MonadFoldMap maps each element to a monoid value and combines them using the monoid.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/string"
// seq := From(1, 2, 3)
// result := MonadFoldMap(seq, func(x int) string {
// return fmt.Sprintf("%d ", x)
// }, string.Monoid)
// // returns: "1 2 3 "
//
//go:inline
func MonadFoldMap[A, B any](fa Seq[A], f func(A) B, m M.Monoid[B]) B {
return MonadReduce(fa, func(b B, a A) B {
return m.Concat(b, f(a))
}, m.Empty())
}
// FoldMap returns a function that maps and folds using a monoid.
// This is the curried version of MonadFoldMap.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/string"
// stringify := FoldMap(string.Monoid)(func(x int) string {
// return fmt.Sprintf("%d ", x)
// })
// seq := From(1, 2, 3)
// result := stringify(seq)
// // returns: "1 2 3 "
//
//go:inline
func FoldMap[A, B any](m M.Monoid[B]) func(func(A) B) func(Seq[A]) B {
return func(f func(A) B) func(Seq[A]) B {
return func(as Seq[A]) B {
return MonadFoldMap(as, f, m)
}
}
}
// MonadFoldMapWithIndex maps each element with its index to a monoid value and combines them.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/string"
// seq := From("a", "b", "c")
// result := MonadFoldMapWithIndex(seq, func(i int, s string) string {
// return fmt.Sprintf("%d:%s ", i, s)
// }, string.Monoid)
// // returns: "0:a 1:b 2:c "
//
//go:inline
func MonadFoldMapWithIndex[A, B any](fa Seq[A], f func(int, A) B, m M.Monoid[B]) B {
return MonadReduceWithIndex(fa, func(i int, b B, a A) B {
return m.Concat(b, f(i, a))
}, m.Empty())
}
// FoldMapWithIndex returns a function that maps with index and folds.
// This is the curried version of MonadFoldMapWithIndex.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/string"
// indexedStringify := FoldMapWithIndex(string.Monoid)(func(i int, s string) string {
// return fmt.Sprintf("%d:%s ", i, s)
// })
// seq := From("a", "b", "c")
// result := indexedStringify(seq)
// // returns: "0:a 1:b 2:c "
//
//go:inline
func FoldMapWithIndex[A, B any](m M.Monoid[B]) func(func(int, A) B) func(Seq[A]) B {
return func(f func(int, A) B) func(Seq[A]) B {
return func(as Seq[A]) B {
return MonadFoldMapWithIndex(as, f, m)
}
}
}
// MonadFoldMapWithKey maps each key-value pair to a monoid value and combines them.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/string"
// seq := Of2("x", 10)
// result := MonadFoldMapWithKey(seq, func(k string, v int) string {
// return fmt.Sprintf("%s:%d ", k, v)
// }, string.Monoid)
// // returns: "x:10 "
//
//go:inline
func MonadFoldMapWithKey[K, A, B any](fa Seq2[K, A], f func(K, A) B, m M.Monoid[B]) B {
return MonadReduceWithKey(fa, func(k K, b B, a A) B {
return m.Concat(b, f(k, a))
}, m.Empty())
}
// FoldMapWithKey returns a function that maps with key and folds.
// This is the curried version of MonadFoldMapWithKey.
//
//go:inline
func FoldMapWithKey[K, A, B any](m M.Monoid[B]) func(func(K, A) B) func(Seq2[K, A]) B {
return func(f func(K, A) B) func(Seq2[K, A]) B {
return func(as Seq2[K, A]) B {
return MonadFoldMapWithKey(as, f, m)
}
}
}
// MonadFlap applies a fixed value to a sequence of functions.
// This is the dual of MonadAp.
//
// Example:
//
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
// result := MonadFlap(fns, 5)
// // yields: 10, 15
//
//go:inline
func MonadFlap[B, A any](fab Seq[func(A) B], a A) Seq[B] {
return functor.MonadFlap(MonadMap[func(A) B, B], fab, a)
}
// Flap returns a function that applies a fixed value to functions.
// This is the curried version of MonadFlap.
//
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return functor.Flap(Map[func(A) B, B], a)
}
// Prepend returns a function that adds an element to the beginning of a sequence.
//
// Example:
//
// seq := From(2, 3, 4)
// result := Prepend(1)(seq)
// // yields: 1, 2, 3, 4
//
//go:inline
func Prepend[A any](head A) Operator[A, A] {
return G.Prepend[Seq[A]](head)
}
// Append returns a function that adds an element to the end of a sequence.
//
// Example:
//
// seq := From(1, 2, 3)
// result := Append(4)(seq)
// // yields: 1, 2, 3, 4
//
//go:inline
func Append[A any](tail A) Operator[A, A] {
return G.Append[Seq[A]](tail)
}
// MonadZip combines two sequences into a sequence of pairs.
// The resulting sequence stops when either input sequence is exhausted.
//
// Example:
//
// seqA := From(1, 2, 3)
// seqB := From("a", "b")
// result := MonadZip(seqB, seqA)
// // yields: (1, "a"), (2, "b")
func MonadZip[A, B any](fb Seq[B], fa Seq[A]) Seq2[A, B] {
return func(yield func(A, B) bool) {
na, sa := I.Pull(fa)
defer sa()
for b := range fb {
a, ok := na()
if !ok {
return
}
if !yield(a, b) {
return
}
}
}
}
// Zip returns a function that zips a sequence with another sequence.
// This is the curried version of MonadZip.
//
// Example:
//
// seqA := From(1, 2, 3)
// zipWithA := Zip(seqA)
// seqB := From("a", "b", "c")
// result := zipWithA(seqB)
// // yields: (1, "a"), (2, "b"), (3, "c")
//
//go:inline
func Zip[A, B any](fa Seq[A]) func(Seq[B]) Seq2[A, B] {
return F.Bind2nd(MonadZip[A, B], fa)
}

View File

@@ -0,0 +1,588 @@
// 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"
"maps"
"slices"
"strings"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// Helper function to collect sequence into a slice
func toSlice[T any](seq Seq[T]) []T {
return slices.Collect(seq)
}
// Helper function to collect Seq2 into a map
func toMap[K comparable, V any](seq Seq2[K, V]) map[K]V {
return maps.Collect(seq)
}
func TestOf(t *testing.T) {
seq := Of(42)
result := toSlice(seq)
assert.Equal(t, []int{42}, result)
}
func TestOf2(t *testing.T) {
seq := Of2("key", 100)
result := toMap(seq)
assert.Equal(t, map[string]int{"key": 100}, result)
}
func TestFrom(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(seq)
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
}
func TestEmpty(t *testing.T) {
seq := Empty[int]()
result := toSlice(seq)
assert.Empty(t, result)
}
func TestMonadMap(t *testing.T) {
seq := From(1, 2, 3)
doubled := MonadMap(seq, func(x int) int { return x * 2 })
result := toSlice(doubled)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMap(t *testing.T) {
seq := From(1, 2, 3)
double := Map(func(x int) int { return x * 2 })
result := toSlice(double(seq))
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMonadMapWithIndex(t *testing.T) {
seq := From("a", "b", "c")
indexed := MonadMapWithIndex(seq, func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := toSlice(indexed)
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result)
}
func TestMapWithIndex(t *testing.T) {
seq := From("a", "b", "c")
indexer := MapWithIndex(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := toSlice(indexer(seq))
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result)
}
func TestMonadMapWithKey(t *testing.T) {
seq := Of2("x", 10)
doubled := MonadMapWithKey(seq, func(k string, v int) int { return v * 2 })
result := toMap(doubled)
assert.Equal(t, map[string]int{"x": 20}, result)
}
func TestMapWithKey(t *testing.T) {
seq := Of2("x", 10)
doubler := MapWithKey(func(k string, v int) int { return v * 2 })
result := toMap(doubler(seq))
assert.Equal(t, map[string]int{"x": 20}, result)
}
func TestMonadFilter(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
result := toSlice(evens)
assert.Equal(t, []int{2, 4}, result)
}
func TestFilter(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
isEven := Filter(func(x int) bool { return x%2 == 0 })
result := toSlice(isEven(seq))
assert.Equal(t, []int{2, 4}, result)
}
func TestMonadFilterWithIndex(t *testing.T) {
seq := From("a", "b", "c", "d")
oddIndices := MonadFilterWithIndex(seq, func(i int, _ string) bool { return i%2 == 1 })
result := toSlice(oddIndices)
assert.Equal(t, []string{"b", "d"}, result)
}
func TestFilterWithIndex(t *testing.T) {
seq := From("a", "b", "c", "d")
oddIndexFilter := FilterWithIndex(func(i int, _ string) bool { return i%2 == 1 })
result := toSlice(oddIndexFilter(seq))
assert.Equal(t, []string{"b", "d"}, result)
}
func TestMonadFilterWithKey(t *testing.T) {
seq := Of2("x", 10)
filtered := MonadFilterWithKey(seq, func(k string, v int) bool { return v > 5 })
result := toMap(filtered)
assert.Equal(t, map[string]int{"x": 10}, result)
seq2 := Of2("y", 3)
filtered2 := MonadFilterWithKey(seq2, func(k string, v int) bool { return v > 5 })
result2 := toMap(filtered2)
assert.Equal(t, map[string]int{}, result2)
}
func TestFilterWithKey(t *testing.T) {
seq := Of2("x", 10)
filter := FilterWithKey(func(k string, v int) bool { return v > 5 })
result := toMap(filter(seq))
assert.Equal(t, map[string]int{"x": 10}, result)
}
func TestMonadFilterMap(t *testing.T) {
seq := From(1, 2, 3, 4)
result := MonadFilterMap(seq, func(x int) Option[int] {
if x%2 == 0 {
return O.Some(x * 10)
}
return O.None[int]()
})
assert.Equal(t, []int{20, 40}, toSlice(result))
}
func TestFilterMap(t *testing.T) {
seq := From(1, 2, 3, 4)
filterMapper := FilterMap(func(x int) Option[int] {
if x%2 == 0 {
return O.Some(x * 10)
}
return O.None[int]()
})
result := toSlice(filterMapper(seq))
assert.Equal(t, []int{20, 40}, result)
}
func TestMonadFilterMapWithIndex(t *testing.T) {
seq := From("a", "b", "c")
result := MonadFilterMapWithIndex(seq, func(i int, s string) Option[string] {
if i%2 == 0 {
return O.Some(strings.ToUpper(s))
}
return O.None[string]()
})
assert.Equal(t, []string{"A", "C"}, toSlice(result))
}
func TestFilterMapWithIndex(t *testing.T) {
seq := From("a", "b", "c")
filterMapper := FilterMapWithIndex(func(i int, s string) Option[string] {
if i%2 == 0 {
return O.Some(strings.ToUpper(s))
}
return O.None[string]()
})
result := toSlice(filterMapper(seq))
assert.Equal(t, []string{"A", "C"}, result)
}
func TestMonadFilterMapWithKey(t *testing.T) {
seq := Of2("x", 10)
result := MonadFilterMapWithKey(seq, func(k string, v int) Option[int] {
if v > 5 {
return O.Some(v * 2)
}
return O.None[int]()
})
assert.Equal(t, map[string]int{"x": 20}, toMap(result))
}
func TestFilterMapWithKey(t *testing.T) {
seq := Of2("x", 10)
filterMapper := FilterMapWithKey(func(k string, v int) Option[int] {
if v > 5 {
return O.Some(v * 2)
}
return O.None[int]()
})
result := toMap(filterMapper(seq))
assert.Equal(t, map[string]int{"x": 20}, result)
}
func TestMonadChain(t *testing.T) {
seq := From(1, 2)
result := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
assert.Equal(t, []int{1, 10, 2, 20}, toSlice(result))
}
func TestChain(t *testing.T) {
seq := From(1, 2)
chainer := Chain(func(x int) Seq[int] {
return From(x, x*10)
})
result := toSlice(chainer(seq))
assert.Equal(t, []int{1, 10, 2, 20}, result)
}
func TestFlatten(t *testing.T) {
seq := From(From(1, 2), From(3, 4))
result := Flatten(seq)
assert.Equal(t, []int{1, 2, 3, 4}, toSlice(result))
}
func TestMonadAp(t *testing.T) {
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
vals := From(1, 2)
result := MonadAp(fns, vals)
assert.Equal(t, []int{2, 4, 11, 12}, toSlice(result))
}
func TestAp(t *testing.T) {
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
vals := From(1, 2)
applier := Ap[int](vals)
result := toSlice(applier(fns))
assert.Equal(t, []int{2, 4, 11, 12}, result)
}
func TestApCurried(t *testing.T) {
f := F.Curry3(func(s1 string, n int, s2 string) string {
return fmt.Sprintf("%s-%d-%s", s1, n, s2)
})
result := F.Pipe4(
Of(f),
Ap[func(int) func(string) string](From("a", "b")),
Ap[func(string) string](From(1, 2)),
Ap[string](From("c", "d")),
toSlice[string],
)
expected := []string{"a-1-c", "a-1-d", "a-2-c", "a-2-d", "b-1-c", "b-1-d", "b-2-c", "b-2-d"}
assert.Equal(t, expected, result)
}
func TestMakeBy(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i * i })
result := toSlice(seq)
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
}
func TestMakeByZero(t *testing.T) {
seq := MakeBy(0, func(i int) int { return i })
result := toSlice(seq)
assert.Empty(t, result)
}
func TestMakeByNegative(t *testing.T) {
seq := MakeBy(-5, func(i int) int { return i })
result := toSlice(seq)
assert.Empty(t, result)
}
func TestReplicate(t *testing.T) {
seq := Replicate(3, "hello")
result := toSlice(seq)
assert.Equal(t, []string{"hello", "hello", "hello"}, result)
}
func TestMonadReduce(t *testing.T) {
seq := From(1, 2, 3, 4)
sum := MonadReduce(seq, func(acc, x int) int { return acc + x }, 0)
assert.Equal(t, 10, sum)
}
func TestReduce(t *testing.T) {
seq := From(1, 2, 3, 4)
sum := Reduce(func(acc, x int) int { return acc + x }, 0)
result := sum(seq)
assert.Equal(t, 10, result)
}
func TestMonadReduceWithIndex(t *testing.T) {
seq := From(10, 20, 30)
result := MonadReduceWithIndex(seq, func(i, acc, x int) int {
return acc + (i * x)
}, 0)
// 0*10 + 1*20 + 2*30 = 0 + 20 + 60 = 80
assert.Equal(t, 80, result)
}
func TestReduceWithIndex(t *testing.T) {
seq := From(10, 20, 30)
reducer := ReduceWithIndex(func(i, acc, x int) int {
return acc + (i * x)
}, 0)
result := reducer(seq)
assert.Equal(t, 80, result)
}
func TestMonadReduceWithKey(t *testing.T) {
seq := Of2("x", 10)
result := MonadReduceWithKey(seq, func(k string, acc, v int) int {
return acc + v
}, 0)
assert.Equal(t, 10, result)
}
func TestReduceWithKey(t *testing.T) {
seq := Of2("x", 10)
reducer := ReduceWithKey(func(k string, acc, v int) int {
return acc + v
}, 0)
result := reducer(seq)
assert.Equal(t, 10, result)
}
func TestMonadFold(t *testing.T) {
seq := From("Hello", " ", "World")
result := MonadFold(seq, S.Monoid)
assert.Equal(t, "Hello World", result)
}
func TestFold(t *testing.T) {
seq := From("Hello", " ", "World")
folder := Fold(S.Monoid)
result := folder(seq)
assert.Equal(t, "Hello World", result)
}
func TestMonadFoldMap(t *testing.T) {
seq := From(1, 2, 3)
result := MonadFoldMap(seq, func(x int) string {
return fmt.Sprintf("%d", x)
}, S.Monoid)
assert.Equal(t, "123", result)
}
func TestFoldMap(t *testing.T) {
seq := From(1, 2, 3)
folder := FoldMap[int](S.Monoid)(func(x int) string {
return fmt.Sprintf("%d", x)
})
result := folder(seq)
assert.Equal(t, "123", result)
}
func TestMonadFoldMapWithIndex(t *testing.T) {
seq := From("a", "b", "c")
result := MonadFoldMapWithIndex(seq, func(i int, s string) string {
return fmt.Sprintf("%d:%s ", i, s)
}, S.Monoid)
assert.Equal(t, "0:a 1:b 2:c ", result)
}
func TestFoldMapWithIndex(t *testing.T) {
seq := From("a", "b", "c")
folder := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s ", i, s)
})
result := folder(seq)
assert.Equal(t, "0:a 1:b 2:c ", result)
}
func TestMonadFoldMapWithKey(t *testing.T) {
seq := Of2("x", 10)
result := MonadFoldMapWithKey(seq, func(k string, v int) string {
return fmt.Sprintf("%s:%d ", k, v)
}, S.Monoid)
assert.Equal(t, "x:10 ", result)
}
func TestFoldMapWithKey(t *testing.T) {
seq := Of2("x", 10)
folder := FoldMapWithKey[string, int](S.Monoid)(func(k string, v int) string {
return fmt.Sprintf("%s:%d ", k, v)
})
result := folder(seq)
assert.Equal(t, "x:10 ", result)
}
func TestMonadFlap(t *testing.T) {
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
result := MonadFlap(fns, 5)
assert.Equal(t, []int{10, 15}, toSlice(result))
}
func TestFlap(t *testing.T) {
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
flapper := Flap[int](5)
result := toSlice(flapper(fns))
assert.Equal(t, []int{10, 15}, result)
}
func TestPrepend(t *testing.T) {
seq := From(2, 3, 4)
result := Prepend(1)(seq)
assert.Equal(t, []int{1, 2, 3, 4}, toSlice(result))
}
func TestAppend(t *testing.T) {
seq := From(1, 2, 3)
result := Append(4)(seq)
assert.Equal(t, []int{1, 2, 3, 4}, toSlice(result))
}
func TestMonadZip(t *testing.T) {
seqA := From(1, 2, 3)
seqB := From("a", "b")
result := MonadZip(seqB, seqA)
var pairs []string
for a, b := range result {
pairs = append(pairs, fmt.Sprintf("%d:%s", a, b))
}
assert.Equal(t, []string{"1:a", "2:b"}, pairs)
}
func TestZip(t *testing.T) {
seqA := From(1, 2, 3)
seqB := From("a", "b", "c")
zipWithA := Zip[int, string](seqA)
result := zipWithA(seqB)
var pairs []string
for a, b := range result {
pairs = append(pairs, fmt.Sprintf("%d:%s", a, b))
}
assert.Equal(t, []string{"1:a", "2:b", "3:c"}, pairs)
}
func TestMonoid(t *testing.T) {
m := Monoid[int]()
seq1 := From(1, 2)
seq2 := From(3, 4)
result := m.Concat(seq1, seq2)
assert.Equal(t, []int{1, 2, 3, 4}, toSlice(result))
}
func TestMonoidEmpty(t *testing.T) {
m := Monoid[int]()
empty := m.Empty()
assert.Empty(t, toSlice(empty))
}
func TestMonoidAssociativity(t *testing.T) {
m := Monoid[int]()
seq1 := From(1, 2)
seq2 := From(3, 4)
seq3 := From(5, 6)
// (seq1 + seq2) + seq3
left := m.Concat(m.Concat(seq1, seq2), seq3)
// seq1 + (seq2 + seq3)
right := m.Concat(seq1, m.Concat(seq2, seq3))
assert.Equal(t, toSlice(left), toSlice(right))
}
func TestMonoidIdentity(t *testing.T) {
m := Monoid[int]()
seq := From(1, 2, 3)
empty := m.Empty()
// seq + empty = seq
leftIdentity := m.Concat(seq, empty)
assert.Equal(t, []int{1, 2, 3}, toSlice(leftIdentity))
// empty + seq = seq
rightIdentity := m.Concat(empty, seq)
assert.Equal(t, []int{1, 2, 3}, toSlice(rightIdentity))
}
func TestPipelineComposition(t *testing.T) {
// Test a complex pipeline
result := F.Pipe4(
From(1, 2, 3, 4, 5, 6),
Filter(func(x int) bool { return x%2 == 0 }),
Map(func(x int) int { return x * 10 }),
Prepend(0),
toSlice[int],
)
assert.Equal(t, []int{0, 20, 40, 60}, result)
}
func TestLazyEvaluation(t *testing.T) {
// Test that operations are lazy
callCount := 0
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, func(x int) int {
callCount++
return x * 2
})
// No calls yet since we haven't iterated
assert.Equal(t, 0, callCount)
// Iterate only first 2 elements
count := 0
for range mapped {
count++
if count == 2 {
break
}
}
// Should have called the function only twice
assert.Equal(t, 2, callCount)
}
func ExampleFoldMap() {
seq := From("a", "b", "c")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(seq)
fmt.Println(result)
// Output: ABC
}
func ExampleChain() {
seq := From(1, 2)
result := F.Pipe2(
seq,
Chain(func(x int) Seq[int] {
return From(x, x*10)
}),
toSlice[int],
)
fmt.Println(result)
// Output: [1 10 2 20]
}
func ExampleMonoid() {
m := Monoid[int]()
seq1 := From(1, 2, 3)
seq2 := From(4, 5, 6)
combined := m.Concat(seq1, seq2)
result := toSlice(combined)
fmt.Println(result)
// Output: [1 2 3 4 5 6]
}

37
v2/iterator/iter/monid.go Normal file
View File

@@ -0,0 +1,37 @@
// 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 (
G "github.com/IBM/fp-go/v2/internal/iter"
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a Monoid instance for Seq[T].
// The monoid's concat operation concatenates sequences, and the empty value is an empty sequence.
//
// Example:
//
// m := Monoid[int]()
// seq1 := From(1, 2)
// seq2 := From(3, 4)
// result := m.Concat(seq1, seq2)
// // yields: 1, 2, 3, 4
//
//go:inline
func Monoid[T any]() M.Monoid[Seq[T]] {
return M.MakeMonoid(G.Concat[Seq[T]], Empty[T]())
}

57
v2/iterator/iter/types.go Normal file
View File

@@ -0,0 +1,57 @@
// 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 (
I "iter"
"github.com/IBM/fp-go/v2/iterator/stateless"
"github.com/IBM/fp-go/v2/optics/lens/option"
"github.com/IBM/fp-go/v2/predicate"
)
type (
// Option represents an optional value, either Some(value) or None.
Option[A any] = option.Option[A]
// Seq is a single-value iterator sequence from Go 1.23+.
// It represents a lazy sequence of values that can be iterated using range.
Seq[T any] = I.Seq[T]
// Seq2 is a key-value iterator sequence from Go 1.23+.
// It represents a lazy sequence of key-value pairs that can be iterated using range.
Seq2[K, V any] = I.Seq2[K, V]
// Iterator is a stateless iterator type.
Iterator[T any] = stateless.Iterator[T]
// Predicate is a function that tests a value and returns a boolean.
Predicate[T any] = predicate.Predicate[T]
// Kleisli represents a function that takes a value and returns a sequence.
// This is the monadic bind operation for sequences.
Kleisli[A, B any] = func(A) Seq[B]
// Kleisli2 represents a function that takes a value and returns a key-value sequence.
Kleisli2[K, A, B any] = func(A) Seq2[K, B]
// Operator represents a transformation from one sequence to another.
// It's a function that takes a Seq[A] and returns a Seq[B].
Operator[A, B any] = Kleisli[Seq[A], B]
// Operator2 represents a transformation from one key-value sequence to another.
Operator2[K, A, B any] = Kleisli2[K, Seq2[K, A], B]
)

View File

@@ -21,6 +21,6 @@ import (
// Any returns `true` if any element of the iterable is `true`. If the iterable is empty, return `false`
// Similar to the [https://docs.python.org/3/library/functions.html#any] function
func Any[U any](pred func(U) bool) func(ma Iterator[U]) bool {
func Any[U any](pred Predicate[U]) Predicate[Iterator[U]] {
return G.Any[Iterator[U]](pred)
}

View File

@@ -72,7 +72,7 @@ func Do[S any](
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Kleisli[Iterator[S1], S2] {
) Operator[S1, S2] {
return G.Bind[Iterator[S1], Iterator[S2]](setter, f)
}
@@ -80,7 +80,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Kleisli[Iterator[S1], S2] {
) Operator[S1, S2] {
return G.Let[Iterator[S1], Iterator[S2]](setter, f)
}
@@ -88,14 +88,14 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Kleisli[Iterator[S1], S2] {
) Operator[S1, S2] {
return G.LetTo[Iterator[S1], Iterator[S2]](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) Kleisli[Iterator[T], S1] {
) Operator[T, S1] {
return G.BindTo[Iterator[S1], Iterator[T]](setter)
}
@@ -135,6 +135,6 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Iterator[T],
) Kleisli[Iterator[S1], S2] {
) Operator[S1, S2] {
return G.ApS[Iterator[func(T) S2], Iterator[S1], Iterator[S2]](setter, fa)
}

View File

@@ -17,11 +17,10 @@ package stateless
import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
P "github.com/IBM/fp-go/v2/pair"
)
// Compress returns an [Iterator] that filters elements from a data [Iterator] returning only those that have a corresponding element in selector [Iterator] that evaluates to `true`.
// Stops when either the data or selectors iterator has been exhausted.
func Compress[U any](sel Iterator[bool]) Kleisli[Iterator[U], U] {
return G.Compress[Iterator[U], Iterator[bool], Iterator[P.Pair[U, bool]]](sel)
func Compress[U any](sel Iterator[bool]) Operator[U, U] {
return G.Compress[Iterator[U], Iterator[bool], Iterator[Pair[U, bool]]](sel)
}

View File

@@ -21,6 +21,6 @@ import (
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
// Note, the [Iterator] does not produce any output until the predicate first becomes false
func DropWhile[U any](pred func(U) bool) Kleisli[Iterator[U], U] {
func DropWhile[U any](pred Predicate[U]) Operator[U, U] {
return G.DropWhile[Iterator[U]](pred)
}

View File

@@ -17,10 +17,9 @@ package stateless
import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
O "github.com/IBM/fp-go/v2/option"
)
// First returns the first item in an iterator if such an item exists
func First[U any](mu Iterator[U]) O.Option[U] {
func First[U any](mu Iterator[U]) Option[U] {
return G.First(mu)
}

View File

@@ -18,11 +18,10 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
)
// Any returns `true` if any element of the iterable is `true`. If the iterable is empty, return `false`
func Any[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) bool, U any](pred FCT) func(ma GU) bool {
func Any[GU ~func() Option[Pair[GU, U]], FCT ~Predicate[U], U any](pred FCT) func(ma GU) bool {
return F.Flow3(
Filter[GU](pred),
First[GU],

View File

@@ -19,8 +19,6 @@ import (
"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"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
@@ -33,7 +31,7 @@ import (
// Y int
// }
// result := generic.Do[Iterator[State]](State{})
func Do[GS ~func() O.Option[P.Pair[GS, S]], S any](
func Do[GS ~func() Option[Pair[GS, S]], S any](
empty S,
) GS {
return Of[GS](empty)
@@ -73,7 +71,7 @@ func Do[GS ~func() O.Option[P.Pair[GS, S]], S any](
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
func Bind[GS1 ~func() Option[Pair[GS1, S1]], GS2 ~func() Option[Pair[GS2, S2]], GA ~func() Option[Pair[GA, A]], S1, S2, A any](
setter func(A) func(S1) S2,
f func(S1) GA,
) func(GS1) GS2 {
@@ -87,7 +85,7 @@ func Bind[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
func Let[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], S1, S2, A any](
func Let[GS1 ~func() Option[Pair[GS1, S1]], GS2 ~func() Option[Pair[GS2, S2]], S1, S2, A any](
key func(A) func(S1) S2,
f func(S1) A,
) func(GS1) GS2 {
@@ -99,7 +97,7 @@ func Let[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2,
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
func LetTo[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], S1, S2, B any](
func LetTo[GS1 ~func() Option[Pair[GS1, S1]], GS2 ~func() Option[Pair[GS2, S2]], S1, S2, B any](
key func(B) func(S1) S2,
b B,
) func(GS1) GS2 {
@@ -111,7 +109,7 @@ func LetTo[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[GS1 ~func() O.Option[P.Pair[GS1, S1]], GA ~func() O.Option[P.Pair[GA, A]], S1, A any](
func BindTo[GS1 ~func() Option[Pair[GS1, S1]], GA ~func() Option[Pair[GA, A]], S1, A any](
setter func(A) S1,
) func(GA) GS1 {
return C.BindTo(
@@ -153,7 +151,7 @@ func BindTo[GS1 ~func() O.Option[P.Pair[GS1, S1]], GA ~func() O.Option[P.Pair[GA
// yIter,
// ),
// ) // Produces: {1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}, {3,"a"}, {3,"b"}
func ApS[GAS2 ~func() O.Option[P.Pair[GAS2, func(A) S2]], GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
func ApS[GAS2 ~func() Option[Pair[GAS2, func(A) S2]], GS1 ~func() Option[Pair[GS1, S1]], GS2 ~func() Option[Pair[GS2, S2]], GA ~func() Option[Pair[GA, A]], S1, S2, A any](
setter func(A) func(S1) S2,
fa GA,
) func(GS1) GS2 {

View File

@@ -23,7 +23,7 @@ import (
// Compress returns an [Iterator] that filters elements from a data [Iterator] returning only those that have a corresponding element in selector [Iterator] that evaluates to `true`.
// Stops when either the data or selectors iterator has been exhausted.
func Compress[GU ~func() O.Option[P.Pair[GU, U]], GB ~func() O.Option[P.Pair[GB, bool]], CS ~func() O.Option[P.Pair[CS, P.Pair[U, bool]]], U any](sel GB) func(GU) GU {
func Compress[GU ~func() Option[Pair[GU, U]], GB ~func() Option[Pair[GB, bool]], CS ~func() Option[Pair[CS, Pair[U, bool]]], U any](sel GB) func(GU) GU {
return F.Flow2(
Zip[GU, GB, CS](sel),
FilterMap[GU, CS](F.Flow2(

View File

@@ -21,9 +21,9 @@ import (
P "github.com/IBM/fp-go/v2/pair"
)
func Cycle[GU ~func() O.Option[P.Pair[GU, U]], U any](ma GU) GU {
func Cycle[GU ~func() Option[Pair[GU, U]], U any](ma GU) GU {
// avoid cyclic references
var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GU, U]]
var m func(Option[Pair[GU, U]]) Option[Pair[GU, U]]
recurse := func(mu GU) GU {
return F.Nullary2(
@@ -32,11 +32,11 @@ func Cycle[GU ~func() O.Option[P.Pair[GU, U]], U any](ma GU) GU {
)
}
m = O.Fold(func() O.Option[P.Pair[GU, U]] {
m = O.Fold(func() Option[Pair[GU, U]] {
return recurse(ma)()
}, F.Flow2(
P.BiMap(recurse, F.Identity[U]),
O.Of[P.Pair[GU, U]],
O.Of[Pair[GU, U]],
))
return recurse(ma)

View File

@@ -24,9 +24,9 @@ import (
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
// Note, the [Iterator] does not produce any output until the predicate first becomes false
func DropWhile[GU ~func() O.Option[P.Pair[GU, U]], U any](pred func(U) bool) func(GU) GU {
func DropWhile[GU ~func() Option[Pair[GU, U]], U any](pred Predicate[U]) func(GU) GU {
// avoid cyclic references
var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GU, U]]
var m func(Option[Pair[GU, U]]) Option[Pair[GU, U]]
fromPred := O.FromPredicate(PR.Not(PR.ContraMap(P.Tail[GU, U])(pred)))
@@ -37,11 +37,11 @@ func DropWhile[GU ~func() O.Option[P.Pair[GU, U]], U any](pred func(U) bool) fun
)
}
m = O.Chain(func(t P.Pair[GU, U]) O.Option[P.Pair[GU, U]] {
m = O.Chain(func(t Pair[GU, U]) Option[Pair[GU, U]] {
return F.Pipe2(
t,
fromPred,
O.Fold(recurse(Next(t)), O.Of[P.Pair[GU, U]]),
O.Fold(recurse(Next(t)), O.Of[Pair[GU, U]]),
)
})

View File

@@ -22,7 +22,7 @@ import (
)
// First returns the first item in an iterator if such an item exists
func First[GU ~func() O.Option[P.Pair[GU, U]], U any](mu GU) O.Option[U] {
func First[GU ~func() Option[Pair[GU, U]], U any](mu GU) Option[U] {
return F.Pipe1(
mu(),
O.Map(P.Tail[GU, U]),

View File

@@ -23,12 +23,12 @@ import (
)
// FromLazy returns an iterator on top of a lazy function
func FromLazy[GU ~func() O.Option[P.Pair[GU, U]], LZ ~func() U, U any](l LZ) GU {
func FromLazy[GU ~func() Option[Pair[GU, U]], LZ ~func() U, U any](l LZ) GU {
return F.Pipe1(
l,
L.Map[LZ, GU](F.Flow2(
F.Bind1st(P.MakePair[GU, U], Empty[GU]()),
O.Of[P.Pair[GU, U]],
O.Of[Pair[GU, U]],
)),
)
}

View File

@@ -27,42 +27,42 @@ import (
P "github.com/IBM/fp-go/v2/pair"
)
// Next returns the iterator for the next element in an iterator `P.Pair`
func Next[GU ~func() O.Option[P.Pair[GU, U]], U any](m P.Pair[GU, U]) GU {
// Next returns the iterator for the next element in an iterator `Pair`
func Next[GU ~func() Option[Pair[GU, U]], U any](m Pair[GU, U]) GU {
return P.Head(m)
}
// Current returns the current element in an iterator `P.Pair`
func Current[GU ~func() O.Option[P.Pair[GU, U]], U any](m P.Pair[GU, U]) U {
// Current returns the current element in an iterator `Pair`
func Current[GU ~func() Option[Pair[GU, U]], U any](m Pair[GU, U]) U {
return P.Tail(m)
}
// From constructs an array from a set of variadic arguments
func From[GU ~func() O.Option[P.Pair[GU, U]], U any](data ...U) GU {
func From[GU ~func() Option[Pair[GU, U]], U any](data ...U) GU {
return FromArray[GU](data)
}
// Empty returns the empty iterator
func Empty[GU ~func() O.Option[P.Pair[GU, U]], U any]() GU {
func Empty[GU ~func() Option[Pair[GU, U]], U any]() GU {
return IO.None[GU]()
}
// Of returns an iterator with one single element
func Of[GU ~func() O.Option[P.Pair[GU, U]], U any](a U) GU {
func Of[GU ~func() Option[Pair[GU, U]], U any](a U) GU {
return IO.Of[GU](P.MakePair(Empty[GU](), a))
}
// FromArray returns an iterator from multiple elements
func FromArray[GU ~func() O.Option[P.Pair[GU, U]], US ~[]U, U any](as US) GU {
func FromArray[GU ~func() Option[Pair[GU, U]], US ~[]U, U any](as US) GU {
return A.MatchLeft(Empty[GU], func(head U, tail US) GU {
return func() O.Option[P.Pair[GU, U]] {
return func() Option[Pair[GU, U]] {
return O.Of(P.MakePair(FromArray[GU](tail), head))
}
})(as)
}
// reduce applies a function for each value of the iterator with a floating result
func reduce[GU ~func() O.Option[P.Pair[GU, U]], U, V any](as GU, f func(V, U) V, initial V) V {
func reduce[GU ~func() Option[Pair[GU, U]], U, V any](as GU, f func(V, U) V, initial V) V {
next, ok := O.Unwrap(as())
current := initial
for ok {
@@ -74,18 +74,18 @@ func reduce[GU ~func() O.Option[P.Pair[GU, U]], U, V any](as GU, f func(V, U) V,
}
// Reduce applies a function for each value of the iterator with a floating result
func Reduce[GU ~func() O.Option[P.Pair[GU, U]], U, V any](f func(V, U) V, initial V) func(GU) V {
func Reduce[GU ~func() Option[Pair[GU, U]], U, V any](f func(V, U) V, initial V) func(GU) V {
return F.Bind23of3(reduce[GU, U, V])(f, initial)
}
// ToArray converts the iterator to an array
func ToArray[GU ~func() O.Option[P.Pair[GU, U]], US ~[]U, U any](u GU) US {
func ToArray[GU ~func() Option[Pair[GU, U]], US ~[]U, U any](u GU) US {
return Reduce[GU](A.Append[US], A.Empty[US]())(u)
}
func Map[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) V, U, V any](f FCT) func(ma GU) GV {
func Map[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], FCT ~func(U) V, U, V any](f FCT) func(ma GU) GV {
// pre-declare to avoid cyclic reference
var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GV, V]]
var m func(Option[Pair[GU, U]]) Option[Pair[GV, V]]
recurse := func(ma GU) GV {
return F.Nullary2(
@@ -99,12 +99,12 @@ func Map[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]],
return recurse
}
func MonadMap[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU, f func(U) V) GV {
func MonadMap[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], U, V any](ma GU, f func(U) V) GV {
return Map[GV, GU](f)(ma)
}
func concat[GU ~func() O.Option[P.Pair[GU, U]], U any](right, left GU) GU {
var m func(ma O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GU, U]]
func concat[GU ~func() Option[Pair[GU, U]], U any](right, left GU) GU {
var m func(ma Option[Pair[GU, U]]) Option[Pair[GU, U]]
recurse := func(left GU) GU {
return F.Nullary2(left, m)
@@ -114,15 +114,15 @@ func concat[GU ~func() O.Option[P.Pair[GU, U]], U any](right, left GU) GU {
right,
F.Flow2(
P.BiMap(recurse, F.Identity[U]),
O.Some[P.Pair[GU, U]],
O.Some[Pair[GU, U]],
))
return recurse(left)
}
func Chain[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](f func(U) GV) func(GU) GV {
func Chain[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], U, V any](f func(U) GV) func(GU) GV {
// pre-declare to avoid cyclic reference
var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GV, V]]
var m func(Option[Pair[GU, U]]) Option[Pair[GV, V]]
recurse := func(ma GU) GV {
return F.Nullary2(
@@ -134,7 +134,7 @@ func Chain[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]
F.Flow3(
P.BiMap(recurse, f),
P.Paired(concat[GV]),
func(v GV) O.Option[P.Pair[GV, V]] {
func(v GV) Option[Pair[GV, V]] {
return v()
},
),
@@ -143,11 +143,11 @@ func Chain[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]
return recurse
}
func MonadChain[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU, f func(U) GV) GV {
func MonadChain[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], U, V any](ma GU, f func(U) GV) GV {
return Chain[GV, GU](f)(ma)
}
func MonadChainFirst[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU, f func(U) GV) GU {
func MonadChainFirst[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], U, V any](ma GU, f func(U) GV) GU {
return C.MonadChainFirst(
MonadChain[GU, GU, U, U],
MonadMap[GU, GV, V, U],
@@ -156,7 +156,7 @@ func MonadChainFirst[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.P
)
}
func ChainFirst[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](f func(U) GV) func(GU) GU {
func ChainFirst[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], U, V any](f func(U) GV) func(GU) GU {
return C.ChainFirst(
Chain[GU, GU, U, U],
Map[GU, GV, func(V) U, V, U],
@@ -164,14 +164,14 @@ func ChainFirst[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[G
)
}
func Flatten[GV ~func() O.Option[P.Pair[GV, GU]], GU ~func() O.Option[P.Pair[GU, U]], U any](ma GV) GU {
func Flatten[GV ~func() Option[Pair[GV, GU]], GU ~func() Option[Pair[GU, U]], U any](ma GV) GU {
return MonadChain(ma, F.Identity[GU])
}
// MakeBy returns an [Iterator] with an infinite number of elements initialized with `f(i)`
func MakeBy[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(int) U, U any](f FCT) GU {
func MakeBy[GU ~func() Option[Pair[GU, U]], FCT ~func(int) U, U any](f FCT) GU {
var m func(int) O.Option[P.Pair[GU, U]]
var m func(int) Option[Pair[GU, U]]
recurse := func(i int) GU {
return F.Nullary2(
@@ -186,7 +186,7 @@ func MakeBy[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(int) U, U any](f FCT)
utils.Inc,
recurse),
f),
O.Of[P.Pair[GU, U]],
O.Of[Pair[GU, U]],
)
// bootstrap
@@ -194,13 +194,13 @@ func MakeBy[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(int) U, U any](f FCT)
}
// Replicate creates an infinite [Iterator] containing a value.
func Replicate[GU ~func() O.Option[P.Pair[GU, U]], U any](a U) GU {
func Replicate[GU ~func() Option[Pair[GU, U]], U any](a U) GU {
return MakeBy[GU](F.Constant1[int](a))
}
// Repeat creates an [Iterator] containing a value repeated the specified number of times.
// Alias of [Replicate] combined with [Take]
func Repeat[GU ~func() O.Option[P.Pair[GU, U]], U any](n int, a U) GU {
func Repeat[GU ~func() Option[Pair[GU, U]], U any](n int, a U) GU {
return F.Pipe2(
a,
Replicate[GU],
@@ -209,13 +209,13 @@ func Repeat[GU ~func() O.Option[P.Pair[GU, U]], U any](n int, a U) GU {
}
// Count creates an [Iterator] containing a consecutive sequence of integers starting with the provided start value
func Count[GU ~func() O.Option[P.Pair[GU, int]]](start int) GU {
func Count[GU ~func() Option[Pair[GU, int]]](start int) GU {
return MakeBy[GU](N.Add(start))
}
func FilterMap[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) O.Option[V], U, V any](f FCT) func(ma GU) GV {
func FilterMap[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], FCT ~func(U) Option[V], U, V any](f FCT) func(ma GU) GV {
// pre-declare to avoid cyclic reference
var m func(O.Option[P.Pair[GU, U]]) O.Option[P.Pair[GV, V]]
var m func(Option[Pair[GU, U]]) Option[Pair[GV, V]]
recurse := func(ma GU) GV {
return F.Nullary2(
@@ -226,11 +226,11 @@ func FilterMap[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU
m = O.Fold(
Empty[GV](),
func(t P.Pair[GU, U]) O.Option[P.Pair[GV, V]] {
func(t Pair[GU, U]) Option[Pair[GV, V]] {
r := recurse(Next(t))
return O.MonadFold(f(Current(t)), r, F.Flow2(
F.Bind1st(P.MakePair[GV, V], r),
O.Some[P.Pair[GV, V]],
O.Some[Pair[GV, V]],
))
},
)
@@ -238,26 +238,26 @@ func FilterMap[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU
return recurse
}
func Filter[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) bool, U any](f FCT) func(ma GU) GU {
func Filter[GU ~func() Option[Pair[GU, U]], FCT ~Predicate[U], U any](f FCT) func(ma GU) GU {
return FilterMap[GU, GU](O.FromPredicate(f))
}
func Ap[GUV ~func() O.Option[P.Pair[GUV, func(U) V]], GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](ma GU) func(fab GUV) GV {
func Ap[GUV ~func() Option[Pair[GUV, func(U) V]], GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], U, V any](ma GU) func(fab GUV) GV {
return Chain[GV, GUV](F.Bind1st(MonadMap[GV, GU], ma))
}
func MonadAp[GUV ~func() O.Option[P.Pair[GUV, func(U) V]], GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], U, V any](fab GUV, ma GU) GV {
func MonadAp[GUV ~func() Option[Pair[GUV, func(U) V]], GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], U, V any](fab GUV, ma GU) GV {
return Ap[GUV, GV](ma)(fab)
}
func FilterChain[GVV ~func() O.Option[P.Pair[GVV, GV]], GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) O.Option[GV], U, V any](f FCT) func(ma GU) GV {
func FilterChain[GVV ~func() Option[Pair[GVV, GV]], GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], FCT ~func(U) Option[GV], U, V any](f FCT) func(ma GU) GV {
return F.Flow2(
FilterMap[GVV, GU](f),
Flatten[GVV],
)
}
func FoldMap[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) V, U, V any](m M.Monoid[V]) func(FCT) func(ma GU) V {
func FoldMap[GU ~func() Option[Pair[GU, U]], FCT ~func(U) V, U, V any](m M.Monoid[V]) func(FCT) func(ma GU) V {
return func(f FCT) func(ma GU) V {
return Reduce[GU](func(cur V, a U) V {
return m.Concat(cur, f(a))
@@ -265,6 +265,6 @@ func FoldMap[GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(U) V, U, V any](m M.M
}
}
func Fold[GU ~func() O.Option[P.Pair[GU, U]], U any](m M.Monoid[U]) func(ma GU) U {
func Fold[GU ~func() Option[Pair[GU, U]], U any](m M.Monoid[U]) func(ma GU) U {
return Reduce[GU](m.Concat, m.Empty())
}

View File

@@ -18,10 +18,9 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
)
// Last returns the last item in an iterator if such an item exists
func Last[GU ~func() O.Option[P.Pair[GU, U]], U any](mu GU) O.Option[U] {
return reduce(mu, F.Ignore1of2[O.Option[U]](O.Of[U]), O.None[U]())
func Last[GU ~func() Option[Pair[GU, U]], U any](mu GU) Option[U] {
return reduce(mu, F.Ignore1of2[Option[U]](O.Of[U]), O.None[U]())
}

View File

@@ -17,11 +17,9 @@ package generic
import (
"github.com/IBM/fp-go/v2/internal/monad"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
)
type iteratorMonad[A, B any, GA ~func() O.Option[P.Pair[GA, A]], GB ~func() O.Option[P.Pair[GB, B]], GAB ~func() O.Option[P.Pair[GAB, func(A) B]]] struct{}
type iteratorMonad[A, B any, GA ~func() Option[Pair[GA, A]], GB ~func() Option[Pair[GB, B]], GAB ~func() Option[Pair[GAB, func(A) B]]] struct{}
func (o *iteratorMonad[A, B, GA, GB, GAB]) Of(a A) GA {
return Of[GA](a)
@@ -40,6 +38,6 @@ func (o *iteratorMonad[A, B, GA, GB, GAB]) Ap(fa GA) func(GAB) GB {
}
// Monad implements the monadic operations for iterators
func Monad[A, B any, GA ~func() O.Option[P.Pair[GA, A]], GB ~func() O.Option[P.Pair[GB, B]], GAB ~func() O.Option[P.Pair[GAB, func(A) B]]]() monad.Monad[A, B, GA, GB, GAB] {
func Monad[A, B any, GA ~func() Option[Pair[GA, A]], GB ~func() Option[Pair[GB, B]], GAB ~func() Option[Pair[GAB, func(A) B]]]() monad.Monad[A, B, GA, GB, GAB] {
return &iteratorMonad[A, B, GA, GB, GAB]{}
}

View File

@@ -18,11 +18,9 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
)
func Monoid[GU ~func() O.Option[P.Pair[GU, U]], U any]() M.Monoid[GU] {
func Monoid[GU ~func() Option[Pair[GU, U]], U any]() M.Monoid[GU] {
return M.MakeMonoid(
F.Swap(concat[GU]),
Empty[GU](),

View File

@@ -27,7 +27,7 @@ import (
P "github.com/IBM/fp-go/v2/pair"
)
func FromReflect[GR ~func() O.Option[P.Pair[GR, R.Value]]](val R.Value) GR {
func FromReflect[GR ~func() Option[Pair[GR, R.Value]]](val R.Value) GR {
// recursive callback
var recurse func(idx int) GR
@@ -39,7 +39,7 @@ func FromReflect[GR ~func() O.Option[P.Pair[GR, R.Value]]](val R.Value) GR {
idx,
L.Of[int],
L.Map(fromPred),
LG.Map[L.Lazy[O.Option[int]], GR](O.Map(
LG.Map[Lazy[Option[int]], GR](O.Map(
F.Flow2(
P.Of[int],
P.BiMap(F.Flow2(N.Add(1), recurse), val.Index),

View File

@@ -21,11 +21,11 @@ import (
P "github.com/IBM/fp-go/v2/pair"
)
func apTuple[A, B any](t P.Pair[func(A) B, A]) P.Pair[B, A] {
func apTuple[A, B any](t Pair[func(A) B, A]) Pair[B, A] {
return P.MakePair(P.Head(t)(P.Tail(t)), P.Tail(t))
}
func Scan[GV ~func() O.Option[P.Pair[GV, V]], GU ~func() O.Option[P.Pair[GU, U]], FCT ~func(V, U) V, U, V any](f FCT, initial V) func(ma GU) GV {
func Scan[GV ~func() Option[Pair[GV, V]], GU ~func() Option[Pair[GU, U]], FCT ~func(V, U) V, U, V any](f FCT, initial V) func(ma GU) GV {
// pre-declare to avoid cyclic reference
var m func(GU) func(V) GV

View File

@@ -22,7 +22,7 @@ import (
P "github.com/IBM/fp-go/v2/pair"
)
func Take[GU ~func() O.Option[P.Pair[GU, U]], U any](n int) func(ma GU) GU {
func Take[GU ~func() Option[Pair[GU, U]], U any](n int) func(ma GU) GU {
// pre-declare to avoid cyclic reference
var recurse func(ma GU, idx int) GU

View File

@@ -0,0 +1,15 @@
package generic
import (
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
)
type (
Option[A any] = option.Option[A]
Lazy[A any] = lazy.Lazy[A]
Pair[L, R any] = pair.Pair[L, R]
Predicate[A any] = predicate.Predicate[A]
)

View File

@@ -31,14 +31,14 @@ func addToMap[A comparable](a A, m map[A]bool) map[A]bool {
return cpy
}
func Uniq[AS ~func() O.Option[P.Pair[AS, A]], K comparable, A any](f func(A) K) func(as AS) AS {
func Uniq[AS ~func() Option[Pair[AS, A]], K comparable, A any](f func(A) K) func(as AS) AS {
var recurse func(as AS, mp map[K]bool) AS
recurse = func(as AS, mp map[K]bool) AS {
return F.Nullary2(
as,
O.Chain(func(a P.Pair[AS, A]) O.Option[P.Pair[AS, A]] {
O.Chain(func(a Pair[AS, A]) Option[Pair[AS, A]] {
return F.Pipe3(
P.Tail(a),
f,
@@ -46,7 +46,7 @@ func Uniq[AS ~func() O.Option[P.Pair[AS, A]], K comparable, A any](f func(A) K)
_, ok := mp[k]
return !ok
}),
O.Fold(recurse(P.Head(a), mp), func(k K) O.Option[P.Pair[AS, A]] {
O.Fold(recurse(P.Head(a), mp), func(k K) Option[Pair[AS, A]] {
return O.Of(P.MakePair(recurse(P.Head(a), addToMap(k, mp)), P.Tail(a)))
}),
)
@@ -57,6 +57,6 @@ func Uniq[AS ~func() O.Option[P.Pair[AS, A]], K comparable, A any](f func(A) K)
return F.Bind2nd(recurse, make(map[K]bool, 0))
}
func StrictUniq[AS ~func() O.Option[P.Pair[AS, A]], A comparable](as AS) AS {
func StrictUniq[AS ~func() Option[Pair[AS, A]], A comparable](as AS) AS {
return Uniq[AS](F.Identity[A])(as)
}

View File

@@ -23,12 +23,12 @@ import (
// ZipWith applies a function to pairs of elements at the same index in two iterators, collecting the results in a new iterator. If one
// input iterator is short, excess elements of the longer iterator are discarded.
func ZipWith[AS ~func() O.Option[P.Pair[AS, A]], BS ~func() O.Option[P.Pair[BS, B]], CS ~func() O.Option[P.Pair[CS, C]], FCT ~func(A, B) C, A, B, C any](fa AS, fb BS, f FCT) CS {
func ZipWith[AS ~func() Option[Pair[AS, A]], BS ~func() Option[Pair[BS, B]], CS ~func() Option[Pair[CS, C]], FCT ~func(A, B) C, A, B, C any](fa AS, fb BS, f FCT) CS {
// pre-declare to avoid cyclic reference
var m func(P.Pair[O.Option[P.Pair[AS, A]], O.Option[P.Pair[BS, B]]]) O.Option[P.Pair[CS, C]]
var m func(Pair[Option[Pair[AS, A]], Option[Pair[BS, B]]]) Option[Pair[CS, C]]
recurse := func(as AS, bs BS) CS {
return func() O.Option[P.Pair[CS, C]] {
return func() Option[Pair[CS, C]] {
// combine
return F.Pipe1(
P.MakePair(as(), bs()),
@@ -38,8 +38,8 @@ func ZipWith[AS ~func() O.Option[P.Pair[AS, A]], BS ~func() O.Option[P.Pair[BS,
}
m = F.Flow2(
O.SequencePair[P.Pair[AS, A], P.Pair[BS, B]],
O.Map(func(t P.Pair[P.Pair[AS, A], P.Pair[BS, B]]) P.Pair[CS, C] {
O.SequencePair[Pair[AS, A], Pair[BS, B]],
O.Map(func(t Pair[Pair[AS, A], Pair[BS, B]]) Pair[CS, C] {
return P.MakePair(recurse(P.Head(P.Head(t)), P.Head(P.Tail(t))), f(P.Tail(P.Head(t)), P.Tail(P.Tail(t))))
}))
@@ -49,6 +49,6 @@ func ZipWith[AS ~func() O.Option[P.Pair[AS, A]], BS ~func() O.Option[P.Pair[BS,
// Zip takes two iterators and returns an iterators of corresponding pairs. If one input iterators is short, excess elements of the
// longer iterator are discarded
func Zip[AS ~func() O.Option[P.Pair[AS, A]], BS ~func() O.Option[P.Pair[BS, B]], CS ~func() O.Option[P.Pair[CS, P.Pair[A, B]]], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) P.Pair[A, B]])(fb, P.MakePair[A, B])
func Zip[AS ~func() Option[Pair[AS, A]], BS ~func() Option[Pair[BS, B]], CS ~func() Option[Pair[CS, Pair[A, B]]], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) Pair[A, B]])(fb, P.MakePair[A, B])
}

View File

@@ -16,17 +16,15 @@
package stateless
import (
IO "github.com/IBM/fp-go/v2/io"
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
L "github.com/IBM/fp-go/v2/lazy"
)
// FromLazy returns an [Iterator] on top of a lazy function
func FromLazy[U any](l L.Lazy[U]) Iterator[U] {
func FromLazy[U any](l Lazy[U]) Iterator[U] {
return G.FromLazy[Iterator[U]](l)
}
// FromIO returns an [Iterator] on top of an IO function
func FromIO[U any](io IO.IO[U]) Iterator[U] {
func FromIO[U any](io IO[U]) Iterator[U] {
return G.FromLazy[Iterator[U]](io)
}

View File

@@ -19,23 +19,22 @@ import (
"github.com/IBM/fp-go/v2/iooption"
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
)
// Next returns the [Iterator] for the next element in an iterator [pair.Pair]
func Next[U any](m pair.Pair[Iterator[U], U]) Iterator[U] {
// Next returns the [Iterator] for the next element in an iterator [Pair]
func Next[U any](m Pair[Iterator[U], U]) Iterator[U] {
return pair.Head(m)
}
// Current returns the current element in an [Iterator] [pair.Pair]
func Current[U any](m pair.Pair[Iterator[U], U]) U {
// Current returns the current element in an [Iterator] [Pair]
func Current[U any](m Pair[Iterator[U], U]) U {
return pair.Tail(m)
}
// Empty returns the empty iterator
func Empty[U any]() Iterator[U] {
return iooption.None[pair.Pair[Iterator[U], U]]()
return iooption.None[Pair[Iterator[U], U]]()
}
// Of returns an iterator with one single element
@@ -97,12 +96,12 @@ func Replicate[U any](a U) Iterator[U] {
}
// FilterMap filters and transforms the content of an iterator
func FilterMap[U, V any](f func(U) O.Option[V]) Operator[U, V] {
func FilterMap[U, V any](f func(U) Option[V]) Operator[U, V] {
return G.FilterMap[Iterator[V], Iterator[U]](f)
}
// Filter filters the content of an iterator
func Filter[U any](f func(U) bool) Operator[U, U] {
func Filter[U any](f Predicate[U]) Operator[U, U] {
return G.Filter[Iterator[U]](f)
}
@@ -128,7 +127,7 @@ func Count(start int) Iterator[int] {
}
// FilterChain filters and transforms the content of an iterator
func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) Operator[U, V] {
func FilterChain[U, V any](f func(U) Option[Iterator[V]]) Operator[U, V] {
return G.FilterChain[Iterator[Iterator[V]], Iterator[V], Iterator[U]](f)
}

View File

@@ -17,11 +17,10 @@ package stateless
import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
O "github.com/IBM/fp-go/v2/option"
)
// Last returns the last item in an iterator if such an item exists
// Note that the function will consume the [Iterator] in this call completely, to identify the last element. Do not use this for infinite iterators
func Last[U any](mu Iterator[U]) O.Option[U] {
func Last[U any](mu Iterator[U]) Option[U] {
return G.Last(mu)
}

View File

@@ -22,6 +22,6 @@ import (
// Scan takes an [Iterator] and returns a new [Iterator] of the same length, where the values
// of the new [Iterator] are the result of the application of `f` to the value of the
// source iterator with the previously accumulated value
func Scan[FCT ~func(V, U) V, U, V any](f FCT, initial V) func(ma Iterator[U]) Iterator[V] {
func Scan[FCT ~func(V, U) V, U, V any](f FCT, initial V) Operator[U, V] {
return G.Scan[Iterator[V], Iterator[U]](f, initial)
}

View File

@@ -29,7 +29,7 @@ func TestScan(t *testing.T) {
dst := F.Pipe1(
src,
Scan(func(cur P.Pair[int, string], val string) P.Pair[int, string] {
Scan(func(cur Pair[int, string], val string) Pair[int, string] {
return P.MakePair(P.Head(cur)+1, val)
}, P.MakePair(0, "")),
)

View File

@@ -0,0 +1,29 @@
package stateless
import (
O "github.com/IBM/fp-go/v2/option"
P "github.com/IBM/fp-go/v2/pair"
)
// ToSeq converts the stateless [Iterator] to an idiomatic go iterator
func ToSeq[T any](it Iterator[T]) Seq[T] {
current := Current[T]
return func(yield Predicate[T]) {
next, ok := O.Unwrap(it())
for ok && yield(current(next)) {
next, ok = O.Unwrap(Next(next)())
}
}
}
// ToSeq2 converts the stateless [Iterator] to an idiomatic go iterator
func ToSeq2[K, V any](it Iterator[Pair[K, V]]) Seq2[K, V] {
current := Current[Pair[K, V]]
return func(yield func(K, V) bool) {
yp := P.Paired(yield)
next, ok := O.Unwrap(it())
for ok && yp(current(next)) {
next, ok = O.Unwrap(Next(next)())
}
}
}

View File

@@ -20,6 +20,6 @@ import (
)
// Take limits the number of values in the [Iterator] to a maximum number
func Take[U any](n int) func(ma Iterator[U]) Iterator[U] {
func Take[U any](n int) Operator[U, U] {
return G.Take[Iterator[U]](n)
}

View File

@@ -16,18 +16,29 @@
package stateless
import (
L "github.com/IBM/fp-go/v2/lazy"
"iter"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"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"
)
type (
Option[A any] = option.Option[A]
Option[A any] = option.Option[A]
Lazy[A any] = lazy.Lazy[A]
Pair[L, R any] = pair.Pair[L, R]
Predicate[A any] = predicate.Predicate[A]
IO[A any] = io.IO[A]
// Iterator represents a stateless, pure way to iterate over a sequence
Iterator[U any] L.Lazy[Option[pair.Pair[Iterator[U], U]]]
Iterator[U any] Lazy[Option[Pair[Iterator[U], U]]]
Kleisli[A, B any] = reader.Reader[A, Iterator[B]]
Operator[A, B any] = Kleisli[Iterator[A], B]
Seq[T any] = iter.Seq[T]
Seq2[K, V any] = iter.Seq2[K, V]
)

View File

@@ -27,6 +27,6 @@ func StrictUniq[A comparable](as Iterator[A]) Iterator[A] {
// Uniq converts an [Iterator] of arbitrary items into an [Iterator] or unique items
// where uniqueness is determined based on a key extractor function
func Uniq[A any, K comparable](f func(A) K) func(as Iterator[A]) Iterator[A] {
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
return G.Uniq[Iterator[A]](f)
}

View File

@@ -17,7 +17,6 @@ package stateless
import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
P "github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two iterators, collecting the results in a new iterator. If one
@@ -28,6 +27,6 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa Iterator[A], fb Iterator[B], f F
// Zip takes two iterators and returns an iterators of corresponding pairs. If one input iterators is short, excess elements of the
// longer iterator are discarded
func Zip[A, B any](fb Iterator[B]) func(Iterator[A]) Iterator[P.Pair[A, B]] {
return G.Zip[Iterator[A], Iterator[B], Iterator[P.Pair[A, B]]](fb)
func Zip[A, B any](fb Iterator[B]) Operator[A, Pair[A, B]] {
return G.Zip[Iterator[A], Iterator[B], Iterator[Pair[A, B]]](fb)
}

View File

@@ -1,4 +1,234 @@
# Optics
Refer to [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) for an introduction about functional optics.
Functional optics for composable data access and manipulation in Go.
## Overview
Optics are first-class, composable references to parts of data structures. They provide a uniform interface for reading, writing, and transforming nested immutable data without verbose boilerplate code.
## Quick Start
```go
import (
"github.com/IBM/fp-go/v2/optics/lens"
F "github.com/IBM/fp-go/v2/function"
)
type Person struct {
Name string
Age int
}
// Create a lens for the Name field
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
p.Name = name
return p
},
)
person := Person{Name: "Alice", Age: 30}
// Get the name
name := nameLens.Get(person) // "Alice"
// Set a new name (returns a new Person)
updated := nameLens.Set("Bob")(person)
// person.Name is still "Alice", updated.Name is "Bob"
```
## Core Optics Types
### Lens - Product Types (Structs)
Focus on a single field within a struct. Provides get and set operations.
**Use when:** Working with struct fields that always exist.
```go
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
p.Age = age
return p
},
)
```
### Prism - Sum Types (Variants)
Focus on one variant of a sum type. Provides optional get and definite set.
**Use when:** Working with Either, Result, or custom sum types.
```go
import "github.com/IBM/fp-go/v2/optics/prism"
successPrism := prism.MakePrism(
func(r Result) option.Option[int] {
if s, ok := r.(Success); ok {
return option.Some(s.Value)
}
return option.None[int]()
},
func(v int) Result { return Success{Value: v} },
)
```
### Iso - Isomorphisms
Bidirectional transformation between equivalent types with no information loss.
**Use when:** Converting between equivalent representations (e.g., Celsius ↔ Fahrenheit).
```go
import "github.com/IBM/fp-go/v2/optics/iso"
celsiusToFahrenheit := iso.MakeIso(
func(c float64) float64 { return c*9/5 + 32 },
func(f float64) float64 { return (f - 32) * 5 / 9 },
)
```
### Optional - Maybe Values
Focus on a value that may or may not exist.
**Use when:** Working with nullable fields or values that may be absent.
```go
import "github.com/IBM/fp-go/v2/optics/optional"
timeoutOptional := optional.MakeOptional(
func(c Config) option.Option[*int] {
return option.FromNillable(c.Timeout)
},
func(c Config, t *int) Config {
c.Timeout = t
return c
},
)
```
### Traversal - Multiple Values
Focus on multiple values simultaneously, allowing batch operations.
**Use when:** Working with collections or updating multiple fields at once.
```go
import (
"github.com/IBM/fp-go/v2/optics/traversal"
TA "github.com/IBM/fp-go/v2/optics/traversal/array"
)
numbers := []int{1, 2, 3, 4, 5}
// Double all elements
doubled := F.Pipe2(
numbers,
TA.Traversal[int](),
traversal.Modify[[]int, int](func(n int) int { return n * 2 }),
)
// Result: [2, 4, 6, 8, 10]
```
## Composition
The real power of optics comes from composition:
```go
type Company struct {
Name string
Address Address
}
type Address struct {
Street string
City string
}
// Individual lenses
addressLens := lens.MakeLens(
func(c Company) Address { return c.Address },
func(c Company, a Address) Company {
c.Address = a
return c
},
)
cityLens := lens.MakeLens(
func(a Address) string { return a.City },
func(a Address, city string) Address {
a.City = city
return a
},
)
// Compose to access city directly from company
companyCityLens := F.Pipe1(
addressLens,
lens.Compose[Company](cityLens),
)
company := Company{
Name: "Acme Corp",
Address: Address{Street: "Main St", City: "NYC"},
}
city := companyCityLens.Get(company) // "NYC"
updated := companyCityLens.Set("Boston")(company)
```
## Optics Hierarchy
```
Iso[S, A]
Lens[S, A]
Optional[S, A]
Traversal[S, A]
Prism[S, A]
Optional[S, A]
Traversal[S, A]
```
More specific optics can be converted to more general ones.
## Package Structure
- **optics/lens**: Lenses for product types (structs)
- **optics/prism**: Prisms for sum types (Either, Result, etc.)
- **optics/iso**: Isomorphisms for equivalent types
- **optics/optional**: Optional optics for maybe values
- **optics/traversal**: Traversals for multiple values
Each package includes specialized sub-packages for common patterns:
- **array**: Optics for arrays/slices
- **either**: Optics for Either types
- **option**: Optics for Option types
- **record**: Optics for maps
## Documentation
For detailed documentation on each optic type, see:
- [Main Package Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics)
- [Lens Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)
- [Prism Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)
- [Iso Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)
- [Optional Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)
- [Traversal Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)
## Further Reading
For an introduction to functional optics concepts:
- [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) by Giulio Canti
## Examples
See the [samples/lens](../samples/lens) directory for complete working examples.
## License
Apache License 2.0 - See LICENSE file for details.

231
v2/optics/iso/lens/doc.go Normal file
View File

@@ -0,0 +1,231 @@
// 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 lens provides conversions from isomorphisms to lenses.
# Overview
This package bridges the gap between isomorphisms (bidirectional transformations)
and lenses (focused accessors). Since every isomorphism can be viewed as a lens,
this package provides functions to perform that conversion.
An isomorphism Iso[S, A] represents a lossless bidirectional transformation between
types S and A. A lens Lens[S, A] provides focused access to a part A within a
structure S. Since an isomorphism can transform the entire structure S to A and back,
it naturally forms a lens that focuses on the "whole as a part".
# Mathematical Foundation
Given an Iso[S, A] with:
- Get: S → A (forward transformation)
- ReverseGet: A → S (reverse transformation)
We can construct a Lens[S, A] with:
- Get: S → A (same as iso's Get)
- Set: A → S → S (implemented as: a => s => ReverseGet(a))
The lens laws are automatically satisfied because the isomorphism laws guarantee:
1. GetSet: Set(Get(s))(s) == s (from iso's round-trip law)
2. SetGet: Get(Set(a)(s)) == a (from iso's inverse law)
3. SetSet: Set(a2)(Set(a1)(s)) == Set(a2)(s) (trivially true)
# Basic Usage
Converting an isomorphism to a lens:
type Celsius float64
type Kelvin float64
// Create an isomorphism between Celsius and Kelvin
celsiusKelvinIso := iso.MakeIso(
func(c Celsius) Kelvin { return Kelvin(c + 273.15) },
func(k Kelvin) Celsius { return Celsius(k - 273.15) },
)
// Convert to a lens
celsiusKelvinLens := lens.IsoAsLens(celsiusKelvinIso)
// Use as a lens
celsius := Celsius(20.0)
kelvin := celsiusKelvinLens.Get(celsius) // 293.15 K
updated := celsiusKelvinLens.Set(Kelvin(300))(celsius) // 26.85°C
# Working with Pointers
For pointer-based structures, use IsoAsLensRef:
type UserId int
type User struct {
id UserId
name string
}
// Isomorphism between User pointer and UserId
userIdIso := iso.MakeIso(
func(u *User) UserId { return u.id },
func(id UserId) *User { return &User{id: id, name: "Unknown"} },
)
// Convert to a reference lens
userIdLens := lens.IsoAsLensRef(userIdIso)
user := &User{id: 42, name: "Alice"}
id := userIdLens.Get(user) // 42
updated := userIdLens.Set(UserId(100))(user) // New user with id 100
# Use Cases
1. Type Wrappers: Convert between newtype wrappers and their underlying types
type Email string
type ValidatedEmail struct{ value Email }
emailIso := iso.MakeIso(
func(ve ValidatedEmail) Email { return ve.value },
func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
)
emailLens := lens.IsoAsLens(emailIso)
2. Unit Conversions: Work with different units of measurement
type Meters float64
type Feet float64
metersFeetIso := iso.MakeIso(
func(m Meters) Feet { return Feet(m * 3.28084) },
func(f Feet) Meters { return Meters(f / 3.28084) },
)
distanceLens := lens.IsoAsLens(metersFeetIso)
3. Encoding/Decoding: Transform between different representations
type JSON string
type Config struct {
Host string
Port int
}
// Assuming encode/decode functions exist
configIso := iso.MakeIso(encode, decode)
configLens := lens.IsoAsLens(configIso)
# Composition
Lenses created from isomorphisms can be composed with other lenses:
type Temperature struct {
celsius Celsius
}
// Lens to access celsius field
celsiusFieldLens := L.MakeLens(
func(t Temperature) Celsius { return t.celsius },
func(t Temperature, c Celsius) Temperature {
t.celsius = c
return t
},
)
// Compose with iso-based lens to work with Kelvin
tempKelvinLens := F.Pipe1(
celsiusFieldLens,
L.Compose[Temperature](celsiusKelvinLens),
)
temp := Temperature{celsius: 20}
kelvin := tempKelvinLens.Get(temp) // 293.15 K
updated := tempKelvinLens.Set(Kelvin(300))(temp) // 26.85°C
# Comparison with Direct Lenses
While you can create a lens directly, using an isomorphism provides benefits:
1. Reusability: The isomorphism can be used in multiple contexts
2. Bidirectionality: The inverse transformation is explicitly available
3. Type Safety: Isomorphism laws ensure correctness
4. Composability: Isomorphisms compose naturally
Direct lens approach requires defining both get and set operations separately,
while the isomorphism approach defines the bidirectional transformation once
and converts it to a lens when needed.
# Performance Considerations
Converting an isomorphism to a lens has minimal overhead. The resulting lens
simply delegates to the isomorphism's Get and ReverseGet functions. However,
keep in mind:
1. Each Set operation performs a full transformation via ReverseGet
2. For pointer types, use IsoAsLensRef to ensure proper copying
3. The lens ignores the original structure in Set, using only the new value
# Function Reference
Conversion Functions:
- IsoAsLens: Convert Iso[S, A] to Lens[S, A] for value types
- IsoAsLensRef: Convert Iso[*S, A] to Lens[*S, A] for pointer types
# Related Packages
- github.com/IBM/fp-go/v2/optics/iso: Isomorphisms (bidirectional transformations)
- github.com/IBM/fp-go/v2/optics/lens: Lenses (focused accessors)
- github.com/IBM/fp-go/v2/optics/lens/iso: Convert lenses to isomorphisms (inverse operation)
- github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions)
- github.com/IBM/fp-go/v2/function: Function composition utilities
# Examples
Complete example with type wrappers:
type UserId int
type Username string
type User struct {
id UserId
name Username
}
// Isomorphism for UserId
userIdIso := iso.MakeIso(
func(u User) UserId { return u.id },
func(id UserId) User { return User{id: id, name: "Unknown"} },
)
// Isomorphism for Username
usernameIso := iso.MakeIso(
func(u User) Username { return u.name },
func(name Username) User { return User{id: 0, name: name} },
)
// Convert to lenses
idLens := lens.IsoAsLens(userIdIso)
nameLens := lens.IsoAsLens(usernameIso)
user := User{id: 42, name: "Alice"}
// Access and modify through lenses
id := idLens.Get(user) // 42
name := nameLens.Get(user) // "Alice"
renamed := nameLens.Set("Bob")(user) // User{id: 0, name: "Bob"}
reidentified := idLens.Set(UserId(100))(user) // User{id: 100, name: "Unknown"}
Note: When using Set with iso-based lenses, the entire structure is replaced
via ReverseGet, so other fields may be reset to default values. For partial
updates, use regular lenses instead.
*/
package lens

View File

@@ -18,16 +18,15 @@ package lens
import (
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// IsoAsLens converts an `Iso` to a `Lens`
func IsoAsLens[S, A any](sa I.Iso[S, A]) L.Lens[S, A] {
func IsoAsLens[S, A any](sa Iso[S, A]) Lens[S, A] {
return L.MakeLensCurried(sa.Get, F.Flow2(sa.ReverseGet, F.Flow2(F.Constant1[S, S], EM.Of[func(S) S])))
}
// IsoAsLensRef converts an `Iso` to a `Lens`
func IsoAsLensRef[S, A any](sa I.Iso[*S, A]) L.Lens[*S, A] {
func IsoAsLensRef[S, A any](sa Iso[*S, A]) Lens[*S, A] {
return L.MakeLensRefCurried(sa.Get, F.Flow2(sa.ReverseGet, F.Flow2(F.Constant1[*S, *S], EM.Of[func(*S) *S])))
}

View File

@@ -0,0 +1,399 @@
// 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 lens
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
ISO "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
// Test types
type Celsius float64
type Fahrenheit float64
type UserId int
type User struct {
id UserId
name string
}
type Meters float64
type Feet float64
// TestIsoAsLensBasic tests basic functionality of IsoAsLens
func TestIsoAsLensBasic(t *testing.T) {
// Create an isomorphism between Celsius and Fahrenheit
celsiusToFahrenheit := func(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
fahrenheitToCelsius := func(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
tempIso := ISO.MakeIso(celsiusToFahrenheit, fahrenheitToCelsius)
tempLens := IsoAsLens(tempIso)
t.Run("Get", func(t *testing.T) {
celsius := Celsius(20.0)
fahrenheit := tempLens.Get(celsius)
assert.InDelta(t, 68.0, float64(fahrenheit), 0.001)
})
t.Run("Set", func(t *testing.T) {
celsius := Celsius(20.0)
newFahrenheit := Fahrenheit(86.0)
updated := tempLens.Set(newFahrenheit)(celsius)
assert.InDelta(t, 30.0, float64(updated), 0.001)
})
t.Run("SetPreservesOriginal", func(t *testing.T) {
original := Celsius(20.0)
newFahrenheit := Fahrenheit(86.0)
_ = tempLens.Set(newFahrenheit)(original)
// Original should be unchanged
assert.Equal(t, Celsius(20.0), original)
})
}
// TestIsoAsLensRefBasic tests basic functionality of IsoAsLensRef
func TestIsoAsLensRefBasic(t *testing.T) {
// Create an isomorphism for User pointer and UserId
userToId := func(u *User) UserId {
return u.id
}
idToUser := func(id UserId) *User {
return &User{id: id, name: "Unknown"}
}
userIdIso := ISO.MakeIso(userToId, idToUser)
userIdLens := IsoAsLensRef(userIdIso)
t.Run("Get", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
id := userIdLens.Get(user)
assert.Equal(t, UserId(42), id)
})
t.Run("Set", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
newId := UserId(100)
updated := userIdLens.Set(newId)(user)
assert.Equal(t, UserId(100), updated.id)
assert.Equal(t, "Unknown", updated.name) // ReverseGet creates new user
})
t.Run("SetCreatesNewPointer", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
newId := UserId(100)
updated := userIdLens.Set(newId)(user)
// Should be different pointers
assert.NotSame(t, user, updated)
// Original should be unchanged
assert.Equal(t, UserId(42), user.id)
assert.Equal(t, "Alice", user.name)
})
}
// TestIsoAsLensLaws verifies that IsoAsLens satisfies lens laws
func TestIsoAsLensLaws(t *testing.T) {
// Create a simple isomorphism
type Wrapper struct{ value int }
wrapperIso := ISO.MakeIso(
func(w Wrapper) int { return w.value },
func(i int) Wrapper { return Wrapper{value: i} },
)
lens := IsoAsLens(wrapperIso)
wrapper := Wrapper{value: 42}
newValue := 100
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := lens.Set(lens.Get(wrapper))(wrapper)
assert.Equal(t, wrapper, result)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := lens.Get(lens.Set(newValue)(wrapper))
assert.Equal(t, newValue, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
result1 := lens.Set(200)(lens.Set(newValue)(wrapper))
result2 := lens.Set(200)(wrapper)
assert.Equal(t, result2, result1)
})
}
// TestIsoAsLensRefLaws verifies that IsoAsLensRef satisfies lens laws
func TestIsoAsLensRefLaws(t *testing.T) {
type Wrapper struct{ value int }
wrapperIso := ISO.MakeIso(
func(w *Wrapper) int { return w.value },
func(i int) *Wrapper { return &Wrapper{value: i} },
)
lens := IsoAsLensRef(wrapperIso)
wrapper := &Wrapper{value: 42}
newValue := 100
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := lens.Set(lens.Get(wrapper))(wrapper)
assert.Equal(t, wrapper.value, result.value)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := lens.Get(lens.Set(newValue)(wrapper))
assert.Equal(t, newValue, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
result1 := lens.Set(200)(lens.Set(newValue)(wrapper))
result2 := lens.Set(200)(wrapper)
assert.Equal(t, result2.value, result1.value)
})
}
// TestIsoAsLensComposition tests composing iso-based lenses with other lenses
func TestIsoAsLensComposition(t *testing.T) {
type Temperature struct {
celsius Celsius
}
// Lens to access celsius field
celsiusFieldLens := L.MakeLens(
func(t Temperature) Celsius { return t.celsius },
func(t Temperature, c Celsius) Temperature {
t.celsius = c
return t
},
)
// Isomorphism between Celsius and Fahrenheit
celsiusToFahrenheit := func(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
fahrenheitToCelsius := func(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
tempIso := ISO.MakeIso(celsiusToFahrenheit, fahrenheitToCelsius)
tempLens := IsoAsLens(tempIso)
// Compose to work with Fahrenheit directly from Temperature
composedLens := F.Pipe1(
celsiusFieldLens,
L.Compose[Temperature](tempLens),
)
temp := Temperature{celsius: 20}
t.Run("ComposedGet", func(t *testing.T) {
fahrenheit := composedLens.Get(temp)
assert.InDelta(t, 68.0, float64(fahrenheit), 0.001)
})
t.Run("ComposedSet", func(t *testing.T) {
newFahrenheit := Fahrenheit(86.0)
updated := composedLens.Set(newFahrenheit)(temp)
assert.InDelta(t, 30.0, float64(updated.celsius), 0.001)
})
}
// TestIsoAsLensModify tests using Modify with iso-based lenses
func TestIsoAsLensModify(t *testing.T) {
// Isomorphism between Meters and Feet
metersToFeet := func(m Meters) Feet {
return Feet(m * 3.28084)
}
feetToMeters := func(f Feet) Meters {
return Meters(f / 3.28084)
}
distanceIso := ISO.MakeIso(metersToFeet, feetToMeters)
distanceLens := IsoAsLens(distanceIso)
meters := Meters(10.0)
t.Run("ModifyDouble", func(t *testing.T) {
// Double the distance in feet, result in meters
doubleFeet := func(f Feet) Feet { return f * 2 }
modified := L.Modify[Meters](doubleFeet)(distanceLens)(meters)
assert.InDelta(t, 20.0, float64(modified), 0.001)
})
t.Run("ModifyIdentity", func(t *testing.T) {
// Identity modification should return same value
identity := func(f Feet) Feet { return f }
modified := L.Modify[Meters](identity)(distanceLens)(meters)
assert.InDelta(t, float64(meters), float64(modified), 0.001)
})
}
// TestIsoAsLensWithIdentityIso tests that identity iso creates identity lens
func TestIsoAsLensWithIdentityIso(t *testing.T) {
type Value int
idIso := ISO.Id[Value]()
idLens := IsoAsLens(idIso)
value := Value(42)
t.Run("IdentityGet", func(t *testing.T) {
result := idLens.Get(value)
assert.Equal(t, value, result)
})
t.Run("IdentitySet", func(t *testing.T) {
newValue := Value(100)
result := idLens.Set(newValue)(value)
assert.Equal(t, newValue, result)
})
}
// TestIsoAsLensRefWithIdentityIso tests identity iso with references
func TestIsoAsLensRefWithIdentityIso(t *testing.T) {
type Value struct{ n int }
idIso := ISO.Id[*Value]()
idLens := IsoAsLensRef(idIso)
value := &Value{n: 42}
t.Run("IdentityGet", func(t *testing.T) {
result := idLens.Get(value)
assert.Equal(t, value, result)
})
t.Run("IdentitySet", func(t *testing.T) {
newValue := &Value{n: 100}
result := idLens.Set(newValue)(value)
assert.Equal(t, newValue, result)
})
}
// TestIsoAsLensRoundTrip tests round-trip conversions
func TestIsoAsLensRoundTrip(t *testing.T) {
type Email string
type ValidatedEmail struct{ value Email }
emailIso := ISO.MakeIso(
func(ve ValidatedEmail) Email { return ve.value },
func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
)
emailLens := IsoAsLens(emailIso)
validated := ValidatedEmail{value: "user@example.com"}
t.Run("RoundTripThroughGet", func(t *testing.T) {
// Get the email, then Set it back
email := emailLens.Get(validated)
restored := emailLens.Set(email)(validated)
assert.Equal(t, validated, restored)
})
t.Run("RoundTripThroughSet", func(t *testing.T) {
// Set a new email, then Get it
newEmail := Email("admin@example.com")
updated := emailLens.Set(newEmail)(validated)
retrieved := emailLens.Get(updated)
assert.Equal(t, newEmail, retrieved)
})
}
// TestIsoAsLensWithComplexTypes tests with more complex type transformations
func TestIsoAsLensWithComplexTypes(t *testing.T) {
type Point struct {
x, y float64
}
type PolarCoord struct {
r, theta float64
}
// Isomorphism between Cartesian and Polar coordinates (simplified for testing)
cartesianToPolar := func(p Point) PolarCoord {
r := p.x*p.x + p.y*p.y
theta := 0.0 // Simplified
return PolarCoord{r: r, theta: theta}
}
polarToCartesian := func(pc PolarCoord) Point {
return Point{x: pc.r, y: pc.theta} // Simplified
}
coordIso := ISO.MakeIso(cartesianToPolar, polarToCartesian)
coordLens := IsoAsLens(coordIso)
point := Point{x: 3.0, y: 4.0}
t.Run("ComplexGet", func(t *testing.T) {
polar := coordLens.Get(point)
assert.NotNil(t, polar)
})
t.Run("ComplexSet", func(t *testing.T) {
newPolar := PolarCoord{r: 5.0, theta: 0.927}
updated := coordLens.Set(newPolar)(point)
assert.NotNil(t, updated)
})
}
// TestIsoAsLensTypeConversion tests type conversion scenarios
func TestIsoAsLensTypeConversion(t *testing.T) {
type StringWrapper string
type IntWrapper int
// Isomorphism that converts string length to int
strLenIso := ISO.MakeIso(
func(s StringWrapper) IntWrapper { return IntWrapper(len(s)) },
func(i IntWrapper) StringWrapper {
// Create a string of given length (simplified)
result := ""
for j := 0; j < int(i); j++ {
result += "x"
}
return StringWrapper(result)
},
)
strLenLens := IsoAsLens(strLenIso)
t.Run("StringToLength", func(t *testing.T) {
str := StringWrapper("hello")
length := strLenLens.Get(str)
assert.Equal(t, IntWrapper(5), length)
})
t.Run("LengthToString", func(t *testing.T) {
str := StringWrapper("hello")
newLength := IntWrapper(3)
updated := strLenLens.Set(newLength)(str)
assert.Equal(t, 3, len(updated))
})
}

View File

@@ -0,0 +1,11 @@
package lens
import (
"github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
)
type (
Lens[S, A any] = L.Lens[S, A]
Iso[S, A any] = iso.Iso[S, A]
)

303
v2/optics/iso/option/doc.go Normal file
View File

@@ -0,0 +1,303 @@
// 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 option provides isomorphisms for working with Option types.
# Overview
This package offers utilities to convert between regular values and Option-wrapped values,
particularly useful for handling zero values and optional data. It provides isomorphisms
that treat certain values (like zero values) as representing absence, mapping them to None,
while other values map to Some.
# Core Functionality
The main function in this package is FromZero, which creates an isomorphism between a
comparable type T and Option[T], treating the zero value as None.
# FromZero Isomorphism
FromZero creates a bidirectional transformation where:
- Forward (Get): T → Option[T]
- Zero value → None
- Non-zero value → Some(value)
- Reverse (ReverseGet): Option[T] → T
- None → Zero value
- Some(value) → value
# Basic Usage
Working with integers:
import (
"github.com/IBM/fp-go/v2/optics/iso/option"
O "github.com/IBM/fp-go/v2/option"
)
isoInt := option.FromZero[int]()
// Convert zero to None
opt := isoInt.Get(0) // None[int]
// Convert non-zero to Some
opt = isoInt.Get(42) // Some(42)
// Convert None to zero
val := isoInt.ReverseGet(O.None[int]()) // 0
// Convert Some to value
val = isoInt.ReverseGet(O.Some(42)) // 42
# Use Cases
## Database Nullable Columns
Convert between database NULL and Go zero values:
type User struct {
ID int
Name string
Age *int // NULL in database
Email *string
}
ageIso := option.FromZero[*int]()
// Reading from database
var dbAge *int = nil
optAge := ageIso.Get(dbAge) // None[*int]
// Writing to database
userAge := 25
dbAge = ageIso.ReverseGet(O.Some(&userAge)) // &25
## Configuration with Defaults
Handle optional configuration values:
type Config struct {
Port int
Timeout int
MaxConn int
}
portIso := option.FromZero[int]()
// Use zero as "not configured"
config := Config{Port: 0, Timeout: 30, MaxConn: 100}
portOpt := portIso.Get(config.Port) // None[int] (use default)
// Set explicit value
config.Port = portIso.ReverseGet(O.Some(8080)) // 8080
## API Response Handling
Work with APIs that use zero values to indicate absence:
type APIResponse struct {
UserID int // 0 means not set
Score float64 // 0.0 means not available
Message string // "" means no message
}
userIDIso := option.FromZero[int]()
scoreIso := option.FromZero[float64]()
messageIso := option.FromZero[string]()
response := APIResponse{UserID: 0, Score: 0.0, Message: ""}
userID := userIDIso.Get(response.UserID) // None[int]
score := scoreIso.Get(response.Score) // None[float64]
message := messageIso.Get(response.Message) // None[string]
## Validation Logic
Simplify required vs optional field validation:
type FormData struct {
Name string // Required
Email string // Required
Phone string // Optional (empty = not provided)
Comments string // Optional
}
phoneIso := option.FromZero[string]()
commentsIso := option.FromZero[string]()
form := FormData{
Name: "Alice",
Email: "alice@example.com",
Phone: "",
Comments: "",
}
// Check optional fields
phone := phoneIso.Get(form.Phone) // None[string]
comments := commentsIso.Get(form.Comments) // None[string]
// Validate: required fields must be non-empty
if form.Name == "" || form.Email == "" {
// Validation error
}
# Working with Different Types
## Strings
strIso := option.FromZero[string]()
opt := strIso.Get("") // None[string]
opt = strIso.Get("hello") // Some("hello")
val := strIso.ReverseGet(O.None[string]()) // ""
val = strIso.ReverseGet(O.Some("world")) // "world"
## Pointers
ptrIso := option.FromZero[*int]()
opt := ptrIso.Get(nil) // None[*int]
num := 42
opt = ptrIso.Get(&num) // Some(&num)
val := ptrIso.ReverseGet(O.None[*int]()) // nil
val = ptrIso.ReverseGet(O.Some(&num)) // &num
## Floating Point Numbers
floatIso := option.FromZero[float64]()
opt := floatIso.Get(0.0) // None[float64]
opt = floatIso.Get(3.14) // Some(3.14)
val := floatIso.ReverseGet(O.None[float64]()) // 0.0
val = floatIso.ReverseGet(O.Some(2.71)) // 2.71
## Booleans
boolIso := option.FromZero[bool]()
opt := boolIso.Get(false) // None[bool]
opt = boolIso.Get(true) // Some(true)
val := boolIso.ReverseGet(O.None[bool]()) // false
val = boolIso.ReverseGet(O.Some(true)) // true
# Composition with Other Optics
Combine with lenses for nested structures:
import (
L "github.com/IBM/fp-go/v2/optics/lens"
I "github.com/IBM/fp-go/v2/optics/iso"
)
type Settings struct {
Volume int // 0 means muted
}
volumeLens := L.MakeLens(
func(s Settings) int { return s.Volume },
func(s Settings, v int) Settings {
s.Volume = v
return s
},
)
volumeIso := option.FromZero[int]()
// Compose lens with iso
volumeOptLens := F.Pipe1(
volumeLens,
L.IMap[Settings](volumeIso.Get, volumeIso.ReverseGet),
)
settings := Settings{Volume: 0}
vol := volumeOptLens.Get(settings) // None[int] (muted)
// Set volume
updated := volumeOptLens.Set(O.Some(75))(settings)
// updated.Volume == 75
# Isomorphism Laws
FromZero satisfies the isomorphism round-trip laws:
1. **ReverseGet(Get(t)) == t** for all t: T
isoInt := option.FromZero[int]()
value := 42
result := isoInt.ReverseGet(isoInt.Get(value))
// result == 42
2. **Get(ReverseGet(opt)) == opt** for all opt: Option[T]
isoInt := option.FromZero[int]()
opt := O.Some(42)
result := isoInt.Get(isoInt.ReverseGet(opt))
// result == Some(42)
These laws ensure that the transformation is truly reversible with no information loss.
# Performance Considerations
The FromZero isomorphism is very efficient:
- No allocations for the iso structure itself
- Simple equality comparison for zero check
- Direct value unwrapping for ReverseGet
- No reflection or runtime type assertions
# Type Safety
The isomorphism is fully type-safe:
- Compile-time type checking ensures T is comparable
- Generic type parameters prevent type mismatches
- No runtime type assertions needed
- The compiler enforces correct usage
# Limitations
The FromZero isomorphism has some limitations to be aware of:
1. **Zero Value Ambiguity**: Cannot distinguish between "intentionally zero" and "absent"
- For int: 0 always maps to None, even if 0 is a valid value
- For string: "" always maps to None, even if empty string is valid
- Solution: Use a different representation (e.g., pointers) if zero is meaningful
2. **Comparable Constraint**: Only works with comparable types
- Cannot use with slices, maps, or functions
- Cannot use with structs containing non-comparable fields
- Solution: Use pointers to such types, or custom isomorphisms
3. **Boolean Limitation**: false always maps to None
- Cannot represent "explicitly false" vs "not set"
- Solution: Use *bool or a custom type if this distinction matters
# Related Packages
- github.com/IBM/fp-go/v2/optics/iso: Core isomorphism functionality
- github.com/IBM/fp-go/v2/option: Option type and operations
- github.com/IBM/fp-go/v2/optics/lens: Lenses for focused access
- github.com/IBM/fp-go/v2/optics/lens/option: Lenses for optional values
# See Also
For more information on isomorphisms and optics:
- optics/iso package documentation
- optics package overview
- option package documentation
*/
package option

364
v2/optics/lens/iso/doc.go Normal file
View File

@@ -0,0 +1,364 @@
// 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 lenses with isomorphisms.
# Overview
This package bridges lenses and isomorphisms, allowing you to transform the focus type
of a lens using an isomorphism. It provides functions to compose lenses with isomorphisms
and to create isomorphisms for common patterns like nullable pointers.
The key insight is that if you have a Lens[S, A] and an Iso[A, B], you can create a
Lens[S, B] by composing them. This allows you to work with transformed views of your
data without changing the underlying structure.
# Core Functions
## FromNillable
Creates an isomorphism between a nullable pointer and an Option type:
type Config struct {
Timeout *int
}
// Create isomorphism: *int ↔ Option[int]
timeoutIso := iso.FromNillable[int]()
// nil → None, &value → Some(value)
opt := timeoutIso.Get(nil) // None[int]
num := 42
opt = timeoutIso.Get(&num) // Some(42)
// None → nil, Some(value) → &value
ptr := timeoutIso.ReverseGet(O.None[int]()) // nil
ptr = timeoutIso.ReverseGet(O.Some(42)) // &42
## Compose
Composes a lens with an isomorphism to transform the focus type:
type Person struct {
Name string
Age int
}
type Celsius float64
type Fahrenheit float64
type Weather struct {
Temperature Celsius
}
// Lens to access temperature
tempLens := L.MakeLens(
func(w Weather) Celsius { return w.Temperature },
func(w Weather, t Celsius) Weather {
w.Temperature = t
return w
},
)
// Isomorphism: Celsius ↔ Fahrenheit
celsiusToFahrenheit := I.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
// Compose to work with Fahrenheit
tempFahrenheitLens := F.Pipe1(
tempLens,
iso.Compose[Weather, Celsius, Fahrenheit](celsiusToFahrenheit),
)
weather := Weather{Temperature: 20} // 20°C
tempF := tempFahrenheitLens.Get(weather) // 68°F
updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C)
# Use Cases
## Working with Nullable Fields
Convert between nullable pointers and Option types:
type DatabaseConfig struct {
Host string
Port int
Username string
Password *string // Nullable
}
type AppConfig struct {
Database *DatabaseConfig
}
// Lens to database config
dbLens := L.MakeLens(
func(c AppConfig) *DatabaseConfig { return c.Database },
func(c AppConfig, db *DatabaseConfig) AppConfig {
c.Database = db
return c
},
)
// Isomorphism for nullable pointer
dbIso := iso.FromNillable[DatabaseConfig]()
// Compose to work with Option
dbOptLens := F.Pipe1(
dbLens,
iso.Compose[AppConfig, *DatabaseConfig, O.Option[DatabaseConfig]](dbIso),
)
config := AppConfig{Database: nil}
dbOpt := dbOptLens.Get(config) // None[DatabaseConfig]
// Set with Some
newDB := DatabaseConfig{Host: "localhost", Port: 5432}
updated := dbOptLens.Set(O.Some(newDB))(config)
## Unit Conversions
Work with different units of measurement:
type Distance struct {
Meters float64
}
type Kilometers float64
type Miles float64
// Lens to meters
metersLens := L.MakeLens(
func(d Distance) float64 { return d.Meters },
func(d Distance, m float64) Distance {
d.Meters = m
return d
},
)
// Isomorphism: meters ↔ kilometers
metersToKm := I.MakeIso(
func(m float64) Kilometers { return Kilometers(m / 1000) },
func(km Kilometers) float64 { return float64(km * 1000) },
)
// Compose to work with kilometers
kmLens := F.Pipe1(
metersLens,
iso.Compose[Distance, float64, Kilometers](metersToKm),
)
distance := Distance{Meters: 5000}
km := kmLens.Get(distance) // 5 km
updated := kmLens.Set(Kilometers(10))(distance) // 10000 meters
## Type Wrappers
Work with newtype wrappers:
type UserId int
type User struct {
ID UserId
Name string
}
// Lens to user ID
idLens := L.MakeLens(
func(u User) UserId { return u.ID },
func(u User, id UserId) User {
u.ID = id
return u
},
)
// Isomorphism: UserId ↔ int
userIdIso := I.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
// Compose to work with raw int
idIntLens := F.Pipe1(
idLens,
iso.Compose[User, UserId, int](userIdIso),
)
user := User{ID: 42, Name: "Alice"}
rawId := idIntLens.Get(user) // 42 (int)
updated := idIntLens.Set(100)(user) // UserId(100)
## Nested Nullable Fields
Safely navigate through nullable nested structures:
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address *Address
}
type Company struct {
Name string
CEO *Person
}
// Lens to CEO
ceoLens := L.MakeLens(
func(c Company) *Person { return c.CEO },
func(c Company, p *Person) Company {
c.CEO = p
return c
},
)
// Isomorphism for nullable person
personIso := iso.FromNillable[Person]()
// Compose to work with Option[Person]
ceoOptLens := F.Pipe1(
ceoLens,
iso.Compose[Company, *Person, O.Option[Person]](personIso),
)
company := Company{Name: "Acme Corp", CEO: nil}
ceo := ceoOptLens.Get(company) // None[Person]
// Set CEO
newCEO := Person{Name: "Alice", Address: nil}
updated := ceoOptLens.Set(O.Some(newCEO))(company)
# Composition Patterns
## Chaining Multiple Isomorphisms
type Meters float64
type Kilometers float64
type Miles float64
type Journey struct {
Distance Meters
}
// Lens to distance
distLens := L.MakeLens(
func(j Journey) Meters { return j.Distance },
func(j Journey, d Meters) Journey {
j.Distance = d
return j
},
)
// Isomorphisms
metersToKm := I.MakeIso(
func(m Meters) Kilometers { return Kilometers(m / 1000) },
func(km Kilometers) Meters { return Meters(km * 1000) },
)
kmToMiles := I.MakeIso(
func(km Kilometers) Miles { return Miles(km * 0.621371) },
func(mi Miles) Kilometers { return Kilometers(mi / 0.621371) },
)
// Compose lens with chained isomorphisms
milesLens := F.Pipe2(
distLens,
iso.Compose[Journey, Meters, Kilometers](metersToKm),
iso.Compose[Journey, Kilometers, Miles](kmToMiles),
)
journey := Journey{Distance: 5000} // 5000 meters
miles := milesLens.Get(journey) // ~3.11 miles
## Combining with Optional Lenses
type Config struct {
Database *DatabaseConfig
}
type DatabaseConfig struct {
Port int
}
// Lens to database (nullable)
dbLens := L.MakeLens(
func(c Config) *DatabaseConfig { return c.Database },
func(c Config, db *DatabaseConfig) Config {
c.Database = db
return c
},
)
// Convert to Option lens
dbIso := iso.FromNillable[DatabaseConfig]()
dbOptLens := F.Pipe1(
dbLens,
iso.Compose[Config, *DatabaseConfig, O.Option[DatabaseConfig]](dbIso),
)
// Now compose with lens to port
portLens := L.MakeLens(
func(db DatabaseConfig) int { return db.Port },
func(db DatabaseConfig, port int) DatabaseConfig {
db.Port = port
return db
},
)
// Use ComposeOption to handle the Option
defaultDB := DatabaseConfig{Port: 5432}
configPortLens := F.Pipe1(
dbOptLens,
L.ComposeOption[Config, int](defaultDB)(portLens),
)
# Performance Considerations
Composing lenses with isomorphisms is efficient:
- No additional allocations beyond the lens and iso structures
- Composition creates function closures but is still performant
- The isomorphism transformations are applied on-demand
- Consider caching composed lenses for frequently used paths
# Type Safety
All operations are fully type-safe:
- Compile-time type checking ensures correct composition
- Generic type parameters prevent type mismatches
- No runtime type assertions needed
- The compiler enforces that isomorphisms are properly reversible
# Related Packages
- github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
- github.com/IBM/fp-go/v2/optics/iso: Core isomorphism functionality
- github.com/IBM/fp-go/v2/optics/iso/lens: Convert isomorphisms to lenses
- github.com/IBM/fp-go/v2/option: Option type and operations
- github.com/IBM/fp-go/v2/function: Function composition utilities
# See Also
For more information on lenses and isomorphisms:
- optics/lens package documentation
- optics/iso package documentation
- optics package overview
*/
package iso

View File

@@ -24,18 +24,18 @@ import (
)
// FromNillable converts a nillable value to an option and back
func FromNillable[T any]() I.Iso[*T, O.Option[T]] {
func FromNillable[T any]() Iso[*T, Option[T]] {
return I.MakeIso(F.Flow2(
O.FromPredicate(F.IsNonNil[T]),
O.Map(F.Deref[T]),
),
O.Fold(F.Constant((*T)(nil)), F.Ref[T]),
O.Fold(F.ConstNil[T], F.Ref[T]),
)
}
// Compose converts a Lens to a property of `A` into a lens to a property of type `B`
// the transformation is done via an ISO
func Compose[S, A, B any](ab I.Iso[A, B]) func(sa L.Lens[S, A]) L.Lens[S, B] {
func Compose[S, A, B any](ab Iso[A, B]) Operator[S, A, B] {
return F.Pipe2(
ab,
IL.IsoAsLens[A, B],

View File

@@ -0,0 +1,14 @@
package iso
import (
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
)
type (
Option[A any] = option.Option[A]
Iso[S, A any] = iso.Iso[S, A]
Lens[S, A any] = lens.Lens[S, A]
Operator[S, A, B any] = lens.Operator[S, A, B]
)

View File

@@ -17,6 +17,7 @@
package lens
import (
"github.com/IBM/fp-go/v2/endomorphism"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
)
@@ -43,7 +44,7 @@ func setCopyWithEq[GET ~func(*S) A, SET ~func(*S, A) *S, S, A any](pred EQ.Eq[A]
// setCopyCurried wraps a setter for a pointer into a setter that first creates a copy before
// modifying that copy
func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(a A) Endomorphism[*S] {
func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(A) Endomorphism[*S] {
return func(a A) Endomorphism[*S] {
seta := setter(a)
return func(s *S) *S {
@@ -91,7 +92,7 @@ func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(a
// name := nameLens.Get(person) // "Alice"
// updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}
func MakeLens[GET ~func(S) A, SET ~func(S, A) S, S, A any](get GET, set SET) Lens[S, A] {
return MakeLensCurried(get, F.Curry2(F.Swap(set)))
return MakeLensCurried(get, F.Bind2of2(set))
}
// MakeLensCurried creates a [Lens] with a curried setter F.
@@ -374,7 +375,7 @@ func IdRef[S any]() Lens[*S, *S] {
}
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GET, set SET) Lens[S, B], ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
func compose[GET ~func(S) B, SET ~func(B) func(S) S, S, A, B any](creator func(get GET, set SET) Lens[S, B], ab Lens[A, B]) Operator[S, A, B] {
abget := ab.Get
abset := ab.Set
return func(sa Lens[S, A]) Lens[S, B] {
@@ -382,8 +383,12 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
saset := sa.Set
return creator(
F.Flow2(saget, abget),
func(s S, b B) S {
return saset(abset(b)(saget(s)))(s)
func(b B) func(S) S {
return endomorphism.Join(F.Flow3(
saget,
abset(b),
saset,
))
},
)
}
@@ -435,8 +440,8 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
// person := Person{Name: "Alice", Address: Address{Street: "Main St"}}
// street := personStreetLens.Get(person) // "Main St"
// updated := personStreetLens.Set("Oak Ave")(person)
func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
return compose(MakeLens[func(S) B, func(S, B) S], ab)
func Compose[S, A, B any](ab Lens[A, B]) Operator[S, A, B] {
return compose(MakeLensCurried[func(S) B, func(B) func(S) S], ab)
}
// ComposeRef combines two lenses for pointer-based structures.
@@ -477,12 +482,8 @@ func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
// )
//
// personStreetLens := F.Pipe1(addressLens, lens.ComposeRef[Person](streetLens))
func ComposeRef[S, A, B any](ab Lens[A, B]) func(Lens[*S, A]) Lens[*S, B] {
return compose(MakeLensRef[func(*S) B, func(*S, B) *S], ab)
}
func modify[FCT ~func(A) A, S, A any](f FCT, sa Lens[S, A], s S) S {
return sa.Set(f(sa.Get(s)))(s)
func ComposeRef[S, A, B any](ab Lens[A, B]) Operator[*S, A, B] {
return compose(MakeLensRefCurried[S, B], ab)
}
// Modify transforms a value through a lens using a transformation F.
@@ -531,7 +532,13 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Lens[S, A], s S) S {
// )
// // doubled.Value == 10
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S] {
return F.Curry3(modify[FCT, S, A])(f)
return func(la Lens[S, A]) Endomorphism[S] {
return endomorphism.Join(F.Flow3(
la.Get,
f,
la.Set,
))
}
}
// IMap transforms the focus type of a lens using an isomorphism.
@@ -585,8 +592,8 @@ func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S
// weather := Weather{Temperature: 20} // 20°C
// tempF := tempFahrenheitLens.Get(weather) // 68°F
// updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C)
func IMap[E any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Lens[E, A]) Lens[E, B] {
return func(ea Lens[E, A]) Lens[E, B] {
return Lens[E, B]{Get: F.Flow2(ea.Get, ab), Set: F.Flow2(ba, ea.Set)}
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[S, A, B] {
return func(ea Lens[S, A]) Lens[S, B] {
return MakeLensCurried(F.Flow2(ea.Get, ab), F.Flow2(ba, ea.Set))
}
}

View File

@@ -636,5 +636,3 @@ func TestComposeAssociativity(t *testing.T) {
assert.Equal(t, composed1.Get(l1), composed2.Get(l1))
assert.Equal(t, composed1.Set("new")(l1), composed2.Set("new")(l1))
}
// Made with Bob

View File

@@ -1,7 +1,9 @@
package option
import (
"github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
@@ -51,9 +53,9 @@ import (
// defaultSettings := &Settings{}
// configRetriesLens := F.Pipe1(settingsLens,
// lens.Compose[Config, *int](defaultSettings)(retriesLens))
func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
func Compose[S, B, A any](defaultA A) func(LensO[A, B]) Operator[S, A, B] {
noneb := O.None[B]()
return func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
return func(ab LensO[A, B]) Operator[S, A, B] {
abGet := ab.Get
abSetNone := ab.Set(noneb)
return func(sa LensO[S, A]) LensO[S, B] {
@@ -62,41 +64,24 @@ func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) Len
setSomeA := F.Flow2(O.Some[A], sa.Set)
return lens.MakeLensCurried(
F.Flow2(saGet, O.Chain(abGet)),
func(optB Option[B]) Endomorphism[S] {
return func(s S) S {
optA := saGet(s)
return O.MonadFold(
optB,
// optB is None
func() S {
return O.MonadFold(
optA,
// optA is None - no-op
F.Constant(s),
// optA is Some - unset B in A
func(a A) S {
return setSomeA(abSetNone(a))(s)
},
)
},
// optB is Some
func(b B) S {
setB := ab.Set(O.Some(b))
return O.MonadFold(
optA,
// optA is None - create with defaultA
func() S {
return setSomeA(setB(defaultA))(s)
},
// optA is Some - update B in A
func(a A) S {
return setSomeA(setB(a))(s)
},
)
},
)
}
},
F.Flow2(
O.Fold(
// optB is None
lazy.Of(F.Flow2(
saGet,
O.Fold(endomorphism.Identity[S], F.Flow2(abSetNone, setSomeA)),
)),
// optB is Some
func(b B) func(S) Endomorphism[S] {
setB := ab.Set(O.Some(b))
return F.Flow2(
saGet,
O.Fold(lazy.Of(setSomeA(setB(defaultA))), F.Flow2(setB, setSomeA)),
)
},
),
endomorphism.Join[S],
),
)
}
}
@@ -150,8 +135,8 @@ func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) Len
// port := configPortLens.Get(config) // None[int]
// updated := configPortLens.Set(O.Some(3306))(config)
// // updated.Database.Port == 3306, Host == "localhost" (from default)
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
return func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
func ComposeOption[S, B, A any](defaultA A) func(Lens[A, B]) Operator[S, A, B] {
return func(ab Lens[A, B]) Operator[S, A, B] {
abGet := ab.Get
abSet := ab.Set
return func(sa LensO[S, A]) LensO[S, B] {
@@ -159,33 +144,23 @@ func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(LensO[S, A]
saSet := sa.Set
// Pre-compute setters
setNoneA := saSet(O.None[A]())
setSomeA := func(a A) Endomorphism[S] {
return saSet(O.Some(a))
}
return lens.MakeLens(
func(s S) Option[B] {
return O.Map(abGet)(saGet(s))
},
func(s S, optB Option[B]) S {
return O.Fold(
// optB is None - remove A entirely
F.Constant(setNoneA(s)),
// optB is Some - set B
func(b B) S {
optA := saGet(s)
return O.Fold(
// optA is None - create with defaultA
func() S {
return setSomeA(abSet(b)(defaultA))(s)
},
// optA is Some - update B in A
func(a A) S {
return setSomeA(abSet(b)(a))(s)
},
)(optA)
},
)(optB)
},
setSomeA := F.Flow2(O.Some[A], saSet)
return lens.MakeLensCurried(
F.Flow2(saGet, O.Map(abGet)),
O.Fold(
// optB is None - remove A entirely
lazy.Of(setNoneA),
// optB is Some - set B
func(b B) Endomorphism[S] {
absetB := abSet(b)
abSetA := absetB(defaultA)
return endomorphism.Join(F.Flow3(
saGet,
O.Fold(lazy.Of(abSetA), absetB),
setSomeA,
))
},
),
)
}
}

View File

@@ -0,0 +1,841 @@
// 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 option
import (
"testing"
EQT "github.com/IBM/fp-go/v2/eq/testing"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// Test types for ComposeOption - using unique names to avoid conflicts
type (
DatabaseCfg struct {
Host string
Port int
}
ServerConfig struct {
Database *DatabaseCfg
}
AppSettings struct {
MaxRetries int
Timeout int
}
ApplicationConfig struct {
Settings *AppSettings
}
)
// Helper methods for DatabaseCfg
func (db *DatabaseCfg) GetPort() int {
return db.Port
}
func (db *DatabaseCfg) SetPort(port int) *DatabaseCfg {
db.Port = port
return db
}
// Helper methods for ServerConfig
func (c ServerConfig) GetDatabase() *DatabaseCfg {
return c.Database
}
func (c ServerConfig) SetDatabase(db *DatabaseCfg) ServerConfig {
c.Database = db
return c
}
// Helper methods for AppSettings
func (s *AppSettings) GetMaxRetries() int {
return s.MaxRetries
}
func (s *AppSettings) SetMaxRetries(retries int) *AppSettings {
s.MaxRetries = retries
return s
}
// Helper methods for ApplicationConfig
func (ac ApplicationConfig) GetSettings() *AppSettings {
return ac.Settings
}
func (ac ApplicationConfig) SetSettings(s *AppSettings) ApplicationConfig {
ac.Settings = s
return ac
}
// TestComposeOptionBasicOperations tests basic get/set operations
func TestComposeOptionBasicOperations(t *testing.T) {
// Create lenses
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
defaultDB := &DatabaseCfg{Host: "localhost", Port: 5432}
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
t.Run("Get from empty config returns None", func(t *testing.T) {
config := ServerConfig{Database: nil}
result := configPortLens.Get(config)
assert.True(t, O.IsNone(result))
})
t.Run("Get from config with database returns Some", func(t *testing.T) {
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
result := configPortLens.Get(config)
assert.Equal(t, O.Some(3306), result)
})
t.Run("Set Some on empty config creates database with default", func(t *testing.T) {
config := ServerConfig{Database: nil}
updated := configPortLens.Set(O.Some(3306))(config)
assert.NotNil(t, updated.Database)
assert.Equal(t, 3306, updated.Database.Port)
assert.Equal(t, "localhost", updated.Database.Host) // From default
})
t.Run("Set Some on existing database updates port", func(t *testing.T) {
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 5432}}
updated := configPortLens.Set(O.Some(8080))(config)
assert.NotNil(t, updated.Database)
assert.Equal(t, 8080, updated.Database.Port)
assert.Equal(t, "example.com", updated.Database.Host) // Preserved
})
t.Run("Set None removes database entirely", func(t *testing.T) {
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
updated := configPortLens.Set(O.None[int]())(config)
assert.Nil(t, updated.Database)
})
t.Run("Set None on empty config is no-op", func(t *testing.T) {
config := ServerConfig{Database: nil}
updated := configPortLens.Set(O.None[int]())(config)
assert.Nil(t, updated.Database)
})
}
// TestComposeOptionLensLawsDetailed verifies that ComposeOption satisfies lens laws
func TestComposeOptionLensLawsDetailed(t *testing.T) {
// Setup
defaultDB := &DatabaseCfg{Host: "localhost", Port: 5432}
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
// Equality predicates
eqInt := EQT.Eq[int]()
eqOptInt := O.Eq(eqInt)
eqServerConfig := func(a, b ServerConfig) bool {
if a.Database == nil && b.Database == nil {
return true
}
if a.Database == nil || b.Database == nil {
return false
}
return a.Database.Host == b.Database.Host && a.Database.Port == b.Database.Port
}
// Test structures
configNil := ServerConfig{Database: nil}
config3306 := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
config5432 := ServerConfig{Database: &DatabaseCfg{Host: "test.com", Port: 5432}}
// Law 1: GetSet - lens.Get(lens.Set(a)(s)) == a
t.Run("Law1_GetSet_WithSome", func(t *testing.T) {
// Setting Some(8080) and getting back should return Some(8080)
result := configPortLens.Get(configPortLens.Set(O.Some(8080))(config3306))
assert.True(t, eqOptInt.Equals(result, O.Some(8080)),
"Get(Set(Some(8080))(s)) should equal Some(8080)")
})
t.Run("Law1_GetSet_WithNone", func(t *testing.T) {
// Setting None and getting back should return None
result := configPortLens.Get(configPortLens.Set(O.None[int]())(config3306))
assert.True(t, eqOptInt.Equals(result, O.None[int]()),
"Get(Set(None)(s)) should equal None")
})
t.Run("Law1_GetSet_OnEmptyWithSome", func(t *testing.T) {
// Setting Some on empty config and getting back
result := configPortLens.Get(configPortLens.Set(O.Some(9000))(configNil))
assert.True(t, eqOptInt.Equals(result, O.Some(9000)),
"Get(Set(Some(9000))(empty)) should equal Some(9000)")
})
// Law 2: SetGet - lens.Set(lens.Get(s))(s) == s
t.Run("Law2_SetGet_WithDatabase", func(t *testing.T) {
// Setting what we get should return the same structure
result := configPortLens.Set(configPortLens.Get(config3306))(config3306)
assert.True(t, eqServerConfig(result, config3306),
"Set(Get(s))(s) should equal s")
})
t.Run("Law2_SetGet_WithoutDatabase", func(t *testing.T) {
// Setting what we get from empty should return the same structure
result := configPortLens.Set(configPortLens.Get(configNil))(configNil)
assert.True(t, eqServerConfig(result, configNil),
"Set(Get(empty))(empty) should equal empty")
})
t.Run("Law2_SetGet_DifferentConfigs", func(t *testing.T) {
// Test with another config
result := configPortLens.Set(configPortLens.Get(config5432))(config5432)
assert.True(t, eqServerConfig(result, config5432),
"Set(Get(s))(s) should equal s for any s")
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("Law3_SetSet_BothSome", func(t *testing.T) {
// Setting twice with Some should be same as setting once
setTwice := configPortLens.Set(O.Some(9000))(configPortLens.Set(O.Some(8080))(config3306))
setOnce := configPortLens.Set(O.Some(9000))(config3306)
assert.True(t, eqServerConfig(setTwice, setOnce),
"Set(a2)(Set(a1)(s)) should equal Set(a2)(s)")
})
t.Run("Law3_SetSet_BothNone", func(t *testing.T) {
// Setting None twice should be same as setting once
setTwice := configPortLens.Set(O.None[int]())(configPortLens.Set(O.None[int]())(config3306))
setOnce := configPortLens.Set(O.None[int]())(config3306)
assert.True(t, eqServerConfig(setTwice, setOnce),
"Set(None)(Set(None)(s)) should equal Set(None)(s)")
})
t.Run("Law3_SetSet_SomeThenNone", func(t *testing.T) {
// Setting None after Some should be same as setting None directly
setTwice := configPortLens.Set(O.None[int]())(configPortLens.Set(O.Some(8080))(config3306))
setOnce := configPortLens.Set(O.None[int]())(config3306)
assert.True(t, eqServerConfig(setTwice, setOnce),
"Set(None)(Set(Some)(s)) should equal Set(None)(s)")
})
t.Run("Law3_SetSet_NoneThenSome", func(t *testing.T) {
// Setting Some after None creates a new database with default values
// This is different from setting Some directly which preserves existing fields
setTwice := configPortLens.Set(O.Some(8080))(configPortLens.Set(O.None[int]())(config3306))
// After setting None, the database is removed, so setting Some creates it with defaults
assert.NotNil(t, setTwice.Database)
assert.Equal(t, 8080, setTwice.Database.Port)
assert.Equal(t, "localhost", setTwice.Database.Host) // From default, not "example.com"
// This demonstrates that ComposeOption's behavior when setting None then Some
// uses the default value for the intermediate structure
setOnce := configPortLens.Set(O.Some(8080))(config3306)
assert.Equal(t, 8080, setOnce.Database.Port)
assert.Equal(t, "example.com", setOnce.Database.Host) // Preserved from original
// They are NOT equal because the Host field differs
assert.False(t, eqServerConfig(setTwice, setOnce),
"Set(Some)(Set(None)(s)) uses default, Set(Some)(s) preserves fields")
})
t.Run("Law3_SetSet_OnEmpty", func(t *testing.T) {
// Setting twice on empty config
setTwice := configPortLens.Set(O.Some(9000))(configPortLens.Set(O.Some(8080))(configNil))
setOnce := configPortLens.Set(O.Some(9000))(configNil)
assert.True(t, eqServerConfig(setTwice, setOnce),
"Set(a2)(Set(a1)(empty)) should equal Set(a2)(empty)")
})
}
// TestComposeOptionWithModify tests the Modify operation
func TestComposeOptionWithModify(t *testing.T) {
defaultDB := &DatabaseCfg{Host: "localhost", Port: 5432}
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
t.Run("Modify with identity returns same structure", func(t *testing.T) {
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
result := L.Modify[ServerConfig](F.Identity[Option[int]])(configPortLens)(config)
assert.Equal(t, config.Database.Port, result.Database.Port)
assert.Equal(t, config.Database.Host, result.Database.Host)
})
t.Run("Modify with Some transformation", func(t *testing.T) {
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
// Double the port if it exists
doublePort := O.Map(func(p int) int { return p * 2 })
result := L.Modify[ServerConfig](doublePort)(configPortLens)(config)
assert.Equal(t, 6612, result.Database.Port)
assert.Equal(t, "example.com", result.Database.Host)
})
t.Run("Modify on empty config with Some transformation", func(t *testing.T) {
config := ServerConfig{Database: nil}
doublePort := O.Map(func(p int) int { return p * 2 })
result := L.Modify[ServerConfig](doublePort)(configPortLens)(config)
// Should remain empty since there's nothing to modify
assert.Nil(t, result.Database)
})
}
// TestComposeOptionComposition tests composing multiple ComposeOption lenses
func TestComposeOptionComposition(t *testing.T) {
type Level3 struct {
Value int
}
type Level2 struct {
Level3 *Level3
}
type Level1 struct {
Level2 *Level2
}
// Create lenses
level2Lens := FromNillable(L.MakeLens(
func(l1 Level1) *Level2 { return l1.Level2 },
func(l1 Level1, l2 *Level2) Level1 { l1.Level2 = l2; return l1 },
))
level3Lens := L.MakeLensRef(
func(l2 *Level2) *Level3 { return l2.Level3 },
func(l2 *Level2, l3 *Level3) *Level2 { l2.Level3 = l3; return l2 },
)
valueLens := L.MakeLensRef(
func(l3 *Level3) int { return l3.Value },
func(l3 *Level3, v int) *Level3 { l3.Value = v; return l3 },
)
// Compose: Level1 -> Option[Level2] -> Option[Level3] -> Option[int]
defaultLevel2 := &Level2{Level3: &Level3{Value: 0}}
defaultLevel3 := &Level3{Value: 0}
// First composition: Level1 -> Option[Level3]
level1ToLevel3 := F.Pipe1(level2Lens, ComposeOption[Level1, *Level3](defaultLevel2)(level3Lens))
// Second composition: Level1 -> Option[int]
level1ToValue := F.Pipe1(level1ToLevel3, ComposeOption[Level1, int](defaultLevel3)(valueLens))
t.Run("Get from fully populated structure", func(t *testing.T) {
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: 42}}}
result := level1ToValue.Get(l1)
assert.Equal(t, O.Some(42), result)
})
t.Run("Get from empty structure", func(t *testing.T) {
l1 := Level1{Level2: nil}
result := level1ToValue.Get(l1)
assert.True(t, O.IsNone(result))
})
t.Run("Set on empty structure creates all levels", func(t *testing.T) {
l1 := Level1{Level2: nil}
updated := level1ToValue.Set(O.Some(100))(l1)
assert.NotNil(t, updated.Level2)
assert.NotNil(t, updated.Level2.Level3)
assert.Equal(t, 100, updated.Level2.Level3.Value)
})
t.Run("Set None removes top level", func(t *testing.T) {
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: 42}}}
updated := level1ToValue.Set(O.None[int]())(l1)
assert.Nil(t, updated.Level2)
})
}
// TestComposeOptionEdgeCasesExtended tests additional edge cases
func TestComposeOptionEdgeCasesExtended(t *testing.T) {
defaultSettings := &AppSettings{MaxRetries: 3, Timeout: 30}
settingsLens := FromNillable(L.MakeLens(ApplicationConfig.GetSettings, ApplicationConfig.SetSettings))
retriesLens := L.MakeLensRef((*AppSettings).GetMaxRetries, (*AppSettings).SetMaxRetries)
configRetriesLens := F.Pipe1(settingsLens, ComposeOption[ApplicationConfig, int](defaultSettings)(retriesLens))
t.Run("Multiple sets with different values", func(t *testing.T) {
config := ApplicationConfig{Settings: nil}
// Set multiple times
config = configRetriesLens.Set(O.Some(5))(config)
assert.Equal(t, 5, config.Settings.MaxRetries)
config = configRetriesLens.Set(O.Some(10))(config)
assert.Equal(t, 10, config.Settings.MaxRetries)
config = configRetriesLens.Set(O.None[int]())(config)
assert.Nil(t, config.Settings)
})
t.Run("Get after Set maintains consistency", func(t *testing.T) {
config := ApplicationConfig{Settings: nil}
updated := configRetriesLens.Set(O.Some(7))(config)
retrieved := configRetriesLens.Get(updated)
assert.Equal(t, O.Some(7), retrieved)
})
t.Run("Default values are used correctly", func(t *testing.T) {
config := ApplicationConfig{Settings: nil}
updated := configRetriesLens.Set(O.Some(15))(config)
// Check that default timeout is used
assert.Equal(t, 30, updated.Settings.Timeout)
assert.Equal(t, 15, updated.Settings.MaxRetries)
})
t.Run("Preserves other fields when updating", func(t *testing.T) {
config := ApplicationConfig{Settings: &AppSettings{MaxRetries: 5, Timeout: 60}}
updated := configRetriesLens.Set(O.Some(10))(config)
assert.Equal(t, 10, updated.Settings.MaxRetries)
assert.Equal(t, 60, updated.Settings.Timeout) // Preserved
})
}
// TestComposeOptionWithZeroValues tests behavior with zero values
func TestComposeOptionWithZeroValues(t *testing.T) {
defaultDB := &DatabaseCfg{Host: "", Port: 0}
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
t.Run("Set zero value", func(t *testing.T) {
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
updated := configPortLens.Set(O.Some(0))(config)
assert.Equal(t, 0, updated.Database.Port)
assert.Equal(t, "example.com", updated.Database.Host)
})
t.Run("Get zero value returns Some(0)", func(t *testing.T) {
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 0}}
result := configPortLens.Get(config)
assert.Equal(t, O.Some(0), result)
})
t.Run("Default with zero values", func(t *testing.T) {
config := ServerConfig{Database: nil}
updated := configPortLens.Set(O.Some(8080))(config)
assert.Equal(t, "", updated.Database.Host) // From default
assert.Equal(t, 8080, updated.Database.Port)
})
}
// ============================================================================
// Tests for Compose function (both lenses return Option values)
// ============================================================================
// TestComposeBasicOperations tests basic get/set operations for Compose
func TestComposeBasicOperations(t *testing.T) {
type Value struct {
Data *string
}
type Container struct {
Value *Value
}
// Create lenses
valueLens := FromNillable(L.MakeLens(
func(c Container) *Value { return c.Value },
func(c Container, v *Value) Container { c.Value = v; return c },
))
dataLens := L.MakeLensRef(
func(v *Value) *string { return v.Data },
func(v *Value, d *string) *Value { v.Data = d; return v },
)
defaultValue := &Value{Data: nil}
composedLens := F.Pipe1(valueLens, Compose[Container, *string](defaultValue)(
FromNillable(dataLens),
))
t.Run("Get from empty container returns None", func(t *testing.T) {
container := Container{Value: nil}
result := composedLens.Get(container)
assert.True(t, O.IsNone(result))
})
t.Run("Get from container with nil data returns None", func(t *testing.T) {
container := Container{Value: &Value{Data: nil}}
result := composedLens.Get(container)
assert.True(t, O.IsNone(result))
})
t.Run("Get from container with data returns Some", func(t *testing.T) {
data := "test"
container := Container{Value: &Value{Data: &data}}
result := composedLens.Get(container)
assert.True(t, O.IsSome(result))
assert.Equal(t, &data, O.GetOrElse(func() *string { return nil })(result))
})
t.Run("Set Some on empty container creates structure with default", func(t *testing.T) {
container := Container{Value: nil}
data := "new"
updated := composedLens.Set(O.Some(&data))(container)
assert.NotNil(t, updated.Value)
assert.NotNil(t, updated.Value.Data)
assert.Equal(t, "new", *updated.Value.Data)
})
t.Run("Set Some on existing container updates data", func(t *testing.T) {
oldData := "old"
container := Container{Value: &Value{Data: &oldData}}
newData := "new"
updated := composedLens.Set(O.Some(&newData))(container)
assert.NotNil(t, updated.Value)
assert.NotNil(t, updated.Value.Data)
assert.Equal(t, "new", *updated.Value.Data)
})
t.Run("Set None when container is empty is no-op", func(t *testing.T) {
container := Container{Value: nil}
updated := composedLens.Set(O.None[*string]())(container)
assert.Nil(t, updated.Value)
})
t.Run("Set None when container exists unsets data", func(t *testing.T) {
data := "test"
container := Container{Value: &Value{Data: &data}}
updated := composedLens.Set(O.None[*string]())(container)
assert.NotNil(t, updated.Value)
assert.Nil(t, updated.Value.Data)
})
}
// TestComposeLensLawsDetailed verifies that Compose satisfies lens laws
func TestComposeLensLawsDetailed(t *testing.T) {
type Inner struct {
Value *int
Extra string
}
type Outer struct {
Inner *Inner
}
// Setup
defaultInner := &Inner{Value: nil, Extra: "default"}
innerLens := FromNillable(L.MakeLens(
func(o Outer) *Inner { return o.Inner },
func(o Outer, i *Inner) Outer { o.Inner = i; return o },
))
valueLens := L.MakeLensRef(
func(i *Inner) *int { return i.Value },
func(i *Inner, v *int) *Inner { i.Value = v; return i },
)
composedLens := F.Pipe1(innerLens, Compose[Outer, *int](defaultInner)(
FromNillable(valueLens),
))
// Equality predicates
eqIntPtr := EQT.Eq[*int]()
eqOptIntPtr := O.Eq(eqIntPtr)
eqOuter := func(a, b Outer) bool {
if a.Inner == nil && b.Inner == nil {
return true
}
if a.Inner == nil || b.Inner == nil {
return false
}
aVal := a.Inner.Value
bVal := b.Inner.Value
if aVal == nil && bVal == nil {
return a.Inner.Extra == b.Inner.Extra
}
if aVal == nil || bVal == nil {
return false
}
return *aVal == *bVal && a.Inner.Extra == b.Inner.Extra
}
// Test structures
val42 := 42
val100 := 100
outerNil := Outer{Inner: nil}
outerWithNilValue := Outer{Inner: &Inner{Value: nil, Extra: "test"}}
outer42 := Outer{Inner: &Inner{Value: &val42, Extra: "test"}}
// Law 1: GetSet - lens.Get(lens.Set(a)(s)) == a
t.Run("Law1_GetSet_WithSome", func(t *testing.T) {
result := composedLens.Get(composedLens.Set(O.Some(&val100))(outer42))
assert.True(t, eqOptIntPtr.Equals(result, O.Some(&val100)),
"Get(Set(Some(100))(s)) should equal Some(100)")
})
t.Run("Law1_GetSet_WithNone", func(t *testing.T) {
result := composedLens.Get(composedLens.Set(O.None[*int]())(outer42))
assert.True(t, eqOptIntPtr.Equals(result, O.None[*int]()),
"Get(Set(None)(s)) should equal None")
})
t.Run("Law1_GetSet_OnEmpty", func(t *testing.T) {
result := composedLens.Get(composedLens.Set(O.Some(&val100))(outerNil))
assert.True(t, eqOptIntPtr.Equals(result, O.Some(&val100)),
"Get(Set(Some(100))(empty)) should equal Some(100)")
})
// Law 2: SetGet - lens.Set(lens.Get(s))(s) == s
t.Run("Law2_SetGet_WithValue", func(t *testing.T) {
result := composedLens.Set(composedLens.Get(outer42))(outer42)
assert.True(t, eqOuter(result, outer42),
"Set(Get(s))(s) should equal s")
})
t.Run("Law2_SetGet_WithNilValue", func(t *testing.T) {
result := composedLens.Set(composedLens.Get(outerWithNilValue))(outerWithNilValue)
assert.True(t, eqOuter(result, outerWithNilValue),
"Set(Get(s))(s) should equal s when value is nil")
})
t.Run("Law2_SetGet_WithNilInner", func(t *testing.T) {
result := composedLens.Set(composedLens.Get(outerNil))(outerNil)
assert.True(t, eqOuter(result, outerNil),
"Set(Get(empty))(empty) should equal empty")
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("Law3_SetSet_BothSome", func(t *testing.T) {
val200 := 200
setTwice := composedLens.Set(O.Some(&val200))(composedLens.Set(O.Some(&val100))(outer42))
setOnce := composedLens.Set(O.Some(&val200))(outer42)
assert.True(t, eqOuter(setTwice, setOnce),
"Set(a2)(Set(a1)(s)) should equal Set(a2)(s)")
})
t.Run("Law3_SetSet_BothNone", func(t *testing.T) {
setTwice := composedLens.Set(O.None[*int]())(composedLens.Set(O.None[*int]())(outer42))
setOnce := composedLens.Set(O.None[*int]())(outer42)
assert.True(t, eqOuter(setTwice, setOnce),
"Set(None)(Set(None)(s)) should equal Set(None)(s)")
})
t.Run("Law3_SetSet_SomeThenNone", func(t *testing.T) {
setTwice := composedLens.Set(O.None[*int]())(composedLens.Set(O.Some(&val100))(outer42))
setOnce := composedLens.Set(O.None[*int]())(outer42)
assert.True(t, eqOuter(setTwice, setOnce),
"Set(None)(Set(Some)(s)) should equal Set(None)(s)")
})
t.Run("Law3_SetSet_NoneThenSome", func(t *testing.T) {
// This case is interesting: setting None then Some uses default
setTwice := composedLens.Set(O.Some(&val100))(composedLens.Set(O.None[*int]())(outer42))
// After None, inner still exists but value is nil
// Then setting Some updates the value
assert.NotNil(t, setTwice.Inner)
assert.NotNil(t, setTwice.Inner.Value)
assert.Equal(t, 100, *setTwice.Inner.Value)
assert.Equal(t, "test", setTwice.Inner.Extra) // Preserved from original
})
}
// TestComposeWithModify tests the Modify operation for Compose
func TestComposeWithModify(t *testing.T) {
type Data struct {
Count *int
}
type Store struct {
Data *Data
}
defaultData := &Data{Count: nil}
dataLens := FromNillable(L.MakeLens(
func(s Store) *Data { return s.Data },
func(s Store, d *Data) Store { s.Data = d; return s },
))
countLens := L.MakeLensRef(
func(d *Data) *int { return d.Count },
func(d *Data, c *int) *Data { d.Count = c; return d },
)
composedLens := F.Pipe1(dataLens, Compose[Store, *int](defaultData)(
FromNillable(countLens),
))
t.Run("Modify with identity returns same structure", func(t *testing.T) {
count := 5
store := Store{Data: &Data{Count: &count}}
result := L.Modify[Store](F.Identity[Option[*int]])(composedLens)(store)
assert.Equal(t, 5, *result.Data.Count)
})
t.Run("Modify with Some transformation", func(t *testing.T) {
count := 5
store := Store{Data: &Data{Count: &count}}
// Double the count if it exists
doubleCount := O.Map(func(c *int) *int {
doubled := *c * 2
return &doubled
})
result := L.Modify[Store](doubleCount)(composedLens)(store)
assert.Equal(t, 10, *result.Data.Count)
})
t.Run("Modify on empty store", func(t *testing.T) {
store := Store{Data: nil}
doubleCount := O.Map(func(c *int) *int {
doubled := *c * 2
return &doubled
})
result := L.Modify[Store](doubleCount)(composedLens)(store)
// Should remain empty since there's nothing to modify
assert.Nil(t, result.Data)
})
}
// TestComposeMultiLevel tests composing multiple Compose operations
func TestComposeMultiLevel(t *testing.T) {
type Level3 struct {
Value *string
}
type Level2 struct {
Level3 *Level3
}
type Level1 struct {
Level2 *Level2
}
// Create lenses
level2Lens := FromNillable(L.MakeLens(
func(l1 Level1) *Level2 { return l1.Level2 },
func(l1 Level1, l2 *Level2) Level1 { l1.Level2 = l2; return l1 },
))
level3Lens := L.MakeLensRef(
func(l2 *Level2) *Level3 { return l2.Level3 },
func(l2 *Level2, l3 *Level3) *Level2 { l2.Level3 = l3; return l2 },
)
valueLens := L.MakeLensRef(
func(l3 *Level3) *string { return l3.Value },
func(l3 *Level3, v *string) *Level3 { l3.Value = v; return l3 },
)
// Compose: Level1 -> Option[Level2] -> Option[Level3] -> Option[string]
defaultLevel2 := &Level2{Level3: nil}
defaultLevel3 := &Level3{Value: nil}
// First composition: Level1 -> Option[Level3]
level1ToLevel3 := F.Pipe1(level2Lens, Compose[Level1, *Level3](defaultLevel2)(
FromNillable(level3Lens),
))
// Second composition: Level1 -> Option[string]
level1ToValue := F.Pipe1(level1ToLevel3, Compose[Level1, *string](defaultLevel3)(
FromNillable(valueLens),
))
t.Run("Get from fully populated structure", func(t *testing.T) {
value := "test"
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: &value}}}
result := level1ToValue.Get(l1)
assert.True(t, O.IsSome(result))
})
t.Run("Get from partially populated structure", func(t *testing.T) {
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: nil}}}
result := level1ToValue.Get(l1)
assert.True(t, O.IsNone(result))
})
t.Run("Get from empty structure", func(t *testing.T) {
l1 := Level1{Level2: nil}
result := level1ToValue.Get(l1)
assert.True(t, O.IsNone(result))
})
t.Run("Set on empty structure creates all levels", func(t *testing.T) {
l1 := Level1{Level2: nil}
value := "new"
updated := level1ToValue.Set(O.Some(&value))(l1)
assert.NotNil(t, updated.Level2)
assert.NotNil(t, updated.Level2.Level3)
assert.NotNil(t, updated.Level2.Level3.Value)
assert.Equal(t, "new", *updated.Level2.Level3.Value)
})
t.Run("Set None when structure exists unsets value", func(t *testing.T) {
value := "test"
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: &value}}}
updated := level1ToValue.Set(O.None[*string]())(l1)
assert.NotNil(t, updated.Level2)
assert.NotNil(t, updated.Level2.Level3)
assert.Nil(t, updated.Level2.Level3.Value)
})
}
// TestComposeEdgeCasesExtended tests additional edge cases for Compose
func TestComposeEdgeCasesExtended(t *testing.T) {
type Metadata struct {
Tags *[]string
}
type Document struct {
Metadata *Metadata
}
defaultMetadata := &Metadata{Tags: nil}
metadataLens := FromNillable(L.MakeLens(
func(d Document) *Metadata { return d.Metadata },
func(d Document, m *Metadata) Document { d.Metadata = m; return d },
))
tagsLens := L.MakeLensRef(
func(m *Metadata) *[]string { return m.Tags },
func(m *Metadata, t *[]string) *Metadata { m.Tags = t; return m },
)
composedLens := F.Pipe1(metadataLens, Compose[Document, *[]string](defaultMetadata)(
FromNillable(tagsLens),
))
t.Run("Multiple sets with different values", func(t *testing.T) {
doc := Document{Metadata: nil}
tags1 := []string{"tag1"}
tags2 := []string{"tag2", "tag3"}
// Set first value
doc = composedLens.Set(O.Some(&tags1))(doc)
assert.NotNil(t, doc.Metadata)
assert.NotNil(t, doc.Metadata.Tags)
assert.Equal(t, 1, len(*doc.Metadata.Tags))
// Set second value
doc = composedLens.Set(O.Some(&tags2))(doc)
assert.Equal(t, 2, len(*doc.Metadata.Tags))
// Set None
doc = composedLens.Set(O.None[*[]string]())(doc)
assert.NotNil(t, doc.Metadata)
assert.Nil(t, doc.Metadata.Tags)
})
t.Run("Get after Set maintains consistency", func(t *testing.T) {
doc := Document{Metadata: nil}
tags := []string{"test"}
updated := composedLens.Set(O.Some(&tags))(doc)
retrieved := composedLens.Get(updated)
assert.True(t, O.IsSome(retrieved))
})
t.Run("Default values are used when creating structure", func(t *testing.T) {
doc := Document{Metadata: nil}
tags := []string{"new"}
updated := composedLens.Set(O.Some(&tags))(doc)
// Metadata should be created with default (Tags: nil initially, then set)
assert.NotNil(t, updated.Metadata)
assert.NotNil(t, updated.Metadata.Tags)
assert.Equal(t, []string{"new"}, *updated.Metadata.Tags)
})
}

View File

@@ -3,6 +3,7 @@ package option
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/lens"
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
O "github.com/IBM/fp-go/v2/option"
)
@@ -17,30 +18,38 @@ func fromPredicate[GET ~func(S) Option[A], SET ~func(Option[A]) Endomorphism[S],
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
//
//go:inline
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
return fromPredicate(lens.MakeLensCurried[func(S) Option[A], func(Option[A]) Endomorphism[S]], pred, nilValue)
}
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, Option[A]] {
//
//go:inline
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) LensO[*S, A] {
return fromPredicate(lens.MakeLensRefCurried[S, Option[A]], pred, nilValue)
}
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the `nil` value will be set instead
func FromNillable[S, A any](sa Lens[S, *A]) Lens[S, Option[*A]] {
//
//go:inline
func FromNillable[S, A any](sa Lens[S, *A]) LensO[S, *A] {
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
}
// FromNillableRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the `nil` value will be set instead
func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] {
//
//go:inline
func FromNillableRef[S, A any](sa Lens[*S, *A]) LensO[*S, *A] {
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
}
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func fromNullableProp[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
func fromNullableProp[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], isNullable O.Kleisli[A, A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
orElse := O.GetOrElse(F.Constant(defaultValue))
return func(sa Lens[S, A]) Lens[S, A] {
return creator(F.Flow3(
@@ -52,17 +61,21 @@ func fromNullableProp[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](cr
}
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func FromNullableProp[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
//
//go:inline
func FromNullableProp[S, A any](isNullable O.Kleisli[A, A], defaultValue A) lens.Operator[S, A, A] {
return fromNullableProp(lens.MakeLensCurried[func(S) A, func(A) Endomorphism[S]], isNullable, defaultValue)
}
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func FromNullablePropRef[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
//
//go:inline
func FromNullablePropRef[S, A any](isNullable O.Kleisli[A, A], defaultValue A) lens.Operator[*S, A, A] {
return fromNullableProp(lens.MakeLensRefCurried[S, A], isNullable, defaultValue)
}
// fromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
func fromOption[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
func fromOption[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(LensO[S, A]) Lens[S, A] {
orElse := O.GetOrElse(F.Constant(defaultValue))
return func(sa LensO[S, A]) Lens[S, A] {
return creator(F.Flow2(
@@ -73,7 +86,9 @@ func fromOption[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator
}
// FromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
//
//go:inline
func FromOption[S, A any](defaultValue A) func(LensO[S, A]) Lens[S, A] {
return fromOption(lens.MakeLensCurried[func(S) A, func(A) Endomorphism[S]], defaultValue)
}
@@ -92,6 +107,76 @@ func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
//
// Returns:
// - A function that takes a Lens[*S, Option[A]] and returns a Lens[*S, A]
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*S, A] {
//
//go:inline
func FromOptionRef[S, A any](defaultValue A) func(LensO[*S, A]) Lens[*S, A] {
return fromOption(lens.MakeLensRefCurried[S, A], defaultValue)
}
// FromIso converts a Lens[S, A] to a LensO[S, A] using an isomorphism.
//
// This function takes an isomorphism between A and Option[A] and uses it to
// transform a regular lens into an optional lens. It's particularly useful when
// you have a custom isomorphism that defines how to convert between a value
// and its optional representation.
//
// The isomorphism must satisfy the round-trip laws:
// 1. iso.ReverseGet(iso.Get(a)) == a for all a: A
// 2. iso.Get(iso.ReverseGet(opt)) == opt for all opt: Option[A]
//
// Type Parameters:
// - S: The structure type containing the field
// - A: The type of the field being focused on
//
// Parameters:
// - iso: An isomorphism between A and Option[A] that defines the conversion
//
// Returns:
// - A function that takes a Lens[S, A] and returns a LensO[S, A]
//
// Example:
//
// type Config struct {
// timeout int
// }
//
// // Create a lens to the timeout field
// timeoutLens := lens.MakeLens(
// func(c Config) int { return c.timeout },
// func(c Config, t int) Config { c.timeout = t; return c },
// )
//
// // Create an isomorphism that treats 0 as None
// zeroAsNone := iso.MakeIso(
// func(t int) option.Option[int] {
// if t == 0 {
// return option.None[int]()
// }
// return option.Some(t)
// },
// func(opt option.Option[int]) int {
// return option.GetOrElse(func() int { return 0 })(opt)
// },
// )
//
// // Convert to optional lens
// optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
//
// config := Config{timeout: 0}
// opt := optTimeoutLens.Get(config) // None[int]()
// updated := optTimeoutLens.Set(option.Some(30))(config) // Config{timeout: 30}
//
// Common Use Cases:
// - Converting between sentinel values (like 0, -1, "") and Option
// - Applying custom validation logic when converting to/from Option
// - Integrating with existing isomorphisms like FromNillable
//
// See also:
// - FromPredicate: For predicate-based optional conversion
// - FromNillable: For pointer-based optional conversion
// - FromOption: For converting from optional to non-optional with defaults
//
//go:inline
func FromIso[S, A any](iso Iso[A, Option[A]]) func(Lens[S, A]) LensO[S, A] {
return LI.Compose[S](iso)
}

View File

@@ -0,0 +1,479 @@
// 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 option
import (
"testing"
EQT "github.com/IBM/fp-go/v2/eq/testing"
F "github.com/IBM/fp-go/v2/function"
ISO "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// Test types
type Config struct {
timeout int
retries int
}
type Settings struct {
maxConnections int
bufferSize int
}
// TestFromIsoBasic tests basic functionality of FromIso
func TestFromIsoBasic(t *testing.T) {
// Create an isomorphism that treats 0 as None
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
// Create a lens to the timeout field
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
// Convert to optional lens using FromIso
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
t.Run("GetNone", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
result := optTimeoutLens.Get(config)
assert.True(t, O.IsNone(result))
})
t.Run("GetSome", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
result := optTimeoutLens.Get(config)
assert.True(t, O.IsSome(result))
assert.Equal(t, 30, O.GetOrElse(F.Constant(0))(result))
})
t.Run("SetNone", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
updated := optTimeoutLens.Set(O.None[int]())(config)
assert.Equal(t, 0, updated.timeout)
assert.Equal(t, 3, updated.retries) // Other fields unchanged
})
t.Run("SetSome", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
updated := optTimeoutLens.Set(O.Some(60))(config)
assert.Equal(t, 60, updated.timeout)
assert.Equal(t, 3, updated.retries) // Other fields unchanged
})
t.Run("SetPreservesOriginal", func(t *testing.T) {
original := Config{timeout: 30, retries: 3}
_ = optTimeoutLens.Set(O.Some(60))(original)
// Original should be unchanged
assert.Equal(t, 30, original.timeout)
assert.Equal(t, 3, original.retries)
})
}
// TestFromIsoWithNegativeSentinel tests using -1 as a sentinel value
func TestFromIsoWithNegativeSentinel(t *testing.T) {
// Create an isomorphism that treats -1 as None
negativeOneAsNone := ISO.MakeIso(
func(n int) O.Option[int] {
if n == -1 {
return O.None[int]()
}
return O.Some(n)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(-1))(opt)
},
)
retriesLens := L.MakeLens(
func(c Config) int { return c.retries },
func(c Config, r int) Config { c.retries = r; return c },
)
optRetriesLens := FromIso[Config, int](negativeOneAsNone)(retriesLens)
t.Run("GetNoneForNegativeOne", func(t *testing.T) {
config := Config{timeout: 30, retries: -1}
result := optRetriesLens.Get(config)
assert.True(t, O.IsNone(result))
})
t.Run("GetSomeForZero", func(t *testing.T) {
config := Config{timeout: 30, retries: 0}
result := optRetriesLens.Get(config)
assert.True(t, O.IsSome(result))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("SetNoneToNegativeOne", func(t *testing.T) {
config := Config{timeout: 30, retries: 5}
updated := optRetriesLens.Set(O.None[int]())(config)
assert.Equal(t, -1, updated.retries)
})
}
// TestFromIsoLaws verifies that FromIso satisfies lens laws
func TestFromIsoLaws(t *testing.T) {
// Create an isomorphism
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
eqOptInt := O.Eq(EQT.Eq[int]())
eqConfig := EQT.Eq[Config]()
config := Config{timeout: 30, retries: 3}
newValue := O.Some(60)
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := optTimeoutLens.Set(optTimeoutLens.Get(config))(config)
assert.True(t, eqConfig.Equals(config, result))
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := optTimeoutLens.Get(optTimeoutLens.Set(newValue)(config))
assert.True(t, eqOptInt.Equals(newValue, result))
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
a1 := O.Some(60)
a2 := O.None[int]()
result1 := optTimeoutLens.Set(a2)(optTimeoutLens.Set(a1)(config))
result2 := optTimeoutLens.Set(a2)(config)
assert.True(t, eqConfig.Equals(result1, result2))
})
// Use the testing helper to verify all laws
t.Run("AllLaws", func(t *testing.T) {
laws := LT.AssertLaws(t, eqOptInt, eqConfig)(optTimeoutLens)
assert.True(t, laws(config, O.Some(100)))
assert.True(t, laws(Config{timeout: 0, retries: 5}, O.None[int]()))
})
}
// TestFromIsoComposition tests composing FromIso with other lenses
func TestFromIsoComposition(t *testing.T) {
type Application struct {
config Config
}
// Isomorphism for zero as none
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
// Lens to config field
configLens := L.MakeLens(
func(a Application) Config { return a.config },
func(a Application, c Config) Application { a.config = c; return a },
)
// Lens to timeout field
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
// Compose: Application -> Config -> timeout (as Option)
optTimeoutFromConfig := FromIso[Config, int](zeroAsNone)(timeoutLens)
optTimeoutFromApp := F.Pipe1(
configLens,
L.Compose[Application](optTimeoutFromConfig),
)
app := Application{config: Config{timeout: 0, retries: 3}}
t.Run("ComposedGet", func(t *testing.T) {
result := optTimeoutFromApp.Get(app)
assert.True(t, O.IsNone(result))
})
t.Run("ComposedSet", func(t *testing.T) {
updated := optTimeoutFromApp.Set(O.Some(45))(app)
assert.Equal(t, 45, updated.config.timeout)
assert.Equal(t, 3, updated.config.retries)
})
}
// TestFromIsoModify tests using Modify with FromIso-based lenses
func TestFromIsoModify(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
t.Run("ModifyNoneToSome", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
// Map None to Some(10)
modified := L.Modify[Config](O.Map(func(x int) int { return x + 10 }))(optTimeoutLens)(config)
// Since it was None, Map doesn't apply, stays None (0)
assert.Equal(t, 0, modified.timeout)
})
t.Run("ModifySomeValue", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
// Double the timeout value
modified := L.Modify[Config](O.Map(func(x int) int { return x * 2 }))(optTimeoutLens)(config)
assert.Equal(t, 60, modified.timeout)
})
t.Run("ModifyWithAlt", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
// Use Alt to provide a default
modified := L.Modify[Config](func(opt O.Option[int]) O.Option[int] {
return O.Alt(F.Constant(O.Some(10)))(opt)
})(optTimeoutLens)(config)
assert.Equal(t, 10, modified.timeout)
})
}
// TestFromIsoWithStringEmpty tests using empty string as None
func TestFromIsoWithStringEmpty(t *testing.T) {
type User struct {
name string
email string
}
// Isomorphism that treats empty string as None
emptyAsNone := ISO.MakeIso(
func(s string) O.Option[string] {
if s == "" {
return O.None[string]()
}
return O.Some(s)
},
func(opt O.Option[string]) string {
return O.GetOrElse(F.Constant(""))(opt)
},
)
emailLens := L.MakeLens(
func(u User) string { return u.email },
func(u User, e string) User { u.email = e; return u },
)
optEmailLens := FromIso[User, string](emptyAsNone)(emailLens)
t.Run("EmptyStringAsNone", func(t *testing.T) {
user := User{name: "Alice", email: ""}
result := optEmailLens.Get(user)
assert.True(t, O.IsNone(result))
})
t.Run("NonEmptyStringAsSome", func(t *testing.T) {
user := User{name: "Alice", email: "alice@example.com"}
result := optEmailLens.Get(user)
assert.True(t, O.IsSome(result))
assert.Equal(t, "alice@example.com", O.GetOrElse(F.Constant(""))(result))
})
t.Run("SetNoneToEmpty", func(t *testing.T) {
user := User{name: "Alice", email: "alice@example.com"}
updated := optEmailLens.Set(O.None[string]())(user)
assert.Equal(t, "", updated.email)
})
}
// TestFromIsoRoundTrip tests round-trip conversions
func TestFromIsoRoundTrip(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
maxConnectionsLens := L.MakeLens(
func(s Settings) int { return s.maxConnections },
func(s Settings, m int) Settings { s.maxConnections = m; return s },
)
optMaxConnectionsLens := FromIso[Settings, int](zeroAsNone)(maxConnectionsLens)
t.Run("RoundTripThroughGet", func(t *testing.T) {
settings := Settings{maxConnections: 100, bufferSize: 1024}
// Get the value, then Set it back
opt := optMaxConnectionsLens.Get(settings)
restored := optMaxConnectionsLens.Set(opt)(settings)
assert.Equal(t, settings, restored)
})
t.Run("RoundTripThroughSet", func(t *testing.T) {
settings := Settings{maxConnections: 0, bufferSize: 1024}
// Set a new value, then Get it
newOpt := O.Some(200)
updated := optMaxConnectionsLens.Set(newOpt)(settings)
retrieved := optMaxConnectionsLens.Get(updated)
assert.True(t, O.Eq(EQT.Eq[int]()).Equals(newOpt, retrieved))
})
t.Run("RoundTripWithNone", func(t *testing.T) {
settings := Settings{maxConnections: 100, bufferSize: 1024}
// Set None, then get it back
updated := optMaxConnectionsLens.Set(O.None[int]())(settings)
retrieved := optMaxConnectionsLens.Get(updated)
assert.True(t, O.IsNone(retrieved))
})
}
// TestFromIsoChaining tests chaining multiple FromIso transformations
func TestFromIsoChaining(t *testing.T) {
// Create two different isomorphisms
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
config := Config{timeout: 30, retries: 3}
t.Run("ChainedOperations", func(t *testing.T) {
// Chain multiple operations
result := F.Pipe2(
config,
optTimeoutLens.Set(O.Some(60)),
optTimeoutLens.Set(O.None[int]()),
)
assert.Equal(t, 0, result.timeout)
})
}
// TestFromIsoMultipleFields tests using FromIso on multiple fields
func TestFromIsoMultipleFields(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
retriesLens := L.MakeLens(
func(c Config) int { return c.retries },
func(c Config, r int) Config { c.retries = r; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
optRetriesLens := FromIso[Config, int](zeroAsNone)(retriesLens)
t.Run("IndependentFields", func(t *testing.T) {
config := Config{timeout: 0, retries: 5}
// Get both fields
timeoutOpt := optTimeoutLens.Get(config)
retriesOpt := optRetriesLens.Get(config)
assert.True(t, O.IsNone(timeoutOpt))
assert.True(t, O.IsSome(retriesOpt))
assert.Equal(t, 5, O.GetOrElse(F.Constant(0))(retriesOpt))
})
t.Run("SetBothFields", func(t *testing.T) {
config := Config{timeout: 0, retries: 0}
// Set both fields
updated := F.Pipe2(
config,
optTimeoutLens.Set(O.Some(30)),
optRetriesLens.Set(O.Some(3)),
)
assert.Equal(t, 30, updated.timeout)
assert.Equal(t, 3, updated.retries)
})
}

View File

@@ -16,7 +16,6 @@
package option
import (
L "github.com/IBM/fp-go/v2/optics/lens"
LG "github.com/IBM/fp-go/v2/optics/lens/generic"
T "github.com/IBM/fp-go/v2/optics/traversal/option"
O "github.com/IBM/fp-go/v2/option"
@@ -60,6 +59,6 @@ import (
// // Now can use traversal operations
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
// // Apply operations across all configs using the traversal
func AsTraversal[S, A any]() func(L.Lens[S, A]) T.Traversal[S, A] {
func AsTraversal[S, A any]() func(Lens[S, A]) T.Traversal[S, A] {
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
}

View File

@@ -17,8 +17,10 @@ package option
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
)
type (
@@ -91,4 +93,9 @@ type (
// optLens := lens.FromNillableRef(timeoutLens)
// // optLens is a LensO[*Config, *int]
LensO[S, A any] = Lens[S, Option[A]]
Kleisli[S, A, B any] = reader.Reader[A, LensO[S, B]]
Operator[S, A, B any] = Kleisli[S, LensO[S, A], B]
Iso[S, A any] = iso.Iso[S, A]
)

View File

@@ -80,4 +80,7 @@ type (
// with the focused value updated to a. The original structure is never modified.
Set func(a A) Endomorphism[S]
}
Kleisli[S, A, B any] = func(A) Lens[S, B]
Operator[S, A, B any] = Kleisli[S, Lens[S, A], B]
)

477
v2/optics/optional/doc.go Normal file
View File

@@ -0,0 +1,477 @@
// 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 optional provides optional optics for focusing on values that may not exist.
# Overview
An Optional is an optic that focuses on a subpart of a data structure that may or may not
be present. Unlike lenses which always focus on an existing field, optionals handle cases
where the target value might be absent, returning Option[A] instead of A.
Optionals are the bridge between lenses (which always succeed) and prisms (which may fail
to match). They combine aspects of both:
- Like lenses: Focus on a specific location in a structure
- Like prisms: The value at that location may not exist
Optionals are essential for:
- Working with nullable fields (pointers that may be nil)
- Accessing nested optional values
- Conditional updates based on value presence
- Safe navigation through potentially missing data
# Mathematical Foundation
An Optional[S, A] consists of two operations:
- GetOption: S → Option[A] (try to extract A from S, may return None)
- Set: A → S → S (update A in S, may be a no-op if value doesn't exist)
Optionals must satisfy the optional laws:
1. GetOptionSet: if GetOption(s) == Some(a), then GetOption(Set(a)(s)) == Some(a)
2. SetGetOption: if GetOption(s) == Some(a), then Set(a)(s) preserves other parts of s
3. SetSet: Set(a2)(Set(a1)(s)) == Set(a2)(s)
# Basic Usage
Creating an optional for a nullable field:
type Config struct {
Timeout *int
MaxSize *int
}
timeoutOptional := optional.MakeOptional(
func(c Config) option.Option[*int] {
return option.FromNillable(c.Timeout)
},
func(c Config, t *int) Config {
c.Timeout = t
return c
},
)
config := Config{Timeout: nil, MaxSize: ptr(100)}
// Get returns None for nil
timeout := timeoutOptional.GetOption(config) // None[*int]
// Set updates the value
newTimeout := 30
updated := timeoutOptional.Set(&newTimeout)(config)
// updated.Timeout points to 30
# Working with Pointers
For pointer-based structures, use MakeOptionalRef which handles copying automatically:
type Database struct {
Host string
Port int
}
type Config struct {
Database *Database
}
dbOptional := optional.MakeOptionalRef(
func(c *Config) option.Option[*Database] {
return option.FromNillable(c.Database)
},
func(c *Config, db *Database) *Config {
c.Database = db
return c
},
)
config := &Config{Database: nil}
// Get returns None when database is nil
db := dbOptional.GetOption(config) // None[*Database]
// Set creates a new config with the database
newDB := &Database{Host: "localhost", Port: 5432}
updated := dbOptional.Set(newDB)(config)
// config.Database is still nil, updated.Database points to newDB
# Identity Optional
The identity optional focuses on the entire structure:
idOpt := optional.Id[Config]()
config := Config{Timeout: ptr(30)}
value := idOpt.GetOption(config) // Some(config)
updated := idOpt.Set(Config{Timeout: ptr(60)})(config)
# Composing Optionals
Optionals can be composed to navigate through nested optional structures:
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address *Address
}
addressOpt := optional.MakeOptional(
func(p Person) option.Option[*Address] {
return option.FromNillable(p.Address)
},
func(p Person, a *Address) Person {
p.Address = a
return p
},
)
cityOpt := optional.MakeOptionalRef(
func(a *Address) option.Option[string] {
if a == nil {
return option.None[string]()
}
return option.Some(a.City)
},
func(a *Address, city string) *Address {
a.City = city
return a
},
)
// Compose to access city from person
personCityOpt := F.Pipe1(
addressOpt,
optional.Compose[Person, *Address, string](cityOpt),
)
person := Person{Name: "Alice", Address: nil}
// Get returns None when address is nil
city := personCityOpt.GetOption(person) // None[string]
// Set updates the city if address exists
withAddress := Person{
Name: "Alice",
Address: &Address{Street: "Main St", City: "NYC"},
}
updated := personCityOpt.Set("Boston")(withAddress)
// updated.Address.City == "Boston"
# From Predicate
Create an optional that only focuses on values satisfying a predicate:
type User struct {
Age int
}
ageOpt := optional.FromPredicate[User, int](
func(age int) bool { return age >= 18 },
)(
func(u User) int { return u.Age },
func(u User, age int) User {
u.Age = age
return u
},
)
adult := User{Age: 25}
age := ageOpt.GetOption(adult) // Some(25)
minor := User{Age: 15}
minorAge := ageOpt.GetOption(minor) // None[int]
// Set only works if predicate is satisfied
updated := ageOpt.Set(30)(adult) // Age becomes 30
unchanged := ageOpt.Set(30)(minor) // Age stays 15 (predicate fails)
# Modifying Values
Use ModifyOption to transform values that exist:
type Counter struct {
Value *int
}
valueOpt := optional.MakeOptional(
func(c Counter) option.Option[*int] {
return option.FromNillable(c.Value)
},
func(c Counter, v *int) Counter {
c.Value = v
return c
},
)
counter := Counter{Value: ptr(5)}
// Increment if value exists
incremented := F.Pipe3(
counter,
valueOpt,
optional.ModifyOption[Counter, *int](func(v *int) *int {
newVal := *v + 1
return &newVal
}),
option.GetOrElse(F.Constant(counter)),
)
// incremented.Value points to 6
// No change if value is nil
nilCounter := Counter{Value: nil}
result := F.Pipe3(
nilCounter,
valueOpt,
optional.ModifyOption[Counter, *int](func(v *int) *int {
newVal := *v + 1
return &newVal
}),
option.GetOrElse(F.Constant(nilCounter)),
)
// result.Value is still nil
# Bidirectional Mapping
Transform the focus type of an optional:
type Celsius float64
type Fahrenheit float64
type Weather struct {
Temperature *Celsius
}
tempCelsiusOpt := optional.MakeOptional(
func(w Weather) option.Option[*Celsius] {
return option.FromNillable(w.Temperature)
},
func(w Weather, t *Celsius) Weather {
w.Temperature = t
return w
},
)
// Create optional that works with Fahrenheit
tempFahrenheitOpt := F.Pipe1(
tempCelsiusOpt,
optional.IMap[Weather, *Celsius, *Fahrenheit](
func(c *Celsius) *Fahrenheit {
f := Fahrenheit(*c*9/5 + 32)
return &f
},
func(f *Fahrenheit) *Celsius {
c := Celsius((*f - 32) * 5 / 9)
return &c
},
),
)
celsius := Celsius(20)
weather := Weather{Temperature: &celsius}
tempF := tempFahrenheitOpt.GetOption(weather) // Some(68°F)
# Real-World Example: Configuration with Defaults
type DatabaseConfig struct {
Host string
Port int
Username string
Password string
}
type AppConfig struct {
Database *DatabaseConfig
Debug bool
}
dbOpt := optional.MakeOptional(
func(c AppConfig) option.Option[*DatabaseConfig] {
return option.FromNillable(c.Database)
},
func(c AppConfig, db *DatabaseConfig) AppConfig {
c.Database = db
return c
},
)
dbHostOpt := optional.MakeOptionalRef(
func(db *DatabaseConfig) option.Option[string] {
if db == nil {
return option.None[string]()
}
return option.Some(db.Host)
},
func(db *DatabaseConfig, host string) *DatabaseConfig {
db.Host = host
return db
},
)
// Compose to access database host
appDbHostOpt := F.Pipe1(
dbOpt,
optional.Compose[AppConfig, *DatabaseConfig, string](dbHostOpt),
)
config := AppConfig{Database: nil, Debug: true}
// Get returns None when database is not configured
host := appDbHostOpt.GetOption(config) // None[string]
// Set creates database if needed
withDB := AppConfig{
Database: &DatabaseConfig{Host: "localhost", Port: 5432},
Debug: true,
}
updated := appDbHostOpt.Set("prod.example.com")(withDB)
// updated.Database.Host == "prod.example.com"
# Real-World Example: Safe Navigation
type Company struct {
Name string
CEO *Person
}
type Person struct {
Name string
Address *Address
}
type Address struct {
City string
}
ceoOpt := optional.MakeOptional(
func(c Company) option.Option[*Person] {
return option.FromNillable(c.CEO)
},
func(c Company, p *Person) Company {
c.CEO = p
return c
},
)
addressOpt := optional.MakeOptionalRef(
func(p *Person) option.Option[*Address] {
return option.FromNillable(p.Address)
},
func(p *Person, a *Address) *Person {
p.Address = a
return p
},
)
cityOpt := optional.MakeOptionalRef(
func(a *Address) option.Option[string] {
if a == nil {
return option.None[string]()
}
return option.Some(a.City)
},
func(a *Address, city string) *Address {
a.City = city
return a
},
)
// Compose all optionals for safe navigation
ceoCityOpt := F.Pipe2(
ceoOpt,
optional.Compose[Company, *Person, *Address](addressOpt),
optional.Compose[Company, *Address, string](cityOpt),
)
company := Company{Name: "Acme Corp", CEO: nil}
// Safe navigation returns None at any missing level
city := ceoCityOpt.GetOption(company) // None[string]
# Optionals in the Optics Hierarchy
Optionals sit between lenses and traversals in the optics hierarchy:
Lens[S, A]
Optional[S, A]
Traversal[S, A]
Prism[S, A]
Optional[S, A]
This means:
- Every Lens can be converted to an Optional (value always exists)
- Every Prism can be converted to an Optional (variant may not match)
- Every Optional can be converted to a Traversal (0 or 1 values)
# Performance Considerations
Optionals are efficient:
- No reflection - all operations are type-safe at compile time
- Minimal allocations - optionals themselves are lightweight
- GetOption short-circuits on None
- Set operations create new copies (immutability)
For best performance:
- Use MakeOptionalRef for pointer structures to ensure proper copying
- Cache composed optionals rather than recomposing
- Consider batch operations when updating multiple optional values
# Type Safety
Optionals are fully type-safe:
- Compile-time type checking
- No runtime type assertions
- Generic type parameters ensure correctness
- Composition maintains type relationships
# Function Reference
Core Optional Creation:
- MakeOptional: Create an optional from getter and setter functions
- MakeOptionalRef: Create an optional for pointer-based structures
- Id: Create an identity optional
- IdRef: Create an identity optional for pointers
Composition:
- Compose: Compose two optionals
- ComposeRef: Compose optionals for pointer structures
Transformation:
- ModifyOption: Transform a value through an optional (returns Option[S])
- SetOption: Set a value through an optional (returns Option[S])
- IMap: Bidirectionally map an optional
- IChain: Bidirectionally map with optional results
- IChainAny: Map to/from any type
Predicate-Based:
- FromPredicate: Create optional from predicate
- FromPredicateRef: Create optional from predicate (ref version)
# Related Packages
- github.com/IBM/fp-go/v2/optics/lens: Lenses for fields that always exist
- github.com/IBM/fp-go/v2/optics/prism: Prisms for sum types
- github.com/IBM/fp-go/v2/optics/traversal: Traversals for multiple values
- github.com/IBM/fp-go/v2/option: Optional values
- github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions)
*/
package optional

View File

@@ -42,7 +42,7 @@ func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeOptionalRef`
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
func MakeOptional[S, A any](get func(S) O.Option[A], set func(S, A) S) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: EM.Curry2(F.Swap(set))}
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set)}
}
// MakeOptionalRef creates an Optional based on a getter and a setter function. The setter passed in does not have to create a shallow

493
v2/optics/traversal/doc.go Normal file
View File

@@ -0,0 +1,493 @@
// 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 traversal provides traversals - optics for focusing on multiple values simultaneously.
# Overview
A Traversal is an optic that focuses on zero or more values within a data structure,
allowing you to view, modify, or fold over multiple elements at once. Unlike lenses
which focus on a single field, or prisms which focus on one variant, traversals can
target collections, multiple fields, or any number of values.
Traversals are the most general optic and sit at the bottom of the optics hierarchy.
They are essential for:
- Working with collections (arrays, slices, maps)
- Batch operations on multiple fields
- Filtering and transforming multiple values
- Aggregating data from multiple sources
- Applying the same operation to all matching elements
# Mathematical Foundation
A Traversal[S, A] is defined using higher-kinded types and applicative functors.
In practical terms, it provides operations to:
- Modify: Apply a function to all focused values
- Set: Replace all focused values with a constant
- FoldMap: Map each value to a monoid and combine results
- GetAll: Collect all focused values into a list
Traversals must satisfy the traversal laws:
1. Identity: traverse(Identity, id) == Identity
2. Composition: traverse(Compose(F, G), f) == Compose(traverse(F, traverse(G, f)))
These laws ensure that traversals compose properly and behave consistently.
# Basic Usage
Creating a traversal for array elements:
import (
A "github.com/IBM/fp-go/v2/array"
T "github.com/IBM/fp-go/v2/optics/traversal"
TA "github.com/IBM/fp-go/v2/optics/traversal/array"
)
numbers := []int{1, 2, 3, 4, 5}
// Get all elements
all := T.GetAll(numbers)(TA.Traversal[int]())
// Result: [1, 2, 3, 4, 5]
// Modify all elements
doubled := F.Pipe2(
numbers,
TA.Traversal[int](),
T.Modify[[]int, int](func(n int) int { return n * 2 }),
)
// Result: [2, 4, 6, 8, 10]
// Set all elements to a constant
allTens := F.Pipe2(
numbers,
TA.Traversal[int](),
T.Set[[]int, int](10),
)
// Result: [10, 10, 10, 10, 10]
# Identity Traversal
The identity traversal focuses on the entire structure:
idTrav := T.Id[int, int]()
value := 42
result := T.Modify[int, int](func(n int) int { return n * 2 })(idTrav)(value)
// Result: 84
# Folding with Traversals
Aggregate values using monoids:
import (
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
)
numbers := []int{1, 2, 3, 4, 5}
// Sum all elements
sum := F.Pipe2(
numbers,
TA.Traversal[int](),
T.FoldMap[int, []int, int](F.Identity[int]),
)(N.MonoidSum[int]())
// Result: 15
// Product of all elements
product := F.Pipe2(
numbers,
TA.Traversal[int](),
T.FoldMap[int, []int, int](F.Identity[int]),
)(N.MonoidProduct[int]())
// Result: 120
# Composing Traversals
Traversals can be composed to focus on nested collections:
type Person struct {
Name string
Friends []string
}
people := []Person{
{Name: "Alice", Friends: []string{"Bob", "Charlie"}},
{Name: "Bob", Friends: []string{"Alice", "David"}},
}
// Traversal for people array
peopleTrav := TA.Traversal[Person]()
// Traversal for friends array within a person
friendsTrav := T.MakeTraversal(func(p Person) []string {
return p.Friends
})
// Compose to access all friends of all people
allFriendsTrav := F.Pipe1(
peopleTrav,
T.Compose[[]Person, Person, string, ...](friendsTrav),
)
// Get all friends
allFriends := T.GetAll(people)(allFriendsTrav)
// Result: ["Bob", "Charlie", "Alice", "David"]
# Working with Records (Maps)
Traverse over map values:
import TR "github.com/IBM/fp-go/v2/optics/traversal/record"
scores := map[string]int{
"Alice": 85,
"Bob": 92,
"Charlie": 78,
}
// Get all scores
allScores := F.Pipe2(
scores,
TR.Traversal[string, int](),
T.GetAll[map[string]int, int],
)
// Result: [85, 92, 78] (order may vary)
// Increase all scores by 5
boosted := F.Pipe2(
scores,
TR.Traversal[string, int](),
T.Modify[map[string]int, int](func(score int) int {
return score + 5
}),
)
// Result: {"Alice": 90, "Bob": 97, "Charlie": 83}
# Working with Either Types
Traverse over the Right values:
import (
E "github.com/IBM/fp-go/v2/either"
TE "github.com/IBM/fp-go/v2/optics/traversal/either"
)
results := []E.Either[string, int]{
E.Right[string](10),
E.Left[int]("error"),
E.Right[string](20),
}
// Traversal for array of Either
arrayTrav := TA.Traversal[E.Either[string, int]]()
// Traversal for Right values
rightTrav := TE.Traversal[string, int]()
// Compose to access all Right values
allRightsTrav := F.Pipe1(
arrayTrav,
T.Compose[[]E.Either[string, int], E.Either[string, int], int, ...](rightTrav),
)
// Get all Right values
rights := T.GetAll(results)(allRightsTrav)
// Result: [10, 20]
// Double all Right values
doubled := F.Pipe2(
results,
allRightsTrav,
T.Modify[[]E.Either[string, int], int](func(n int) int { return n * 2 }),
)
// Result: [Right(20), Left("error"), Right(40)]
# Working with Option Types
Traverse over Some values:
import (
O "github.com/IBM/fp-go/v2/option"
TO "github.com/IBM/fp-go/v2/optics/traversal/option"
)
values := []O.Option[int]{
O.Some(1),
O.None[int](),
O.Some(2),
O.None[int](),
O.Some(3),
}
// Compose array and option traversals
allSomesTrav := F.Pipe1(
TA.Traversal[O.Option[int]](),
T.Compose[[]O.Option[int], O.Option[int], int, ...](TO.Traversal[int]()),
)
// Get all Some values
somes := T.GetAll(values)(allSomesTrav)
// Result: [1, 2, 3]
// Increment all Some values
incremented := F.Pipe2(
values,
allSomesTrav,
T.Modify[[]O.Option[int], int](func(n int) int { return n + 1 }),
)
// Result: [Some(2), None, Some(3), None, Some(4)]
# Real-World Example: Nested Data Structures
type Department struct {
Name string
Employees []Employee
}
type Employee struct {
Name string
Salary int
}
company := []Department{
{
Name: "Engineering",
Employees: []Employee{
{Name: "Alice", Salary: 100000},
{Name: "Bob", Salary: 95000},
},
},
{
Name: "Sales",
Employees: []Employee{
{Name: "Charlie", Salary: 80000},
{Name: "David", Salary: 85000},
},
},
}
// Traversal for departments
deptTrav := TA.Traversal[Department]()
// Traversal for employees within a department
empTrav := T.MakeTraversal(func(d Department) []Employee {
return d.Employees
})
// Traversal for employee array
empArrayTrav := TA.Traversal[Employee]()
// Compose to access all employees
allEmpTrav := F.Pipe2(
deptTrav,
T.Compose[[]Department, Department, []Employee, ...](empTrav),
T.Compose[[]Department, []Employee, Employee, ...](empArrayTrav),
)
// Get all employee names
names := F.Pipe2(
company,
allEmpTrav,
T.FoldMap[[]string, []Department, Employee](func(e Employee) []string {
return []string{e.Name}
}),
)(A.Monoid[string]())
// Result: ["Alice", "Bob", "Charlie", "David"]
// Give everyone a 10% raise
withRaises := F.Pipe2(
company,
allEmpTrav,
T.Modify[[]Department, Employee](func(e Employee) Employee {
e.Salary = int(float64(e.Salary) * 1.1)
return e
}),
)
# Real-World Example: Filtering with Traversals
type Product struct {
Name string
Price float64
InStock bool
}
products := []Product{
{Name: "Laptop", Price: 999.99, InStock: true},
{Name: "Mouse", Price: 29.99, InStock: false},
{Name: "Keyboard", Price: 79.99, InStock: true},
}
// Create a traversal that only focuses on in-stock products
inStockTrav := T.MakeTraversal(func(ps []Product) []Product {
return A.Filter(func(p Product) bool {
return p.InStock
})(ps)
})
// Apply discount to in-stock items
discounted := F.Pipe2(
products,
inStockTrav,
T.Modify[[]Product, Product](func(p Product) Product {
p.Price = p.Price * 0.9
return p
}),
)
// Only Laptop and Keyboard prices are reduced
# Real-World Example: Data Aggregation
type Order struct {
ID string
Items []OrderItem
Status string
}
type OrderItem struct {
Product string
Quantity int
Price float64
}
orders := []Order{
{
ID: "001",
Items: []OrderItem{
{Product: "Widget", Quantity: 2, Price: 10.0},
{Product: "Gadget", Quantity: 1, Price: 25.0},
},
Status: "completed",
},
{
ID: "002",
Items: []OrderItem{
{Product: "Widget", Quantity: 5, Price: 10.0},
},
Status: "completed",
},
}
// Traversal for orders
orderTrav := TA.Traversal[Order]()
// Traversal for items within an order
itemsTrav := T.MakeTraversal(func(o Order) []OrderItem {
return o.Items
})
// Traversal for item array
itemArrayTrav := TA.Traversal[OrderItem]()
// Compose to access all items
allItemsTrav := F.Pipe2(
orderTrav,
T.Compose[[]Order, Order, []OrderItem, ...](itemsTrav),
T.Compose[[]Order, []OrderItem, OrderItem, ...](itemArrayTrav),
)
// Calculate total revenue
totalRevenue := F.Pipe2(
orders,
allItemsTrav,
T.FoldMap[float64, []Order, OrderItem](func(item OrderItem) float64 {
return float64(item.Quantity) * item.Price
}),
)(N.MonoidSum[float64]())
// Result: 95.0 (2*10 + 1*25 + 5*10)
# Traversals in the Optics Hierarchy
Traversals are the most general optic:
Iso[S, A]
Lens[S, A]
Optional[S, A]
Traversal[S, A]
Prism[S, A]
Optional[S, A]
Traversal[S, A]
This means:
- Every Iso, Lens, Prism, and Optional can be converted to a Traversal
- Traversals are the most flexible but least specific optic
- Use more specific optics when possible for better type safety
# Performance Considerations
Traversals can be efficient but consider:
- Each traversal operation may iterate over all elements
- Composition creates nested iterations
- FoldMap is often more efficient than GetAll followed by reduction
- Modify creates new copies (immutability)
For best performance:
- Use specialized traversals (array, record, etc.) when available
- Avoid unnecessary composition
- Consider batch operations
- Cache composed traversals
# Type Safety
Traversals are fully type-safe:
- Compile-time type checking
- Generic type parameters ensure correctness
- Composition maintains type relationships
- No runtime type assertions
# Function Reference
Core Functions:
- Id: Create an identity traversal
- Modify: Apply a function to all focused values
- Set: Replace all focused values with a constant
- Compose: Compose two traversals
Aggregation:
- FoldMap: Map each value to a monoid and combine
- Fold: Fold over all values using a monoid
- GetAll: Collect all focused values into a list
# Specialized Traversals
The package includes specialized sub-packages for common patterns:
- array: Traversals for arrays and slices
- record: Traversals for maps
- either: Traversals for Either types
- option: Traversals for Option types
Each specialized package provides optimized implementations for its data type.
# Related Packages
- github.com/IBM/fp-go/v2/optics/lens: Lenses for single fields
- github.com/IBM/fp-go/v2/optics/prism: Prisms for sum types
- github.com/IBM/fp-go/v2/optics/optional: Optionals for maybe values
- github.com/IBM/fp-go/v2/optics/traversal/array: Array traversals
- github.com/IBM/fp-go/v2/optics/traversal/record: Record/map traversals
- github.com/IBM/fp-go/v2/optics/traversal/either: Either traversals
- github.com/IBM/fp-go/v2/optics/traversal/option: Option traversals
- github.com/IBM/fp-go/v2/array: Array utilities
- github.com/IBM/fp-go/v2/monoid: Monoid type class
*/
package traversal

View File

@@ -21,7 +21,7 @@ import (
)
type (
Traversal[E, S, A any] T.Traversal[S, A, ET.Either[E, S], ET.Either[E, A]]
Traversal[E, S, A any] = T.Traversal[S, A, ET.Either[E, S], ET.Either[E, A]]
)
func Compose[

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