mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-07 23:03:15 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02d0be9dad | ||
|
|
2c1d8196b4 | ||
|
|
17eb8ae66f | ||
|
|
b70e481e7d | ||
|
|
3c3bb7c166 | ||
|
|
d3007cbbfa | ||
|
|
5aa0e1ea2e | ||
|
|
d586428cb0 | ||
|
|
d2dbce6e8b | ||
|
|
6f7ec0768d | ||
|
|
ca813b673c |
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
9
v2/array/types.go
Normal 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]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
4
v2/bytes/coverage.out
Normal 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
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
417
v2/cli/lens.go
417
v2/cli/lens.go
@@ -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 {
|
||||
|
||||
@@ -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
11
v2/constant/monoid.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
10
v2/endomorphism/from.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
158
v2/eq/eq.go
158
v2/eq/eq.go
@@ -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)
|
||||
|
||||
120
v2/eq/monoid.go
120
v2/eq/monoid.go
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
13
v2/http/builder/type.go
Normal 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]
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
61
v2/internal/iter/iter.go
Normal 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) {}
|
||||
}
|
||||
152
v2/internal/iter/traverse.go
Normal file
152
v2/internal/iter/traverse.go
Normal 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)
|
||||
}
|
||||
}
|
||||
9
v2/internal/iter/types.go
Normal file
9
v2/internal/iter/types.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
I "iter"
|
||||
)
|
||||
|
||||
type (
|
||||
Seq[A any] = I.Seq[A]
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
7
v2/io/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package io
|
||||
|
||||
import "iter"
|
||||
|
||||
type (
|
||||
Seq[T any] = iter.Seq[T]
|
||||
)
|
||||
@@ -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]]]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
887
v2/iterator/iter/iter.go
Normal 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)
|
||||
}
|
||||
588
v2/iterator/iter/iter_test.go
Normal file
588
v2/iterator/iter/iter_test.go
Normal 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
37
v2/iterator/iter/monid.go
Normal 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
57
v2/iterator/iter/types.go
Normal 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]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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]],
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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]())
|
||||
}
|
||||
|
||||
@@ -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]{}
|
||||
}
|
||||
|
||||
@@ -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](),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
15
v2/iterator/stateless/generic/types.go
Normal file
15
v2/iterator/stateless/generic/types.go
Normal 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]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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, "")),
|
||||
)
|
||||
|
||||
29
v2/iterator/stateless/seq.go
Normal file
29
v2/iterator/stateless/seq.go
Normal 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)())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
231
v2/optics/iso/lens/doc.go
Normal 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
|
||||
@@ -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])))
|
||||
}
|
||||
|
||||
399
v2/optics/iso/lens/lens_test.go
Normal file
399
v2/optics/iso/lens/lens_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
11
v2/optics/iso/lens/types.go
Normal file
11
v2/optics/iso/lens/types.go
Normal 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
303
v2/optics/iso/option/doc.go
Normal 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
364
v2/optics/lens/iso/doc.go
Normal 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
|
||||
@@ -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],
|
||||
|
||||
14
v2/optics/lens/iso/types.go
Normal file
14
v2/optics/lens/iso/types.go
Normal 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]
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
841
v2/optics/lens/option/compose_test.go
Normal file
841
v2/optics/lens/option/compose_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
479
v2/optics/lens/option/from_test.go
Normal file
479
v2/optics/lens/option/from_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
477
v2/optics/optional/doc.go
Normal 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
|
||||
@@ -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
493
v2/optics/traversal/doc.go
Normal 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
|
||||
@@ -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
Reference in New Issue
Block a user