1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-04-09 15:26:02 +02:00

Compare commits

..

13 Commits

Author SHA1 Message Date
Dr. Carsten Leue
21b517d388 fix: better doc and NonEmptyString
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-09 14:51:39 +02:00
Dr. Carsten Leue
0df62c0031 fix: unroll array Fold and FoldMap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-09 11:11:00 +02:00
Dr. Carsten Leue
57318e2d1d fix: make use of Empty lazy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-08 15:00:39 +02:00
Dr. Carsten Leue
2b937d3e93 doc: add marble diagrams
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-30 10:15:27 +02:00
Dr. Carsten Leue
747a1794e5 fix: add more iter operators
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-30 10:04:20 +02:00
renovate[bot]
c754cacf1f fix(deps): update module github.com/urfave/cli/v3 to v3.8.0 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 20:40:07 +00:00
Dr. Carsten Leue
d357b32847 fix: add TapThunkK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-23 18:43:49 +01:00
Dr. Carsten Leue
a3af003e74 fix: undo Pipe and Flow changes, did not have the desired effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-20 23:20:07 +01:00
Dr. Carsten Leue
c81235827b fix: try to change the way Pipe and Flow are structured
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-20 12:05:25 +01:00
Dr. Carsten Leue
f35430cf18 fix: introduce async iterators
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-18 10:19:03 +01:00
Dr. Carsten Leue
d3ffc71808 fix: add ModifyF
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-17 15:23:10 +01:00
Dr. Carsten Leue
62844b7030 fix: add Filter and FilterMap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 23:33:08 +01:00
Dr. Carsten Leue
99a0ddd4b6 fix: implement filter and filtermap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-15 23:18:14 +01:00
43 changed files with 6953 additions and 97 deletions

View File

@@ -22,8 +22,10 @@ This document provides guidelines for AI agents working on the fp-go/v2 project.
1. **Use Standard Go Doc Format**
- Do NOT use markdown-style links like `[text](url)`
- Do NOT use markdown-style headers like `# Section` or `## Subsection`
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
- Go's documentation system will automatically create links
- Use plain text with blank lines to separate sections
2. **Structure**
```go
@@ -31,24 +33,20 @@ This document provides guidelines for AI agents working on the fp-go/v2 project.
//
// Longer description explaining the purpose and behavior.
//
// # Type Parameters
//
// Type Parameters:
// - T: Description of type parameter
//
// # Parameters
//
// Parameters:
// - param: Description of parameter
//
// # Returns
//
// Returns:
// - ReturnType: Description of return value
//
// # Example Usage
// Example:
//
// code example here
//
// # See Also
//
// See Also:
// - RelatedFunction: Brief description
func FunctionName[T any](param T) ReturnType {
```
@@ -57,6 +55,7 @@ This document provides guidelines for AI agents working on the fp-go/v2 project.
- Use idiomatic Go patterns
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
- Show realistic, runnable examples
- Indent code examples with spaces (not tabs) for proper godoc rendering
### File Headers

View File

@@ -24,21 +24,75 @@ import (
"github.com/IBM/fp-go/v2/pair"
)
// From constructs an array from a set of variadic arguments
// From constructs an array from a set of variadic arguments.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - data: Variadic arguments to include in the array
//
// # Returns
//
// - A new array containing all provided arguments
//
// # Example
//
// arr := array.From(1, 2, 3, 4, 5)
// // arr: []int{1, 2, 3, 4, 5}
//
//go:inline
func From[A any](data ...A) []A {
return G.From[[]A](data...)
}
// MakeBy returns a `Array` of length `n` with element `i` initialized with `f(i)`.
// MakeBy returns an array of length n with element i initialized with f(i).
//
// # Type Parameters
//
// - F: Function type that takes an int and returns A
// - A: The type of elements in the array
//
// # Parameters
//
// - n: The length of the array to create
// - f: Function to generate each element based on its index
//
// # Returns
//
// - A new array where element at index i equals f(i)
//
// # Example
//
// squares := array.MakeBy(5, func(i int) int { return i * i })
// // squares: []int{0, 1, 4, 9, 16}
//
//go:inline
func MakeBy[F ~func(int) A, A any](n int, f F) []A {
return G.MakeBy[[]A](n, f)
}
// Replicate creates a `Array` containing a value repeated the specified number of times.
// Replicate creates an array containing a value repeated the specified number of times.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - n: The number of times to repeat the value
// - a: The value to repeat
//
// # Returns
//
// - A new array containing n copies of a
//
// # Example
//
// zeros := array.Replicate(5, 0)
// // zeros: []int{0, 0, 0, 0, 0}
//
//go:inline
func Replicate[A any](n int, a A) []A {
@@ -55,6 +109,27 @@ func MonadMap[A, B any](as []A, f func(A) B) []B {
// 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.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// # Parameters
//
// - as: The input array
// - f: Function that takes a pointer to an element and returns a transformed value
//
// # Returns
//
// - A new array with transformed elements
//
// # Example
//
// type Point struct { X, Y int }
// points := []Point{{1, 2}, {3, 4}}
// xs := array.MonadMapRef(points, func(p *Point) int { return p.X })
// // xs: []int{1, 3}
func MonadMapRef[A, B any](as []A, f func(*A) B) []B {
count := len(as)
bs := make([]B, count)
@@ -86,6 +161,27 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
// 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.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// # Parameters
//
// - f: Function that takes a pointer to an element and returns a transformed value
//
// # Returns
//
// - A function that transforms an array of A into an array of B
//
// # Example
//
// type Point struct { X, Y int }
// extractX := array.MapRef(func(p *Point) int { return p.X })
// points := []Point{{1, 2}, {3, 4}}
// xs := extractX(points)
// // xs: []int{1, 3}
func MapRef[A, B any](f func(*A) B) Operator[A, B] {
return F.Bind2nd(MonadMapRef[A, B], f)
}
@@ -114,14 +210,51 @@ func filterMapRef[A, B any](fa []A, pred func(*A) bool, f func(*A) B) []B {
return result
}
// Filter returns a new array with all elements from the original array that match a predicate
// Filter returns a new array with all elements from the original array that match a predicate.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - pred: Predicate function to test each element
//
// # Returns
//
// - A function that filters an array based on the predicate
//
// # Example
//
// isEven := array.Filter(func(x int) bool { return x%2 == 0 })
// result := isEven([]int{1, 2, 3, 4, 5, 6})
// // result: []int{2, 4, 6}
//
//go:inline
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
// FilterWithIndex returns a new array with all elements from the original array that match a predicate.
// The predicate receives both the index and the element.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - pred: Predicate function that takes an index and element
//
// # Returns
//
// - A function that filters an array based on the predicate
//
// # Example
//
// filterOddIndices := array.FilterWithIndex(func(i int, _ int) bool { return i%2 == 1 })
// result := filterOddIndices([]int{10, 20, 30, 40, 50})
// // result: []int{20, 40}
//
//go:inline
func FilterWithIndex[A any](pred func(int, A) bool) Operator[A, A] {
@@ -129,6 +262,26 @@ func FilterWithIndex[A any](pred func(int, A) bool) Operator[A, A] {
}
// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - pred: Predicate function that takes a pointer to an element
//
// # Returns
//
// - A function that filters an array based on the predicate
//
// # Example
//
// type Point struct { X, Y int }
// filterPositiveX := array.FilterRef(func(p *Point) bool { return p.X > 0 })
// points := []Point{{-1, 2}, {3, 4}, {-5, 6}}
// result := filterPositiveX(points)
// // result: []Point{{3, 4}}
func FilterRef[A any](pred func(*A) bool) Operator[A, A] {
return F.Bind2nd(filterRef[A], pred)
}
@@ -149,21 +302,73 @@ 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 [Option] and it keeps only the Some values discarding the Nones.
// FilterMap maps an array with an iterating function that returns an Option and keeps only the Some values discarding the Nones.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// # Parameters
//
// - f: Function that maps elements to Option values
//
// # Returns
//
// - A function that transforms and filters an array
//
// # Example
//
// parseInt := array.FilterMap(func(s string) option.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return option.Some(n)
// }
// return option.None[int]()
// })
// result := parseInt([]string{"1", "bad", "3", "4"})
// // result: []int{1, 3, 4}
//
//go:inline
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 [Option] and it keeps only the Some values discarding the Nones.
// FilterMapWithIndex maps an array with an iterating function that returns an Option and keeps only the Some values discarding the Nones.
// The function receives both the index and the element.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// # Parameters
//
// - f: Function that takes an index and element and returns an Option
//
// # Returns
//
// - A function that transforms and filters an array
//
//go:inline
func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return G.FilterMapWithIndex[[]A, []B](f)
}
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// ChainOptionK maps an array with an iterating function that returns an Option of an array.
// It keeps only the Some values discarding the Nones and then flattens the result.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// # Parameters
//
// - f: Function that maps elements to Option of arrays
//
// # Returns
//
// - A function that transforms, filters, and flattens an array
//
//go:inline
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
@@ -171,6 +376,20 @@ func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// # Parameters
//
// - pred: Predicate function that takes a pointer to an element
// - f: Function that transforms a pointer to an element
//
// # Returns
//
// - A function that filters and transforms an array
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)
@@ -185,11 +404,48 @@ func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
return current
}
// MonadReduce folds an array from left to right, applying a function to accumulate a result.
// This is the monadic version that takes the array as the first parameter.
//
// # Type Parameters
//
// - A: The type of elements in the array
// - B: The type of the accumulated result
//
// # Parameters
//
// - fa: The input array
// - f: Function that combines the accumulator with each element
// - initial: The initial accumulator value
//
// # Returns
//
// - The final accumulated result
//
//go:inline
func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
return G.MonadReduce(fa, f, initial)
}
// MonadReduceWithIndex folds an array from left to right with access to the index,
// applying a function to accumulate a result.
// This is the monadic version that takes the array as the first parameter.
//
// # Type Parameters
//
// - A: The type of elements in the array
// - B: The type of the accumulated result
//
// # Parameters
//
// - fa: The input array
// - f: Function that combines the index, accumulator, and element
// - initial: The initial accumulator value
//
// # Returns
//
// - The final accumulated result
//
//go:inline
func MonadReduceWithIndex[A, B any](fa []A, f func(int, B, A) B, initial B) B {
return G.MonadReduceWithIndex(fa, f, initial)
@@ -232,6 +488,20 @@ func ReduceRightWithIndex[A, B any](f func(int, A, B) B, initial B) func([]A) B
// ReduceRef folds an array from left to right using pointers to elements,
// applying a function to accumulate a result.
//
// # Type Parameters
//
// - A: The type of elements in the array
// - B: The type of the accumulated result
//
// # Parameters
//
// - f: Function that combines the accumulator with a pointer to each element
// - initial: The initial accumulator value
//
// # Returns
//
// - A function that reduces an array to a single value
func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B {
return func(as []A) B {
return reduceRef(as, f, initial)
@@ -257,12 +527,36 @@ func Append[A any](as []A, a A) []A {
// IsEmpty checks if an array has no elements.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - as: The array to check
//
// # Returns
//
// - true if the array is empty, false otherwise
//
//go:inline
func IsEmpty[A any](as []A) bool {
return G.IsEmpty(as)
}
// IsNonEmpty checks if an array has at least one element.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - as: The array to check
//
// # Returns
//
// - true if the array has at least one element, false otherwise
func IsNonEmpty[A any](as []A) bool {
return len(as) > 0
}
@@ -372,6 +666,23 @@ func Last[A any](as []A) Option[A] {
}
// PrependAll inserts a separator before each element of an array.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - middle: The separator to insert before each element
//
// # Returns
//
// - A function that transforms an array by prepending the separator to each element
//
// # Example
//
// result := array.PrependAll(0)([]int{1, 2, 3})
// // result: []int{0, 1, 0, 2, 0, 3}
func PrependAll[A any](middle A) Operator[A, A] {
return func(as []A) []A {
count := len(as)
@@ -389,9 +700,22 @@ func PrependAll[A any](middle A) Operator[A, A] {
// Intersperse inserts a separator between each element of an array.
//
// Example:
// # Type Parameters
//
// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3]
// - A: The type of elements in the array
//
// # Parameters
//
// - middle: The separator to insert between elements
//
// # Returns
//
// - A function that transforms an array by inserting the separator between elements
//
// # Example
//
// result := array.Intersperse(0)([]int{1, 2, 3})
// // result: []int{1, 0, 2, 0, 3}
func Intersperse[A any](middle A) Operator[A, A] {
prepend := PrependAll(middle)
return func(as []A) []A {
@@ -403,6 +727,18 @@ func Intersperse[A any](middle A) Operator[A, A] {
}
// Intercalate inserts a separator between elements and concatenates them using a Monoid.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - m: The Monoid to use for concatenation
//
// # Returns
//
// - A curried function that takes a separator and returns a function that reduces an array
func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A {
return func(middle A) func([]A) A {
return Match(m.Empty, F.Flow2(Intersperse(middle), ConcatAll(m)))
@@ -411,9 +747,22 @@ func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A {
// Flatten converts a nested array into a flat array by concatenating all inner arrays.
//
// Example:
// # Type Parameters
//
// result := array.Flatten([][]int{{1, 2}, {3, 4}, {5}}) // [1, 2, 3, 4, 5]
// - A: The type of elements in the inner arrays
//
// # Parameters
//
// - mma: A nested array (array of arrays)
//
// # Returns
//
// - A flat array containing all elements from all inner arrays
//
// # Example
//
// result := array.Flatten([][]int{{1, 2}, {3, 4}, {5}})
// // result: []int{1, 2, 3, 4, 5}
//
//go:inline
func Flatten[A any](mma [][]A) []A {
@@ -421,6 +770,25 @@ func Flatten[A any](mma [][]A) []A {
}
// Slice extracts a subarray from index low (inclusive) to high (exclusive).
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - low: The starting index (inclusive)
// - high: The ending index (exclusive)
//
// # Returns
//
// - A function that extracts a subarray
//
// # Example
//
// middle := array.Slice[int](2, 5)
// result := middle([]int{0, 1, 2, 3, 4, 5, 6})
// // result: []int{2, 3, 4}
func Slice[A any](low, high int) Operator[A, A] {
return array.Slice[[]A](low, high)
}
@@ -428,6 +796,24 @@ func Slice[A any](low, high int) Operator[A, A] {
// Lookup returns the element at the specified index, wrapped in an Option.
// Returns None if the index is out of bounds.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - idx: The index to look up
//
// # Returns
//
// - A function that retrieves an element at the given index, wrapped in an Option
//
// # Example
//
// getSecond := array.Lookup[int](1)
// result := getSecond([]int{10, 20, 30})
// // result: option.Some(20)
//
//go:inline
func Lookup[A any](idx int) func([]A) Option[A] {
return G.Lookup[[]A](idx)
@@ -436,6 +822,18 @@ func Lookup[A any](idx int) func([]A) Option[A] {
// UpsertAt returns a function that inserts or updates an element at a specific index.
// If the index is out of bounds, the element is appended.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - a: The element to insert or update
//
// # Returns
//
// - A function that takes an index and returns a function that upserts at that index
//
//go:inline
func UpsertAt[A any](a A) Operator[A, A] {
return G.UpsertAt[[]A](a)
@@ -443,6 +841,18 @@ func UpsertAt[A any](a A) Operator[A, A] {
// Size returns the number of elements in an array.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - as: The array to measure
//
// # Returns
//
// - The number of elements in the array
//
//go:inline
func Size[A any](as []A) int {
return G.Size(as)
@@ -457,58 +867,178 @@ func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
return G.MonadPartition(as, pred)
}
// Partition creates two new arrays out of one, the left result contains the elements
// for which the predicate returns false, the right one those for which the predicate returns true
// Partition creates two new arrays out of one. The left result contains the elements
// for which the predicate returns false, the right one those for which the predicate returns true.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - pred: Predicate function to test each element
//
// # Returns
//
// - A function that partitions an array into a pair of arrays
//
// # Example
//
// isEven := array.Partition(func(x int) bool { return x%2 == 0 })
// result := isEven([]int{1, 2, 3, 4, 5, 6})
// // result: pair.Pair{Left: []int{1, 3, 5}, Right: []int{2, 4, 6}}
//
//go:inline
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
return G.Partition[[]A](pred)
}
// IsNil checks if the array is set to nil
// IsNil checks if the array is set to nil.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - as: The array to check
//
// # Returns
//
// - true if the array is nil, false otherwise
func IsNil[A any](as []A) bool {
return array.IsNil(as)
}
// IsNonNil checks if the array is set to nil
// IsNonNil checks if the array is not nil.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - as: The array to check
//
// # Returns
//
// - true if the array is not nil, false otherwise
func IsNonNil[A any](as []A) bool {
return array.IsNonNil(as)
}
// ConstNil returns a nil array
// ConstNil returns a nil array.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Returns
//
// - A nil array of type A
func ConstNil[A any]() []A {
return array.ConstNil[[]A]()
}
// SliceRight extracts a subarray from the specified start index to the end.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - start: The starting index (inclusive)
//
// # Returns
//
// - A function that extracts a subarray from start to end
//
// # Example
//
// fromThird := array.SliceRight[int](2)
// result := fromThird([]int{0, 1, 2, 3, 4, 5})
// // result: []int{2, 3, 4, 5}
//
//go:inline
func SliceRight[A any](start int) Operator[A, A] {
return G.SliceRight[[]A](start)
}
// Copy creates a shallow copy of the array
// Copy creates a shallow copy of the array.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - b: The array to copy
//
// # Returns
//
// - A new array with the same elements
//
//go:inline
func Copy[A any](b []A) []A {
return G.Copy(b)
}
// Clone creates a deep copy of the array using the provided endomorphism to clone the values
// Clone creates a deep copy of the array using the provided endomorphism to clone the values.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - f: Function to clone each element
//
// # Returns
//
// - A function that creates a deep copy of an array
//
//go:inline
func Clone[A any](f func(A) A) Operator[A, A] {
return G.Clone[[]A](f)
}
// FoldMap maps and folds an array. Map the Array passing each value to the iterating function. Then fold the results using the provided Monoid.
// FoldMap maps and folds an array. Maps each value using the iterating function,
// then folds the results using the provided Monoid.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements after mapping
//
// # Parameters
//
// - m: The Monoid to use for folding
//
// # Returns
//
// - A curried function that takes a mapping function and returns a function that folds an array
//
//go:inline
func FoldMap[A, B any](m M.Monoid[B]) func(func(A) B) func([]A) B {
return G.FoldMap[[]A](m)
}
// FoldMapWithIndex maps and folds an array. Map the Array passing each value to the iterating function. Then fold the results using the provided Monoid.
// FoldMapWithIndex maps and folds an array with access to indices. Maps each value using the iterating function,
// then folds the results using the provided Monoid.
//
// # Type Parameters
//
// - A: The type of elements in the input array
// - B: The type of elements after mapping
//
// # Parameters
//
// - m: The Monoid to use for folding
//
// # Returns
//
// - A curried function that takes a mapping function and returns a function that folds an array
//
//go:inline
func FoldMapWithIndex[A, B any](m M.Monoid[B]) func(func(int, A) B) func([]A) B {
@@ -517,12 +1047,46 @@ func FoldMapWithIndex[A, B any](m M.Monoid[B]) func(func(int, A) B) func([]A) B
// Fold folds the array using the provided Monoid.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - m: The Monoid to use for folding
//
// # Returns
//
// - A function that folds an array to a single value
//
//go:inline
func Fold[A any](m M.Monoid[A]) func([]A) A {
return G.Fold[[]A](m)
}
// Push adds an element to the end of an array (alias for Append).
// Push adds an element to the end of an array (curried version of Append).
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - a: The element to add
//
// # Returns
//
// - A function that appends the element to an array
//
// # Example
//
// addFive := array.Push(5)
// result := addFive([]int{1, 2, 3})
// // result: []int{1, 2, 3, 5}
//
// # See Also
//
// - Append: Non-curried version
//
//go:inline
func Push[A any](a A) Operator[A, A] {
@@ -660,6 +1224,20 @@ func Concat[A any](suffix []A) Operator[A, A] {
// MonadFlap applies a value to an array of functions, producing an array of results.
// This is the monadic version that takes both parameters.
//
// # Type Parameters
//
// - B: The type of results
// - A: The type of the input value
//
// # Parameters
//
// - fab: Array of functions to apply
// - a: The value to apply to each function
//
// # Returns
//
// - An array of results from applying the value to each function
//
//go:inline
func MonadFlap[B, A any](fab []func(A) B, a A) []B {
return G.MonadFlap[func(A) B, []func(A) B, []B](fab, a)
@@ -668,6 +1246,30 @@ func MonadFlap[B, A any](fab []func(A) B, a A) []B {
// Flap applies a value to an array of functions, producing an array of results.
// This is the curried version.
//
// # Type Parameters
//
// - B: The type of results
// - A: The type of the input value
//
// # Parameters
//
// - a: The value to apply to each function
//
// # Returns
//
// - A function that applies the value to an array of functions
//
// # Example
//
// fns := []func(int) int{
// func(x int) int { return x * 2 },
// func(x int) int { return x + 10 },
// func(x int) int { return x * x },
// }
// applyFive := array.Flap[int](5)
// result := applyFive(fns)
// // result: []int{10, 15, 25}
//
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return G.Flap[func(A) B, []func(A) B, []B](a)
@@ -675,6 +1277,24 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
// Prepend adds an element to the beginning of an array, returning a new array.
//
// # Type Parameters
//
// - A: The type of elements in the array
//
// # Parameters
//
// - head: The element to add at the beginning
//
// # Returns
//
// - A function that prepends the element to an array
//
// # Example
//
// addZero := array.Prepend(0)
// result := addZero([]int{1, 2, 3})
// // result: []int{0, 1, 2, 3}
//
//go:inline
func Prepend[A any](head A) Operator[A, A] {
return G.Prepend[Operator[A, A]](head)

View File

@@ -198,11 +198,228 @@ func TestFilterMap(t *testing.T) {
}
func TestFoldMap(t *testing.T) {
src := From("a", "b", "c")
t.Run("FoldMap with 0 items", func(t *testing.T) {
empty := []int{}
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(empty)
assert.Equal(t, 0, result, "FoldMap should return monoid empty for 0 items")
})
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
t.Run("FoldMap with 1 item", func(t *testing.T) {
single := From(5)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(single)
assert.Equal(t, 10, result, "FoldMap should map and return single item")
})
assert.Equal(t, "ABC", fold(src))
t.Run("FoldMap with 2 items", func(t *testing.T) {
two := From(3, 4)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(two)
assert.Equal(t, 14, result, "FoldMap should map and fold 2 items: (3*2) + (4*2) = 14")
})
t.Run("FoldMap with many items", func(t *testing.T) {
many := From(1, 2, 3, 4, 5)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
result := foldMap(many)
assert.Equal(t, 30, result, "FoldMap should map and fold many items: (1*2) + (2*2) + (3*2) + (4*2) + (5*2) = 30")
})
t.Run("FoldMap with string concatenation - 0 items", func(t *testing.T) {
empty := []string{}
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(empty)
assert.Equal(t, "", result, "FoldMap should return empty string for 0 items")
})
t.Run("FoldMap with string concatenation - 1 item", func(t *testing.T) {
single := From("a")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(single)
assert.Equal(t, "A", result, "FoldMap should map single string")
})
t.Run("FoldMap with string concatenation - 2 items", func(t *testing.T) {
two := From("a", "b")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(two)
assert.Equal(t, "AB", result, "FoldMap should map and concatenate 2 strings")
})
t.Run("FoldMap with string concatenation - many items", func(t *testing.T) {
many := From("a", "b", "c", "d", "e")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
result := fold(many)
assert.Equal(t, "ABCDE", result, "FoldMap should map and concatenate many strings")
})
}
func TestFold(t *testing.T) {
t.Run("Fold with 0 items", func(t *testing.T) {
empty := []int{}
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(empty)
assert.Equal(t, 0, result, "Fold should return monoid empty for 0 items")
})
t.Run("Fold with 1 item", func(t *testing.T) {
single := From(42)
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(single)
assert.Equal(t, 42, result, "Fold should return single item")
})
t.Run("Fold with 2 items", func(t *testing.T) {
two := From(10, 20)
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(two)
assert.Equal(t, 30, result, "Fold should combine 2 items: 10 + 20 = 30")
})
t.Run("Fold with many items", func(t *testing.T) {
many := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
sumMonoid := N.MonoidSum[int]()
fold := Fold[int](sumMonoid)
result := fold(many)
assert.Equal(t, 55, result, "Fold should combine many items: 1+2+3+4+5+6+7+8+9+10 = 55")
})
t.Run("Fold with string concatenation - 0 items", func(t *testing.T) {
empty := []string{}
fold := Fold[string](S.Monoid)
result := fold(empty)
assert.Equal(t, "", result, "Fold should return empty string for 0 items")
})
t.Run("Fold with string concatenation - 1 item", func(t *testing.T) {
single := From("hello")
fold := Fold[string](S.Monoid)
result := fold(single)
assert.Equal(t, "hello", result, "Fold should return single string")
})
t.Run("Fold with string concatenation - 2 items", func(t *testing.T) {
two := From("hello", "world")
fold := Fold[string](S.Monoid)
result := fold(two)
assert.Equal(t, "helloworld", result, "Fold should concatenate 2 strings")
})
t.Run("Fold with string concatenation - many items", func(t *testing.T) {
many := From("a", "b", "c", "d", "e", "f")
fold := Fold[string](S.Monoid)
result := fold(many)
assert.Equal(t, "abcdef", result, "Fold should concatenate many strings")
})
t.Run("Fold with product monoid - 0 items", func(t *testing.T) {
empty := []int{}
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(empty)
assert.Equal(t, 1, result, "Fold should return monoid empty (1) for product with 0 items")
})
t.Run("Fold with product monoid - 1 item", func(t *testing.T) {
single := From(7)
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(single)
assert.Equal(t, 7, result, "Fold should return single item for product")
})
t.Run("Fold with product monoid - 2 items", func(t *testing.T) {
two := From(3, 4)
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(two)
assert.Equal(t, 12, result, "Fold should multiply 2 items: 3 * 4 = 12")
})
t.Run("Fold with product monoid - many items", func(t *testing.T) {
many := From(2, 3, 4, 5)
productMonoid := N.MonoidProduct[int]()
fold := Fold[int](productMonoid)
result := fold(many)
assert.Equal(t, 120, result, "Fold should multiply many items: 2*3*4*5 = 120")
})
}
func TestFoldMapWithIndex(t *testing.T) {
t.Run("FoldMapWithIndex with 0 items", func(t *testing.T) {
empty := []int{}
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
result := foldMap(empty)
assert.Equal(t, 0, result, "FoldMapWithIndex should return monoid empty for 0 items")
})
t.Run("FoldMapWithIndex with 1 item", func(t *testing.T) {
single := From(10)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
result := foldMap(single)
assert.Equal(t, 10, result, "FoldMapWithIndex should map with index: 0 + 10 = 10")
})
t.Run("FoldMapWithIndex with 2 items", func(t *testing.T) {
two := From(10, 20)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
result := foldMap(two)
assert.Equal(t, 31, result, "FoldMapWithIndex should map with indices: (0+10) + (1+20) = 31")
})
t.Run("FoldMapWithIndex with many items", func(t *testing.T) {
many := From(5, 10, 15, 20)
sumMonoid := N.MonoidSum[int]()
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i * x })
result := foldMap(many)
assert.Equal(t, 100, result, "FoldMapWithIndex should map with indices: (0*5) + (1*10) + (2*15) + (3*20) = 100")
})
t.Run("FoldMapWithIndex with string concatenation - 0 items", func(t *testing.T) {
empty := []string{}
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := foldMap(empty)
assert.Equal(t, "", result, "FoldMapWithIndex should return empty string for 0 items")
})
t.Run("FoldMapWithIndex with string concatenation - 1 item", func(t *testing.T) {
single := From("a")
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := foldMap(single)
assert.Equal(t, "0:a", result, "FoldMapWithIndex should format single item with index")
})
t.Run("FoldMapWithIndex with string concatenation - 2 items", func(t *testing.T) {
two := From("a", "b")
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s,", i, s)
})
result := foldMap(two)
assert.Equal(t, "0:a,1:b,", result, "FoldMapWithIndex should format 2 items with indices")
})
t.Run("FoldMapWithIndex with string concatenation - many items", func(t *testing.T) {
many := From("a", "b", "c", "d")
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("[%d]%s", i, s)
})
result := foldMap(many)
assert.Equal(t, "[0]a[1]b[2]c[3]d", result, "FoldMapWithIndex should format many items with indices")
})
}
func ExampleFoldMap() {

View File

@@ -323,34 +323,49 @@ func Clone[AS ~[]A, A any](f func(A) A) func(as AS) AS {
}
func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B {
empty := m.Empty()
concat := m.Concat
return func(f func(A) B) func(AS) B {
return func(as AS) B {
return array.Reduce(as, func(cur B, a A) B {
return concat(cur, f(a))
}, empty)
switch len(as) {
case 0:
return m.Empty()
case 1:
return f(as[0])
case 2:
return concat(f(as[0]), f(as[1]))
default:
return array.Reduce(as[1:], func(cur B, a A) B {
return concat(cur, f(a))
}, f(as[0]))
}
}
}
}
func FoldMapWithIndex[AS ~[]A, A, B any](m M.Monoid[B]) func(func(int, A) B) func(AS) B {
empty := m.Empty()
concat := m.Concat
return func(f func(int, A) B) func(AS) B {
return func(as AS) B {
return array.ReduceWithIndex(as, func(idx int, cur B, a A) B {
return concat(cur, f(idx, a))
}, empty)
}, m.Empty())
}
}
}
func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A {
empty := m.Empty()
concat := m.Concat
return func(as AS) A {
return array.Reduce(as, concat, empty)
switch len(as) {
case 0:
return m.Empty()
case 1:
return as[0]
case 2:
return concat(as[0], as[1])
default:
return array.Reduce(as[1:], concat, as[0])
}
}
}

View File

@@ -25,7 +25,7 @@ func MonadSequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
ma []HKTA) HKTRA {
return array.MonadSequence(fof, m.Empty(), m.Concat, ma)
return array.MonadSequence(fof, m.Empty, m.Concat, ma)
}
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
@@ -67,7 +67,7 @@ func Sequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
) func([]HKTA) HKTRA {
return array.Sequence[[]HKTA](fof, m.Empty(), m.Concat)
return array.Sequence[[]HKTA](fof, m.Empty, m.Concat)
}
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.

View File

@@ -13,28 +13,218 @@
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package constraints defines a set of useful type constraints for generic programming in Go.
# Overview
This package provides type constraints that can be used with Go generics to restrict
type parameters to specific categories of types. These constraints are similar to those
in Go's standard constraints package but are defined here for consistency within the
fp-go project.
# Type Constraints
Ordered - Types that support comparison operators:
type Ordered interface {
Integer | Float | ~string
}
Used for types that can be compared using <, <=, >, >= operators.
Integer - All integer types (signed and unsigned):
type Integer interface {
Signed | Unsigned
}
Signed - Signed integer types:
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
Unsigned - Unsigned integer types:
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
Float - Floating-point types:
type Float interface {
~float32 | ~float64
}
Complex - Complex number types:
type Complex interface {
~complex64 | ~complex128
}
# Usage Examples
Using Ordered constraint for comparison:
import C "github.com/IBM/fp-go/v2/constraints"
func Min[T C.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
result := Min(5, 3) // 3
result := Min(3.14, 2.71) // 2.71
result := Min("apple", "banana") // "apple"
Using Integer constraint:
func Abs[T C.Integer](n T) T {
if n < 0 {
return -n
}
return n
}
result := Abs(-42) // 42
result := Abs(uint(10)) // 10
Using Float constraint:
func Average[T C.Float](a, b T) T {
return (a + b) / 2
}
result := Average(3.14, 2.86) // 3.0
Using Complex constraint:
func Magnitude[T C.Complex](c T) float64 {
r, i := real(c), imag(c)
return math.Sqrt(r*r + i*i)
}
c := complex(3, 4)
result := Magnitude(c) // 5.0
# Combining Constraints
Constraints can be combined to create more specific type restrictions:
type Number interface {
C.Integer | C.Float | C.Complex
}
func Add[T Number](a, b T) T {
return a + b
}
# Tilde Operator
The ~ operator in type constraints means "underlying type". For example, ~int
matches not only int but also any type whose underlying type is int:
type MyInt int
func Double[T C.Integer](n T) T {
return n * 2
}
var x MyInt = 5
result := Double(x) // Works because MyInt's underlying type is int
# Related Packages
- number: Provides algebraic structures and utilities for numeric types
- ord: Provides ordering operations using these constraints
- eq: Provides equality operations for comparable types
*/
package constraints
// Ordered is a constraint that permits any ordered type: any type that supports
// the operators < <= >= >. Ordered types include integers, floats, and strings.
//
// This constraint is commonly used for comparison operations, sorting, and
// finding minimum/maximum values.
//
// Example:
//
// func Max[T Ordered](a, b T) T {
// if a > b {
// return a
// }
// return b
// }
type Ordered interface {
Integer | Float | ~string
}
// Signed is a constraint that permits any signed integer type.
// This includes int, int8, int16, int32, and int64, as well as any
// types whose underlying type is one of these.
//
// Example:
//
// func Negate[T Signed](n T) T {
// return -n
// }
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// Unsigned is a constraint that permits any unsigned integer type.
// This includes uint, uint8, uint16, uint32, uint64, and uintptr, as well
// as any types whose underlying type is one of these.
//
// Example:
//
// func IsEven[T Unsigned](n T) bool {
// return n%2 == 0
// }
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// Integer is a constraint that permits any integer type, both signed and unsigned.
// This is a union of the Signed and Unsigned constraints.
//
// Example:
//
// func Abs[T Integer](n T) T {
// if n < 0 {
// return -n
// }
// return n
// }
type Integer interface {
Signed | Unsigned
}
// Float is a constraint that permits any floating-point type.
// This includes float32 and float64, as well as any types whose
// underlying type is one of these.
//
// Example:
//
// func Round[T Float](f T) T {
// return T(math.Round(float64(f)))
// }
type Float interface {
~float32 | ~float64
}
// Complex is a constraint that permits any complex numeric type.
// This includes complex64 and complex128, as well as any types whose
// underlying type is one of these.
//
// Example:
//
// func Conjugate[T Complex](c T) T {
// return complex(real(c), -imag(c))
// }
type Complex interface {
~complex64 | ~complex128
}

View File

@@ -18,6 +18,10 @@ package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/internal/witherable"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/option"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
@@ -49,3 +53,43 @@ import (
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
return RIOR.FilterOrElse[context.Context](pred, onFalse)
}
//go:inline
func Filter[HKTA, A any](
filter func(Predicate[A]) Endomorphism[HKTA],
) func(Predicate[A]) Operator[HKTA, HKTA] {
return witherable.Filter(
Map,
filter,
)
}
//go:inline
func FilterArray[A any](p Predicate[A]) Operator[[]A, []A] {
return Filter(array.Filter[A])(p)
}
//go:inline
func FilterIter[A any](p Predicate[A]) Operator[Seq[A], Seq[A]] {
return Filter(iter.Filter[A])(p)
}
//go:inline
func FilterMap[HKTA, HKTB, A, B any](
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
) func(option.Kleisli[A, B]) Operator[HKTA, HKTB] {
return witherable.FilterMap(
Map,
filter,
)
}
//go:inline
func FilterMapArray[A, B any](p option.Kleisli[A, B]) Operator[[]A, []B] {
return FilterMap(array.FilterMap[A, B])(p)
}
//go:inline
func FilterMapIter[A, B any](p option.Kleisli[A, B]) Operator[Seq[A], Seq[B]] {
return FilterMap(iter.FilterMap[A, B])(p)
}

View File

@@ -17,6 +17,7 @@ package readerioresult
import (
"context"
"iter"
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/context/ioresult"
@@ -220,4 +221,10 @@ type (
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
// Seq is an iterator over sequences of individual values.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
Seq[A any] = iter.Seq[A]
)

View File

@@ -0,0 +1,48 @@
package readerreaderioresult
import (
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/internal/witherable"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/option"
)
//go:inline
func Filter[C, HKTA, A any](
filter func(Predicate[A]) Endomorphism[HKTA],
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
return witherable.Filter(
Map[C],
filter,
)
}
//go:inline
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
return Filter[C](array.Filter[A])(p)
}
//go:inline
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
return Filter[C](iter.Filter[A])(p)
}
//go:inline
func FilterMap[C, HKTA, HKTB, A, B any](
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
return witherable.FilterMap(
Map[C],
filter,
)
}
//go:inline
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
return FilterMap[C](array.FilterMap[A, B])(p)
}
//go:inline
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
return FilterMap[C](iter.FilterMap[A, B])(p)
}

View File

@@ -834,7 +834,7 @@ func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
// This is the monadic version that takes the computation as the first parameter.
//
//go:inline
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endomorphism[error]) ReaderReaderIOResult[R, A] {
return RRIOE.MonadMapLeft(fa, f)
}
@@ -843,7 +843,7 @@ func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error])
// This is the curried version that returns an operator.
//
//go:inline
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] {
return RRIOE.MapLeft[R, context.Context, A](f)
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/traversal/result"
@@ -146,9 +147,15 @@ type (
// It's an alias for predicate.Predicate[A].
Predicate[A any] = predicate.Predicate[A]
// Endmorphism represents a function from type A to type A.
// Endomorphism represents a function from type A to type A.
// It's an alias for endomorphism.Endomorphism[A].
Endmorphism[A any] = endomorphism.Endomorphism[A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Seq is an iterator over sequences of individual values.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
Seq[A any] = iter.Seq[A]
Void = function.Void
)

View File

@@ -204,6 +204,102 @@ func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirst(f)
}
// ChainFirstThunkK chains an effect with a function that returns a Thunk,
// but discards the result and returns the original value.
// This is useful for performing side effects (like logging or IO operations) that don't
// need the effect's context, without changing the value flowing through the computation.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the Thunk (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns Thunk[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the Thunk but preserves the original value
//
// # Example
//
// logToFile := func(n int) readerioresult.ReaderIOResult[any] {
// return func(ctx context.Context) io.IO[result.Result[any]] {
// return func() result.Result[any] {
// // Perform IO operation that doesn't need effect context
// fmt.Printf("Logging: %d\n", n)
// return result.Of[any](nil)
// }
// }
// }
//
// eff := effect.Of[MyContext](42)
// logged := effect.ChainFirstThunkK[MyContext](logToFile)(eff)
// // Prints "Logging: 42" but still produces 42
//
// # See Also
//
// - ChainThunkK: Chains with a Thunk and uses its result
// - TapThunkK: Alias for ChainFirstThunkK
// - ChainFirstIOK: Similar but for IO operations
//
//go:inline
func ChainFirstThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[C, A, B],
FromThunk[C, B],
f,
)
}
// TapThunkK is an alias for ChainFirstThunkK.
// It chains an effect with a function that returns a Thunk for side effects,
// but preserves the original value. This is useful for logging, debugging, or
// performing IO operations that don't need the effect's context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the Thunk (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns Thunk[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the Thunk but preserves the original value
//
// # Example
//
// performSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
// return func(ctx context.Context) io.IO[result.Result[any]] {
// return func() result.Result[any] {
// // Perform context-independent IO operation
// log.Printf("Processing value: %d", n)
// return result.Of[any](nil)
// }
// }
// }
//
// eff := effect.Of[MyContext](42)
// tapped := effect.TapThunkK[MyContext](performSideEffect)(eff)
// // Logs "Processing value: 42" but still produces 42
//
// # See Also
//
// - ChainFirstThunkK: The underlying implementation
// - TapIOK: Similar but for IO operations
// - Tap: Similar but for full effects
//
//go:inline
func TapThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, A] {
return ChainFirstThunkK[C](f)
}
// ChainIOK chains an effect with a function that returns an IO action.
// This is useful for integrating IO-based computations (synchronous side effects)
// into effect chains. The IO action is automatically lifted into the Effect context.
@@ -652,6 +748,8 @@ func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
//
// # See Also
//
// See Also:
//
// - Ask: Returns the entire context as the value
// - Map: Transforms the value after extraction
//

View File

@@ -678,6 +678,587 @@ func TestChainThunkK_Integration(t *testing.T) {
})
}
func TestChainFirstThunkK_Success(t *testing.T) {
t.Run("executes thunk but preserves original value", func(t *testing.T) {
sideEffectExecuted := false
sideEffect := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
sideEffectExecuted = true
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Of[TestConfig](42),
ChainFirstThunkK[TestConfig](sideEffect),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.True(t, sideEffectExecuted)
})
t.Run("chains multiple side effects", func(t *testing.T) {
log := []string{}
logValue := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("log: %d", n))
return result.Of[any](nil)
}
}
}
computation := F.Pipe2(
Of[TestConfig](10),
ChainFirstThunkK[TestConfig](logValue),
ChainFirstThunkK[TestConfig](logValue),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, 2, len(log))
assert.Equal(t, "log: 10", log[0])
assert.Equal(t, "log: 10", log[1])
})
t.Run("side effect can access runtime context", func(t *testing.T) {
var capturedCtx context.Context
captureContext := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
capturedCtx = ctx
return result.Of[any](nil)
}
}
}
ctx := context.Background()
computation := F.Pipe1(
Of[TestConfig](42),
ChainFirstThunkK[TestConfig](captureContext),
)
outcome := computation(testConfig)(ctx)()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, ctx, capturedCtx)
})
t.Run("side effect result is discarded", func(t *testing.T) {
returnDifferentValue := func(n int) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) io.IO[result.Result[string]] {
return func() result.Result[string] {
return result.Of("different value")
}
}
}
computation := F.Pipe1(
Of[TestConfig](42),
ChainFirstThunkK[TestConfig](returnDifferentValue),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
}
func TestChainFirstThunkK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := fmt.Errorf("previous error")
sideEffectExecuted := false
sideEffect := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
sideEffectExecuted = true
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainFirstThunkK[TestConfig](sideEffect),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, sideEffectExecuted)
})
t.Run("propagates error from thunk side effect", func(t *testing.T) {
testErr := fmt.Errorf("side effect error")
failingSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
return result.Left[any](testErr)
}
}
}
computation := F.Pipe1(
Of[TestConfig](42),
ChainFirstThunkK[TestConfig](failingSideEffect),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("stops execution on first error", func(t *testing.T) {
testErr := fmt.Errorf("first error")
secondEffectExecuted := false
failingEffect := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
return result.Left[any](testErr)
}
}
}
secondEffect := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
secondEffectExecuted = true
return result.Of[any](nil)
}
}
}
computation := F.Pipe2(
Of[TestConfig](42),
ChainFirstThunkK[TestConfig](failingEffect),
ChainFirstThunkK[TestConfig](secondEffect),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, secondEffectExecuted)
})
}
func TestChainFirstThunkK_EdgeCases(t *testing.T) {
t.Run("handles zero value", func(t *testing.T) {
callCount := 0
countCalls := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
callCount++
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Of[TestConfig](0),
ChainFirstThunkK[TestConfig](countCalls),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(0), outcome)
assert.Equal(t, 1, callCount)
})
t.Run("handles empty string", func(t *testing.T) {
var capturedValue string
captureValue := func(s string) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
capturedValue = s
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Of[TestConfig](""),
ChainFirstThunkK[TestConfig](captureValue),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(""), outcome)
assert.Equal(t, "", capturedValue)
})
t.Run("handles nil pointer", func(t *testing.T) {
var capturedPtr *int
capturePtr := func(ptr *int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
capturedPtr = ptr
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Of[TestConfig]((*int)(nil)),
ChainFirstThunkK[TestConfig](capturePtr),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of((*int)(nil)), outcome)
assert.Nil(t, capturedPtr)
})
}
func TestChainFirstThunkK_Integration(t *testing.T) {
t.Run("composes with Map and Chain", func(t *testing.T) {
log := []string{}
logValue := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("value: %d", n))
return result.Of[any](nil)
}
}
}
computation := F.Pipe3(
Of[TestConfig](5),
Map[TestConfig](func(x int) int { return x * 2 }),
ChainFirstThunkK[TestConfig](logValue),
Map[TestConfig](func(x int) int { return x + 3 }),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(13), outcome) // (5 * 2) + 3
assert.Equal(t, 1, len(log))
assert.Equal(t, "value: 10", log[0])
})
t.Run("composes with ChainThunkK", func(t *testing.T) {
log := []string{}
logSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("side-effect: %d", n))
return result.Of[any](nil)
}
}
}
transformValue := func(n int) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) io.IO[result.Result[string]] {
return func() result.Result[string] {
log = append(log, fmt.Sprintf("transform: %d", n))
return result.Of(fmt.Sprintf("Result: %d", n))
}
}
}
computation := F.Pipe2(
Of[TestConfig](42),
ChainFirstThunkK[TestConfig](logSideEffect),
ChainThunkK[TestConfig](transformValue),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of("Result: 42"), outcome)
assert.Equal(t, 2, len(log))
assert.Equal(t, "side-effect: 42", log[0])
assert.Equal(t, "transform: 42", log[1])
})
t.Run("composes with ChainReaderK and ChainReaderIOK", func(t *testing.T) {
log := []string{}
addMultiplier := func(n int) reader.Reader[TestConfig, int] {
return func(cfg TestConfig) int {
return n + cfg.Multiplier
}
}
logReaderIO := func(n int) readerio.ReaderIO[TestConfig, int] {
return func(cfg TestConfig) io.IO[int] {
return func() int {
log = append(log, fmt.Sprintf("reader-io: %d", n))
return n * 2
}
}
}
logThunk := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("thunk: %d", n))
return result.Of[any](nil)
}
}
}
computation := F.Pipe3(
Of[TestConfig](5),
ChainReaderK(addMultiplier),
ChainReaderIOK(logReaderIO),
ChainFirstThunkK[TestConfig](logThunk),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(16), outcome) // (5 + 3) * 2
assert.Equal(t, 2, len(log))
assert.Equal(t, "reader-io: 8", log[0])
assert.Equal(t, "thunk: 16", log[1])
})
}
func TestTapThunkK_Success(t *testing.T) {
t.Run("is alias for ChainFirstThunkK", func(t *testing.T) {
log := []string{}
logValue := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("tapped: %d", n))
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Of[TestConfig](42),
TapThunkK[TestConfig](logValue),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, 1, len(log))
assert.Equal(t, "tapped: 42", log[0])
})
t.Run("useful for logging without changing value", func(t *testing.T) {
log := []string{}
logStep := func(step string) func(int) readerioresult.ReaderIOResult[any] {
return func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("%s: %d", step, n))
return result.Of[any](nil)
}
}
}
}
computation := F.Pipe4(
Of[TestConfig](10),
TapThunkK[TestConfig](logStep("start")),
Map[TestConfig](func(x int) int { return x * 2 }),
TapThunkK[TestConfig](logStep("after-map")),
Map[TestConfig](func(x int) int { return x + 5 }),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(25), outcome) // (10 * 2) + 5
assert.Equal(t, 2, len(log))
assert.Equal(t, "start: 10", log[0])
assert.Equal(t, "after-map: 20", log[1])
})
t.Run("can perform IO operations", func(t *testing.T) {
var ioExecuted bool
performIO := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
// Simulate IO operation
ioExecuted = true
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Of[TestConfig](42),
TapThunkK[TestConfig](performIO),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.True(t, ioExecuted)
})
}
func TestTapThunkK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := fmt.Errorf("previous error")
tapExecuted := false
tapValue := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
tapExecuted = true
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
Fail[TestConfig, int](testErr),
TapThunkK[TestConfig](tapValue),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, tapExecuted)
})
t.Run("propagates error from tap operation", func(t *testing.T) {
testErr := fmt.Errorf("tap error")
failingTap := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
return result.Left[any](testErr)
}
}
}
computation := F.Pipe1(
Of[TestConfig](42),
TapThunkK[TestConfig](failingTap),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
func TestTapThunkK_EdgeCases(t *testing.T) {
t.Run("handles multiple taps in sequence", func(t *testing.T) {
log := []string{}
tap1 := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, "tap1")
return result.Of[any](nil)
}
}
}
tap2 := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, "tap2")
return result.Of[any](nil)
}
}
}
tap3 := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, "tap3")
return result.Of[any](nil)
}
}
}
computation := F.Pipe3(
Of[TestConfig](42),
TapThunkK[TestConfig](tap1),
TapThunkK[TestConfig](tap2),
TapThunkK[TestConfig](tap3),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tap1", "tap2", "tap3"}, log)
})
}
func TestTapThunkK_Integration(t *testing.T) {
t.Run("real-world logging scenario", func(t *testing.T) {
log := []string{}
logStart := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("Starting computation with: %d", n))
return result.Of[any](nil)
}
}
}
logIntermediate := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("Intermediate result: %d", n))
return result.Of[any](nil)
}
}
}
logFinal := func(s string) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("Final result: %s", s))
return result.Of[any](nil)
}
}
}
computation := F.Pipe5(
Of[TestConfig](10),
TapThunkK[TestConfig](logStart),
Map[TestConfig](func(x int) int { return x * 3 }),
TapThunkK[TestConfig](logIntermediate),
Map[TestConfig](func(x int) string { return fmt.Sprintf("Value: %d", x) }),
TapThunkK[TestConfig](logFinal),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 30"), outcome)
assert.Equal(t, 3, len(log))
assert.Equal(t, "Starting computation with: 10", log[0])
assert.Equal(t, "Intermediate result: 30", log[1])
assert.Equal(t, "Final result: Value: 30", log[2])
})
t.Run("composes with FromThunk", func(t *testing.T) {
log := []string{}
thunk := func(ctx context.Context) io.IO[result.Result[int]] {
return func() result.Result[int] {
return result.Of(100)
}
}
logValue := func(n int) readerioresult.ReaderIOResult[any] {
return func(ctx context.Context) io.IO[result.Result[any]] {
return func() result.Result[any] {
log = append(log, fmt.Sprintf("value: %d", n))
return result.Of[any](nil)
}
}
}
computation := F.Pipe1(
FromThunk[TestConfig](thunk),
TapThunkK[TestConfig](logValue),
)
outcome := computation(testConfig)(context.Background())()
assert.Equal(t, result.Of(100), outcome)
assert.Equal(t, 1, len(log))
assert.Equal(t, "value: 100", log[0])
})
}
func TestAsks_Success(t *testing.T) {
t.Run("extracts a field from context", func(t *testing.T) {
type Config struct {
@@ -685,7 +1266,7 @@ func TestAsks_Success(t *testing.T) {
Port int
}
getHost := Asks[Config](func(cfg Config) string {
getHost := Asks(func(cfg Config) string {
return cfg.Host
})
@@ -701,7 +1282,7 @@ func TestAsks_Success(t *testing.T) {
Port int
}
getURL := Asks[Config](func(cfg Config) string {
getURL := Asks(func(cfg Config) string {
return fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)
})
@@ -712,7 +1293,7 @@ func TestAsks_Success(t *testing.T) {
})
t.Run("extracts numeric field", func(t *testing.T) {
getPort := Asks[TestConfig](func(cfg TestConfig) int {
getPort := Asks(func(cfg TestConfig) int {
return cfg.Multiplier
})
@@ -728,7 +1309,7 @@ func TestAsks_Success(t *testing.T) {
Height int
}
getArea := Asks[Config](func(cfg Config) int {
getArea := Asks(func(cfg Config) int {
return cfg.Width * cfg.Height
})
@@ -739,7 +1320,7 @@ func TestAsks_Success(t *testing.T) {
})
t.Run("transforms string field", func(t *testing.T) {
getUpperPrefix := Asks[TestConfig](func(cfg TestConfig) string {
getUpperPrefix := Asks(func(cfg TestConfig) string {
return fmt.Sprintf("[%s]", cfg.Prefix)
})
@@ -756,7 +1337,7 @@ func TestAsks_EdgeCases(t *testing.T) {
Value int
}
getValue := Asks[Config](func(cfg Config) int {
getValue := Asks(func(cfg Config) int {
return cfg.Value
})
@@ -771,7 +1352,7 @@ func TestAsks_EdgeCases(t *testing.T) {
Name string
}
getName := Asks[Config](func(cfg Config) string {
getName := Asks(func(cfg Config) string {
return cfg.Name
})
@@ -786,7 +1367,7 @@ func TestAsks_EdgeCases(t *testing.T) {
Data *string
}
hasData := Asks[Config](func(cfg Config) bool {
hasData := Asks(func(cfg Config) bool {
return cfg.Data != nil
})
@@ -805,7 +1386,7 @@ func TestAsks_EdgeCases(t *testing.T) {
DB Database
}
getDBHost := Asks[Config](func(cfg Config) string {
getDBHost := Asks(func(cfg Config) string {
return cfg.DB.Host
})
@@ -825,7 +1406,7 @@ func TestAsks_Integration(t *testing.T) {
}
computation := F.Pipe1(
Asks[Config](func(cfg Config) int {
Asks(func(cfg Config) int {
return cfg.Value
}),
Map[Config](func(x int) int { return x * 2 }),
@@ -843,7 +1424,7 @@ func TestAsks_Integration(t *testing.T) {
}
computation := F.Pipe1(
Asks[Config](func(cfg Config) int {
Asks(func(cfg Config) int {
return cfg.Multiplier
}),
Chain(func(mult int) Effect[Config, int] {
@@ -859,7 +1440,7 @@ func TestAsks_Integration(t *testing.T) {
t.Run("composes with ChainReaderK", func(t *testing.T) {
computation := F.Pipe1(
Asks[TestConfig](func(cfg TestConfig) int {
Asks(func(cfg TestConfig) int {
return cfg.Multiplier
}),
ChainReaderK(func(mult int) reader.Reader[TestConfig, int] {
@@ -879,7 +1460,7 @@ func TestAsks_Integration(t *testing.T) {
log := []string{}
computation := F.Pipe1(
Asks[TestConfig](func(cfg TestConfig) string {
Asks(func(cfg TestConfig) string {
return cfg.Prefix
}),
ChainReaderIOK(func(prefix string) readerio.ReaderIO[TestConfig, string] {
@@ -906,11 +1487,11 @@ func TestAsks_Integration(t *testing.T) {
}
computation := F.Pipe2(
Asks[Config](func(cfg Config) string {
Asks(func(cfg Config) string {
return cfg.First
}),
Chain(func(_ string) Effect[Config, string] {
return Asks[Config](func(cfg Config) string {
return Asks(func(cfg Config) string {
return cfg.Second
})
}),
@@ -933,7 +1514,7 @@ func TestAsks_Integration(t *testing.T) {
computation := F.Pipe1(
Ask[Config](),
Chain(func(cfg Config) Effect[Config, int] {
return Asks[Config](func(c Config) int {
return Asks(func(c Config) int {
return c.Value * 2
})
}),
@@ -953,7 +1534,7 @@ func TestAsks_Comparison(t *testing.T) {
}
// Using Asks
asksVersion := Asks[Config](func(cfg Config) int {
asksVersion := Asks(func(cfg Config) int {
return cfg.Port
})
@@ -983,7 +1564,7 @@ func TestAsks_Comparison(t *testing.T) {
}
// Asks is more direct for field extraction
getHost := Asks[Config](func(cfg Config) string {
getHost := Asks(func(cfg Config) string {
return cfg.Host
})
@@ -1003,7 +1584,7 @@ func TestAsks_RealWorldScenarios(t *testing.T) {
User string
}
getConnectionString := Asks[DatabaseConfig](func(cfg DatabaseConfig) string {
getConnectionString := Asks(func(cfg DatabaseConfig) string {
return fmt.Sprintf("postgres://%s@%s:%d/%s",
cfg.User, cfg.Host, cfg.Port, cfg.Database)
})
@@ -1027,7 +1608,7 @@ func TestAsks_RealWorldScenarios(t *testing.T) {
BasePath string
}
getEndpoint := Asks[APIConfig](func(cfg APIConfig) string {
getEndpoint := Asks(func(cfg APIConfig) string {
return fmt.Sprintf("%s://%s:%d%s",
cfg.Protocol, cfg.Host, cfg.Port, cfg.BasePath)
})
@@ -1049,7 +1630,7 @@ func TestAsks_RealWorldScenarios(t *testing.T) {
MaxRetries int
}
isValid := Asks[Config](func(cfg Config) bool {
isValid := Asks(func(cfg Config) bool {
return cfg.Timeout > 0 && cfg.MaxRetries >= 0
})

296
v2/effect/filter.go Normal file
View File

@@ -0,0 +1,296 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/option"
)
// Filter lifts a filtering operation on a higher-kinded type into an Effect operator.
// This is a generic function that works with any filterable data structure by taking
// a filter function and returning an operator that can be used in effect chains.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - HKTA: The higher-kinded type being filtered (e.g., []A, Seq[A])
// - A: The element type being filtered
//
// # Parameters
//
// - filter: A function that takes a predicate and returns an endomorphism on HKTA
//
// # Returns
//
// - func(Predicate[A]) Operator[C, HKTA, HKTA]: A function that takes a predicate
// and returns an operator that filters effects containing HKTA values
//
// # Example Usage
//
// import A "github.com/IBM/fp-go/v2/array"
//
// // Create a custom filter operator for arrays
// filterOp := Filter[MyContext](A.Filter[int])
// isEven := func(n int) bool { return n%2 == 0 }
//
// pipeline := F.Pipe2(
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
// filterOp(isEven),
// Map[MyContext](func(arr []int) int { return len(arr) }),
// )
// // Result: Effect that produces 2 (count of even numbers)
//
// # See Also
//
// - FilterArray: Specialized version for array filtering
// - FilterIter: Specialized version for iterator filtering
// - FilterMap: For filtering and mapping simultaneously
//
//go:inline
func Filter[C, HKTA, A any](
filter func(Predicate[A]) Endomorphism[HKTA],
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
return readerreaderioresult.Filter[C](filter)
}
// FilterArray creates an operator that filters array elements within an Effect based on a predicate.
// Elements that satisfy the predicate are kept, while others are removed.
// This is a specialized version of Filter for arrays.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The element type in the array
//
// # Parameters
//
// - p: A predicate function that tests each element
//
// # Returns
//
// - Operator[C, []A, []A]: An operator that filters array elements in an effect
//
// # Example Usage
//
// isPositive := func(n int) bool { return n > 0 }
// filterPositive := FilterArray[MyContext](isPositive)
//
// pipeline := F.Pipe1(
// Succeed[MyContext]([]int{-2, -1, 0, 1, 2, 3}),
// filterPositive,
// )
// // Result: Effect that produces []int{1, 2, 3}
//
// # See Also
//
// - Filter: Generic version for any filterable type
// - FilterIter: For filtering iterators
// - FilterMapArray: For filtering and mapping arrays simultaneously
//
//go:inline
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
return readerreaderioresult.FilterArray[C](p)
}
// FilterIter creates an operator that filters iterator elements within an Effect based on a predicate.
// Elements that satisfy the predicate are kept in the resulting iterator, while others are removed.
// This is a specialized version of Filter for iterators (Seq).
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The element type in the iterator
//
// # Parameters
//
// - p: A predicate function that tests each element
//
// # Returns
//
// - Operator[C, Seq[A], Seq[A]]: An operator that filters iterator elements in an effect
//
// # Example Usage
//
// isEven := func(n int) bool { return n%2 == 0 }
// filterEven := FilterIter[MyContext](isEven)
//
// pipeline := F.Pipe1(
// Succeed[MyContext](slices.Values([]int{1, 2, 3, 4, 5, 6})),
// filterEven,
// )
// // Result: Effect that produces an iterator over [2, 4, 6]
//
// # See Also
//
// - Filter: Generic version for any filterable type
// - FilterArray: For filtering arrays
// - FilterMapIter: For filtering and mapping iterators simultaneously
//
//go:inline
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
return readerreaderioresult.FilterIter[C](p)
}
// FilterMap lifts a filter-map operation on a higher-kinded type into an Effect operator.
// This combines filtering and mapping in a single operation: elements are transformed
// using a function that returns Option, and only Some values are kept in the result.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - HKTA: The input higher-kinded type (e.g., []A, Seq[A])
// - HKTB: The output higher-kinded type (e.g., []B, Seq[B])
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - filter: A function that takes an option.Kleisli and returns a transformation from HKTA to HKTB
//
// # Returns
//
// - func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB]: A function that takes a Kleisli arrow
// and returns an operator that filter-maps effects
//
// # Example Usage
//
// import A "github.com/IBM/fp-go/v2/array"
// import O "github.com/IBM/fp-go/v2/option"
//
// // Parse and filter positive integers
// parsePositive := func(s string) O.Option[int] {
// var n int
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n > 0 {
// return O.Some(n)
// }
// return O.None[int]()
// }
//
// filterMapOp := FilterMap[MyContext](A.FilterMap[string, int])
// pipeline := F.Pipe1(
// Succeed[MyContext]([]string{"1", "-2", "3", "invalid", "5"}),
// filterMapOp(parsePositive),
// )
// // Result: Effect that produces []int{1, 3, 5}
//
// # See Also
//
// - FilterMapArray: Specialized version for arrays
// - FilterMapIter: Specialized version for iterators
// - Filter: For filtering without transformation
//
//go:inline
func FilterMap[C, HKTA, HKTB, A, B any](
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
return readerreaderioresult.FilterMap[C](filter)
}
// FilterMapArray creates an operator that filters and maps array elements within an Effect.
// Each element is transformed using a function that returns Option[B]. Elements that
// produce Some(b) are kept in the result array, while None values are filtered out.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
//
// # Returns
//
// - Operator[C, []A, []B]: An operator that filter-maps array elements in an effect
//
// # Example Usage
//
// import O "github.com/IBM/fp-go/v2/option"
//
// // Double even numbers, filter out odd numbers
// doubleEven := func(n int) O.Option[int] {
// if n%2 == 0 {
// return O.Some(n * 2)
// }
// return O.None[int]()
// }
//
// pipeline := F.Pipe1(
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
// FilterMapArray[MyContext](doubleEven),
// )
// // Result: Effect that produces []int{4, 8}
//
// # See Also
//
// - FilterMap: Generic version for any filterable type
// - FilterMapIter: For filter-mapping iterators
// - FilterArray: For filtering without transformation
//
//go:inline
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
return readerreaderioresult.FilterMapArray[C](p)
}
// FilterMapIter creates an operator that filters and maps iterator elements within an Effect.
// Each element is transformed using a function that returns Option[B]. Elements that
// produce Some(b) are kept in the resulting iterator, while None values are filtered out.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
//
// # Returns
//
// - Operator[C, Seq[A], Seq[B]]: An operator that filter-maps iterator elements in an effect
//
// # Example Usage
//
// import O "github.com/IBM/fp-go/v2/option"
//
// // Parse strings to integers, keeping only valid ones
// parseInt := func(s string) O.Option[int] {
// var n int
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
// return O.Some(n)
// }
// return O.None[int]()
// }
//
// pipeline := F.Pipe1(
// Succeed[MyContext](slices.Values([]string{"1", "2", "invalid", "3"})),
// FilterMapIter[MyContext](parseInt),
// )
// // Result: Effect that produces an iterator over [1, 2, 3]
//
// # See Also
//
// - FilterMap: Generic version for any filterable type
// - FilterMapArray: For filter-mapping arrays
// - FilterIter: For filtering without transformation
//
//go:inline
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
return readerreaderioresult.FilterMapIter[C](p)
}

653
v2/effect/filter_test.go Normal file
View File

@@ -0,0 +1,653 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"errors"
"fmt"
"slices"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
type FilterTestConfig struct {
MaxValue int
MinValue int
}
// Helper to collect iterator results from an effect
func collectSeqEffect[C, A any](eff Effect[C, Seq[A]], cfg C) []A {
result, err := runEffect(eff, cfg)
if err != nil {
return nil
}
return slices.Collect(result)
}
func TestFilterArray_Success(t *testing.T) {
t.Run("filters array keeping matching elements", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
input := Succeed[FilterTestConfig]([]int{1, -2, 3, -4, 5})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{1, 3, 5}, result)
})
t.Run("returns empty array when no elements match", func(t *testing.T) {
// Arrange
isNegative := N.LessThan(0)
filterOp := FilterArray[FilterTestConfig](isNegative)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("returns all elements when all match", func(t *testing.T) {
// Arrange
alwaysTrue := func(n int) bool { return true }
filterOp := FilterArray[FilterTestConfig](alwaysTrue)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, result)
})
}
func TestFilterIter_Success(t *testing.T) {
t.Run("filters iterator keeping matching elements", func(t *testing.T) {
// Arrange
isEven := func(n int) bool { return n%2 == 0 }
filterOp := FilterIter[FilterTestConfig](isEven)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5, 6}))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, []int{2, 4, 6}, collected)
})
t.Run("returns empty iterator when no elements match", func(t *testing.T) {
// Arrange
isNegative := N.LessThan(0)
filterOp := FilterIter[FilterTestConfig](isNegative)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3}))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Empty(t, collected)
})
}
func TestFilterArray_WithContext(t *testing.T) {
t.Run("uses context for filtering", func(t *testing.T) {
// Arrange
cfg := FilterTestConfig{MaxValue: 100, MinValue: 0}
inRange := func(n int) bool { return n >= cfg.MinValue && n <= cfg.MaxValue }
filterOp := FilterArray[FilterTestConfig](inRange)
input := Succeed[FilterTestConfig]([]int{-10, 50, 150, 75})
// Act
result, err := runEffect(filterOp(input), cfg)
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{50, 75}, result)
})
}
func TestFilterArray_EdgeCases(t *testing.T) {
t.Run("handles empty array", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
input := Succeed[FilterTestConfig]([]int{})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("preserves error from input", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
inputErr := errors.New("input error")
input := Fail[FilterTestConfig, []int](inputErr)
// Act
_, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, inputErr, err)
})
}
func TestFilterIter_EdgeCases(t *testing.T) {
t.Run("handles empty iterator", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterIter[FilterTestConfig](isPositive)
input := Succeed[FilterTestConfig](slices.Values([]int{}))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Empty(t, collected)
})
t.Run("preserves error from input", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterIter[FilterTestConfig](isPositive)
inputErr := errors.New("input error")
input := Fail[FilterTestConfig, Seq[int]](inputErr)
// Act
_, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, inputErr, err)
})
}
func TestFilter_GenericFilter(t *testing.T) {
t.Run("works with custom filter function", func(t *testing.T) {
// Arrange
customFilter := func(p Predicate[int]) Endomorphism[[]int] {
return A.Filter(p)
}
filterOp := Filter[FilterTestConfig](customFilter)
isEven := func(n int) bool { return n%2 == 0 }
input := Succeed[FilterTestConfig]([]int{1, 2, 3, 4, 5})
// Act
result, err := runEffect(filterOp(isEven)(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4}, result)
})
}
func TestFilterMapArray_Success(t *testing.T) {
t.Run("filters and maps array elements", func(t *testing.T) {
// Arrange
parsePositive := func(n int) O.Option[string] {
if n > 0 {
return O.Some(fmt.Sprintf("positive:%d", n))
}
return O.None[string]()
}
filterMapOp := FilterMapArray[FilterTestConfig](parsePositive)
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4, 5})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"positive:2", "positive:4", "positive:5"}, result)
})
t.Run("returns empty when no elements match", func(t *testing.T) {
// Arrange
neverMatch := func(n int) O.Option[int] {
return O.None[int]()
}
filterMapOp := FilterMapArray[FilterTestConfig](neverMatch)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("maps all elements when all match", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6}, result)
})
}
func TestFilterMapIter_Success(t *testing.T) {
t.Run("filters and maps iterator elements", func(t *testing.T) {
// Arrange
doubleEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
filterMapOp := FilterMapIter[FilterTestConfig](doubleEven)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
// Act
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, []int{4, 8}, collected)
})
}
func TestFilterMapArray_TypeConversion(t *testing.T) {
t.Run("converts int to string", func(t *testing.T) {
// Arrange
intToString := func(n int) O.Option[string] {
if n > 0 {
return O.Some(fmt.Sprintf("%d", n))
}
return O.None[string]()
}
filterMapOp := FilterMapArray[FilterTestConfig](intToString)
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"2", "4"}, result)
})
t.Run("converts string to int", func(t *testing.T) {
// Arrange
parseEven := func(s string) O.Option[int] {
var n int
if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n%2 == 0 {
return O.Some(n)
}
return O.None[int]()
}
filterMapOp := FilterMapArray[FilterTestConfig](parseEven)
input := Succeed[FilterTestConfig]([]string{"1", "2", "3", "4", "invalid"})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4}, result)
})
}
func TestFilterMapArray_EdgeCases(t *testing.T) {
t.Run("handles empty array", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
input := Succeed[FilterTestConfig]([]int{})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{}, result)
})
t.Run("preserves error from input", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
inputErr := errors.New("input error")
input := Fail[FilterTestConfig, []int](inputErr)
// Act
_, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, inputErr, err)
})
}
func TestFilterMapIter_EdgeCases(t *testing.T) {
t.Run("handles empty iterator", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapIter[FilterTestConfig](double)
input := Succeed[FilterTestConfig](slices.Values([]int{}))
// Act
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Empty(t, collected)
})
}
func TestFilterMap_GenericFilterMap(t *testing.T) {
t.Run("works with custom filterMap function", func(t *testing.T) {
// Arrange
customFilterMap := func(f O.Kleisli[int, string]) Reader[[]int, []string] {
return A.FilterMap(f)
}
filterMapOp := FilterMap[FilterTestConfig](customFilterMap)
intToString := func(n int) O.Option[string] {
if n > 0 {
return O.Some(fmt.Sprintf("%d", n))
}
return O.None[string]()
}
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
// Act
result, err := runEffect(filterMapOp(intToString)(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"2", "4"}, result)
})
}
func TestFilter_Composition(t *testing.T) {
t.Run("chains multiple filters", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
isEven := func(n int) bool { return n%2 == 0 }
filterPositive := FilterArray[FilterTestConfig](isPositive)
filterEven := FilterArray[FilterTestConfig](isEven)
input := Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6})
// Act
result, err := runEffect(filterEven(filterPositive(input)), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6}, result)
})
t.Run("chains filter and filterMap", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
doubleEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
filterOp := FilterArray[FilterTestConfig](isPositive)
filterMapOp := FilterMapArray[FilterTestConfig](doubleEven)
input := Succeed[FilterTestConfig]([]int{-2, 1, 2, 3, 4, 5})
// Act
result, err := runEffect(filterMapOp(filterOp(input)), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{4, 8}, result)
})
}
func TestFilter_WithComplexTypes(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("filters structs", func(t *testing.T) {
// Arrange
isAdult := func(u User) bool { return u.Age >= 18 }
filterOp := FilterArray[FilterTestConfig](isAdult)
users := []User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 16},
{Name: "Charlie", Age: 30},
}
input := Succeed[FilterTestConfig](users)
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
expected := []User{
{Name: "Alice", Age: 25},
{Name: "Charlie", Age: 30},
}
assert.Equal(t, expected, result)
})
t.Run("filterMaps structs to different type", func(t *testing.T) {
// Arrange
extractAdultName := func(u User) O.Option[string] {
if u.Age >= 18 {
return O.Some(u.Name)
}
return O.None[string]()
}
filterMapOp := FilterMapArray[FilterTestConfig](extractAdultName)
users := []User{
{Name: "Alice", Age: 25},
{Name: "Bob", Age: 16},
{Name: "Charlie", Age: 30},
}
input := Succeed[FilterTestConfig](users)
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []string{"Alice", "Charlie"}, result)
})
}
func TestFilter_BoundaryConditions(t *testing.T) {
t.Run("filters with boundary predicate", func(t *testing.T) {
// Arrange
inRange := func(n int) bool { return n >= 0 && n <= 100 }
filterOp := FilterArray[FilterTestConfig](inRange)
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
// Act
result, err := runEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{0, 50, 100}, result)
})
t.Run("filterMap with boundary conditions", func(t *testing.T) {
// Arrange
clampToRange := func(n int) O.Option[int] {
if n >= 0 && n <= 100 {
return O.Some(n)
}
return O.None[int]()
}
filterMapOp := FilterMapArray[FilterTestConfig](clampToRange)
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
// Act
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.NoError(t, err)
assert.Equal(t, []int{0, 50, 100}, result)
})
}
func TestFilter_WithIterators(t *testing.T) {
t.Run("filters large iterator efficiently", func(t *testing.T) {
// Arrange
isEven := func(n int) bool { return n%2 == 0 }
filterOp := FilterIter[FilterTestConfig](isEven)
// Create iterator for range 0-99
makeSeq := func(yield func(int) bool) {
for i := range 100 {
if !yield(i) {
return
}
}
}
input := Succeed[FilterTestConfig](Seq[int](makeSeq))
// Act
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, 50, len(collected))
assert.Equal(t, 0, collected[0])
assert.Equal(t, 98, collected[49])
})
t.Run("filterMap with iterator", func(t *testing.T) {
// Arrange
squareEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * n)
}
return O.None[int]()
}
filterMapOp := FilterMapIter[FilterTestConfig](squareEven)
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
// Act
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
// Assert
assert.Equal(t, []int{4, 16}, collected)
})
}
func TestFilter_ErrorPropagation(t *testing.T) {
t.Run("filter propagates Left through chain", func(t *testing.T) {
// Arrange
isPositive := N.MoreThan(0)
filterOp := FilterArray[FilterTestConfig](isPositive)
originalErr := errors.New("original error")
// Create an effect that fails
failedEffect := F.Pipe1(
Succeed[FilterTestConfig]([]int{1, 2, 3}),
Chain(func([]int) Effect[FilterTestConfig, []int] {
return Fail[FilterTestConfig, []int](originalErr)
}),
)
// Act
_, err := runEffect(filterOp(failedEffect), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, originalErr, err)
})
t.Run("filterMap propagates Left through chain", func(t *testing.T) {
// Arrange
double := func(n int) O.Option[int] {
return O.Some(n * 2)
}
filterMapOp := FilterMapArray[FilterTestConfig](double)
originalErr := errors.New("original error")
// Create an effect that fails
failedEffect := F.Pipe1(
Succeed[FilterTestConfig]([]int{1, 2, 3}),
Chain(func([]int) Effect[FilterTestConfig, []int] {
return Fail[FilterTestConfig, []int](originalErr)
}),
)
// Act
_, err := runEffect(filterMapOp(failedEffect), FilterTestConfig{})
// Assert
assert.Error(t, err)
assert.Equal(t, originalErr, err)
})
}
func TestFilter_Integration(t *testing.T) {
t.Run("complex filtering pipeline", func(t *testing.T) {
// Arrange: Filter positive numbers, then double evens, then filter > 5
isPositive := N.MoreThan(0)
doubleEven := func(n int) O.Option[int] {
if n%2 == 0 {
return O.Some(n * 2)
}
return O.None[int]()
}
isGreaterThan5 := N.MoreThan(5)
pipeline := F.Pipe3(
Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6}),
FilterArray[FilterTestConfig](isPositive),
FilterMapArray[FilterTestConfig](doubleEven),
FilterArray[FilterTestConfig](isGreaterThan5),
)
// Act
result, err := runEffect(pipeline, FilterTestConfig{})
// Assert
assert.NoError(t, err)
// Positive: [1,2,3,4,5,6] -> DoubleEven: [4,8,12] -> >5: [8,12]
assert.Equal(t, []int{8, 12}, result)
})
}

View File

@@ -19,9 +19,11 @@ import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/iterator/iter"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
@@ -89,4 +91,14 @@ type (
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
// It's used for lifting operations over effects.
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
// Endomorphism represents a function from type A to type A.
// It's an alias for endomorphism.Endomorphism[A].
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Seq is an iterator over sequences of individual values.
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
// stopping early if yield returns false.
// See the [iter] package documentation for more details.
Seq[A any] = iter.Seq[A]
)

View File

@@ -4,7 +4,7 @@ go 1.24
require (
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.7.0
github.com/urfave/cli/v3 v3.8.0
)
require (

View File

@@ -4,10 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -46,7 +46,7 @@ import (
// - Multiple elements: recursively divides and conquers
func MonadSequenceSegment[HKTB, HKTRB any](
fof func(HKTB) HKTRB,
empty HKTRB,
empty func() HKTRB,
concat func(HKTRB, HKTRB) HKTRB,
fbs []HKTB,
start, end int,
@@ -54,7 +54,7 @@ func MonadSequenceSegment[HKTB, HKTRB any](
switch end - start {
case 0:
return empty
return empty()
case 1:
return fof(fbs[start])
default:
@@ -254,7 +254,7 @@ HKTAB = HKT<func(A)B>
*/
func MonadSequence[GA ~[]HKTA, HKTA, HKTRA any](
fof func(HKTA) HKTRA,
empty HKTRA,
empty func() HKTRA,
concat func(HKTRA, HKTRA) HKTRA,
ta GA) HKTRA {
@@ -263,7 +263,7 @@ func MonadSequence[GA ~[]HKTA, HKTA, HKTRA any](
func Sequence[GA ~[]HKTA, HKTA, HKTRA any](
fof func(HKTA) HKTRA,
empty HKTRA,
empty func() HKTRA,
concat func(HKTRA, HKTRA) HKTRA,
) func(GA) HKTRA {

View File

@@ -0,0 +1,15 @@
package filterable
import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
)
type (
Option[A any] = option.Option[A]
Separated[A, B any] = pair.Pair[A, B]
FilterType[A, HKTA any] = func(func(A) bool) func(HKTA) HKTA
FilterMapType[A, B, HKTA, HKTB any] = func(func(A) Option[B]) func(HKTA) HKTB
)

View File

@@ -73,7 +73,7 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
fof := F.Bind2nd(fmap_b, Of[GB])
empty := fof_gb(Empty[GB]())
empty := F.Nullary2(Empty[GB], fof_gb)
cb := F.Curry2(Concat[GB])
concat_gb := F.Bind2nd(fmap_gb, cb)
@@ -180,7 +180,7 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
// convert to an array
hktb := ToArray[GA, []HKTA](ta)
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
return INTA.MonadSequenceSegment(fof, m.Empty, m.Concat, hktb, 0, len(hktb))
}
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
@@ -223,7 +223,7 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
// convert to an array
hktb := MonadMapToArrayWithIndex[GA, []HKTB](ta, f)
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
return INTA.MonadSequenceSegment(fof, m.Empty, m.Concat, hktb, 0, len(hktb))
}
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.

View File

@@ -0,0 +1,27 @@
package witherable
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/filterable"
"github.com/IBM/fp-go/v2/internal/functor"
)
func Filter[A, HKT_G_A, HKT_F_HKT_G_A any](
fmap functor.MapType[HKT_G_A, HKT_G_A, HKT_F_HKT_G_A, HKT_F_HKT_G_A],
ffilter filterable.FilterType[A, HKT_G_A],
) func(func(A) bool) func(HKT_F_HKT_G_A) HKT_F_HKT_G_A {
return function.Flow2(
ffilter,
fmap,
)
}
func FilterMap[A, B, HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B any](
fmap functor.MapType[HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B],
ffilter filterable.FilterMapType[A, B, HKT_G_A, HKT_G_B],
) func(func(A) Option[B]) func(HKT_F_HKT_G_A) HKT_F_HKT_G_B {
return function.Flow2(
ffilter,
fmap,
)
}

View File

@@ -0,0 +1 @@
package witherable

View File

@@ -0,0 +1,7 @@
package witherable
import "github.com/IBM/fp-go/v2/option"
type (
Option[A any] = option.Option[A]
)

195
v2/iterator/iter/async.go Normal file
View File

@@ -0,0 +1,195 @@
// 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 (
N "github.com/IBM/fp-go/v2/number"
)
// Async converts a synchronous sequence into an asynchronous buffered sequence.
// It spawns a goroutine to consume the input sequence and sends values through
// a buffered channel, allowing concurrent production and consumption of elements.
//
// The function provides backpressure control through the buffer size and properly
// handles early termination when the consumer stops iterating. This is useful for
// decoupling producers and consumers, enabling pipeline parallelism, or when you
// need to process sequences concurrently.
//
// # Type Parameters
//
// - T: The type of elements in the sequence
//
// # Parameters
//
// - input: The source sequence to be consumed asynchronously
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
// A larger buffer allows more elements to be produced ahead of consumption,
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
// synchronization between producer and consumer.
//
// # Returns
//
// - Seq[T]: A new sequence that yields elements from the input sequence asynchronously
//
// # Behavior
//
// - Spawns a goroutine that consumes the input sequence
// - Elements are sent through a buffered channel to the output sequence
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
// the producer goroutine is signaled to stop via a done channel
// - Both the producer goroutine and the done channel are properly cleaned up
// - The channel is closed when the input sequence is exhausted or early termination occurs
//
// # Example Usage
//
// // Create an async sequence with a buffer of 10
// seq := From(1, 2, 3, 4, 5)
// async := Async(seq, 10)
//
// // Elements are produced concurrently
// for v := range async {
// fmt.Println(v) // Prints: 1, 2, 3, 4, 5
// }
//
// # Example with Early Termination
//
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// async := Async(seq, 5)
//
// // Stop after 3 elements - producer goroutine will be properly cleaned up
// count := 0
// for v := range async {
// fmt.Println(v)
// count++
// if count >= 3 {
// break
// }
// }
//
// # Example with Unbuffered Channel
//
// // bufSize of 0 creates an unbuffered channel
// seq := From(1, 2, 3)
// async := Async(seq, 0)
//
// // Producer and consumer are synchronized
// for v := range async {
// fmt.Println(v)
// }
//
// # See Also
//
// - From: Creates a sequence from values
// - Map: Transforms sequence elements
// - Filter: Filters sequence elements
func Async[T any](input Seq[T], bufSize int) Seq[T] {
return func(yield func(T) bool) {
ch := make(chan T, N.Max(bufSize, 0))
done := make(chan Void)
go func() {
defer close(ch)
for v := range input {
select {
case ch <- v:
case <-done:
return
}
}
}()
defer close(done)
for v := range ch {
if !yield(v) {
return
}
}
}
}
// Async2 converts a synchronous key-value sequence into an asynchronous buffered sequence.
// It spawns a goroutine to consume the input sequence and sends key-value pairs through
// a buffered channel, allowing concurrent production and consumption of elements.
//
// This function is the Seq2 variant of Async, providing the same asynchronous behavior
// for key-value sequences. It internally converts the Seq2 to a sequence of Pairs,
// applies Async, and converts back to Seq2.
//
// # Type Parameters
//
// - K: The type of keys in the sequence
// - V: The type of values in the sequence
//
// # Parameters
//
// - input: The source key-value sequence to be consumed asynchronously
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
// A larger buffer allows more elements to be produced ahead of consumption,
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
// synchronization between producer and consumer.
//
// # Returns
//
// - Seq2[K, V]: A new key-value sequence that yields elements from the input sequence asynchronously
//
// # Behavior
//
// - Spawns a goroutine that consumes the input key-value sequence
// - Key-value pairs are sent through a buffered channel to the output sequence
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
// the producer goroutine is signaled to stop via a done channel
// - Both the producer goroutine and the done channel are properly cleaned up
// - The channel is closed when the input sequence is exhausted or early termination occurs
//
// # Example Usage
//
// // Create an async key-value sequence with a buffer of 10
// seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
// async := Async2(seq, 10)
//
// // Elements are produced concurrently
// for k, v := range async {
// fmt.Printf("%d: %s\n", k, v)
// }
// // Output:
// // 1: a
// // 2: b
// // 3: c
//
// # Example with Early Termination
//
// seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
// async := Async2(seq, 5)
//
// // Stop after 2 pairs - producer goroutine will be properly cleaned up
// count := 0
// for k, v := range async {
// fmt.Printf("%d: %s\n", k, v)
// count++
// if count >= 2 {
// break
// }
// }
//
// # See Also
//
// - Async: Asynchronous sequence for single-value sequences
// - ToSeqPair: Converts Seq2 to Seq of Pairs
// - FromSeqPair: Converts Seq of Pairs to Seq2
// - MonadZip: Creates key-value sequences from two sequences
func Async2[K, V any](input Seq2[K, V], bufSize int) Seq2[K, V] {
return FromSeqPair(Async(ToSeqPair(input), bufSize))
}

View File

@@ -0,0 +1,905 @@
// 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"
"sync/atomic"
"testing"
"time"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
// TestAsync_Success tests basic Async functionality
func TestAsync_Success(t *testing.T) {
t.Run("converts sequence to async with buffer", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 10)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("preserves element order", func(t *testing.T) {
seq := From("a", "b", "c", "d", "e")
async := Async(seq, 5)
result := toSlice(async)
assert.Equal(t, []string{"a", "b", "c", "d", "e"}, result)
})
t.Run("works with single element", func(t *testing.T) {
seq := From(42)
async := Async(seq, 1)
result := toSlice(async)
assert.Equal(t, []int{42}, result)
})
t.Run("works with large sequence", func(t *testing.T) {
data := make([]int, 100)
for i := range data {
data[i] = i
}
seq := From(data...)
async := Async(seq, 20)
result := toSlice(async)
assert.Equal(t, data, result)
})
}
// TestAsync_BufferSizes tests different buffer sizes
func TestAsync_BufferSizes(t *testing.T) {
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
seq := From(1, 2, 3)
async := Async(seq, 0)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("small buffer", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 2)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("large buffer", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 100)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("negative buffer size treated as 0", func(t *testing.T) {
seq := From(1, 2, 3)
async := Async(seq, -5)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("buffer size equals sequence length", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 5)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("buffer size larger than sequence", func(t *testing.T) {
seq := From(1, 2, 3)
async := Async(seq, 10)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3}, result)
})
}
// TestAsync_Empty tests Async with empty sequences
func TestAsync_Empty(t *testing.T) {
t.Run("empty integer sequence", func(t *testing.T) {
seq := Empty[int]()
async := Async(seq, 5)
result := toSlice(async)
assert.Empty(t, result)
})
t.Run("empty string sequence", func(t *testing.T) {
seq := Empty[string]()
async := Async(seq, 10)
result := toSlice(async)
assert.Empty(t, result)
})
t.Run("empty with zero buffer", func(t *testing.T) {
seq := Empty[int]()
async := Async(seq, 0)
result := toSlice(async)
assert.Empty(t, result)
})
}
// TestAsync_EarlyTermination tests that Async properly handles early termination
func TestAsync_EarlyTermination(t *testing.T) {
t.Run("stops producer when consumer breaks", func(t *testing.T) {
var producerCount atomic.Int32
// Create a sequence that tracks how many elements were produced
seq := func(yield func(int) bool) {
for i := range 100 {
producerCount.Add(1)
if !yield(i) {
return
}
}
}
async := Async(seq, 10)
// Consume only 5 elements
count := 0
for range async {
count++
if count >= 5 {
break
}
}
// Give goroutine time to clean up
time.Sleep(10 * time.Millisecond)
// Producer should have stopped shortly after consumer stopped
// It may produce a few extra due to buffering, but not all 100
produced := producerCount.Load()
assert.LessOrEqual(t, produced, int32(20), "producer should stop after consumer breaks")
assert.GreaterOrEqual(t, produced, int32(5), "producer should produce at least what was consumed")
})
t.Run("handles yield returning false", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
async := Async(seq, 5)
collected := []int{}
for v := range async {
collected = append(collected, v)
if v == 3 {
break
}
}
assert.Equal(t, []int{1, 2, 3}, collected)
})
t.Run("early termination with unbuffered channel", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 0)
collected := []int{}
for v := range async {
collected = append(collected, v)
if v == 2 {
break
}
}
assert.Equal(t, []int{1, 2}, collected)
})
}
// TestAsync_WithComplexTypes tests Async with complex data types
func TestAsync_WithComplexTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("works with structs", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
)
async := Async(seq, 5)
result := toSlice(async)
expected := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
assert.Equal(t, expected, result)
})
t.Run("works with pointers", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
p3 := &Person{"Charlie", 35}
seq := From(p1, p2, p3)
async := Async(seq, 3)
result := toSlice(async)
assert.Equal(t, []*Person{p1, p2, p3}, result)
})
t.Run("works with slices", func(t *testing.T) {
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6})
async := Async(seq, 2)
result := toSlice(async)
expected := [][]int{{1, 2}, {3, 4}, {5, 6}}
assert.Equal(t, expected, result)
})
t.Run("works with maps", func(t *testing.T) {
m1 := map[string]int{"a": 1}
m2 := map[string]int{"b": 2}
m3 := map[string]int{"c": 3}
seq := From(m1, m2, m3)
async := Async(seq, 3)
result := toSlice(async)
assert.Equal(t, []map[string]int{m1, m2, m3}, result)
})
}
// TestAsync_WithChainedOperations tests Async with other sequence operations
func TestAsync_WithChainedOperations(t *testing.T) {
t.Run("async after map", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
async := Async(mapped, 5)
result := toSlice(async)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
t.Run("map after async", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 5)
mapped := MonadMap(async, N.Mul(2))
result := toSlice(mapped)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
t.Run("async after filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
async := Async(filtered, 5)
result := toSlice(async)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
t.Run("filter after async", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
async := Async(seq, 5)
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
result := toSlice(filtered)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
t.Run("async after chain", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
async := Async(chained, 10)
result := toSlice(async)
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
})
t.Run("multiple async operations", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async1 := Async(seq, 3)
async2 := Async(async1, 2)
result := toSlice(async2)
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
}
// TestAsync_Concurrency tests concurrent behavior
func TestAsync_Concurrency(t *testing.T) {
t.Run("allows concurrent production and consumption", func(t *testing.T) {
// Create a slow producer
seq := func(yield func(int) bool) {
for i := range 5 {
time.Sleep(5 * time.Millisecond)
if !yield(i) {
return
}
}
}
async := Async(seq, 10)
result := toSlice(async)
// Verify all elements are produced correctly
assert.Equal(t, []int{0, 1, 2, 3, 4}, result)
})
t.Run("handles concurrent consumption safely", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
async := Async(seq, 5)
// Consume with some processing time
var sum atomic.Int32
for v := range async {
sum.Add(int32(v))
time.Sleep(1 * time.Millisecond)
}
assert.Equal(t, int32(55), sum.Load())
})
}
// TestAsync_EdgeCases tests edge cases
func TestAsync_EdgeCases(t *testing.T) {
t.Run("very large buffer size", func(t *testing.T) {
seq := From(1, 2, 3)
async := Async(seq, 1000000)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("buffer size of 1", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 1)
result := toSlice(async)
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("works with replicate", func(t *testing.T) {
seq := Replicate(5, 42)
async := Async(seq, 3)
result := toSlice(async)
assert.Equal(t, []int{42, 42, 42, 42, 42}, result)
})
t.Run("works with makeBy", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i * i })
async := Async(seq, 3)
result := toSlice(async)
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
})
}
// Benchmark tests
func BenchmarkAsync(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
async := Async(seq, 5)
for range async {
}
}
}
func BenchmarkAsync_LargeSequence(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
seq := From(data...)
b.ResetTimer()
for range b.N {
async := Async(seq, 100)
for range async {
}
}
}
func BenchmarkAsync_SmallBuffer(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
async := Async(seq, 1)
for range async {
}
}
}
func BenchmarkAsync_LargeBuffer(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
async := Async(seq, 100)
for range async {
}
}
}
func BenchmarkAsync_Unbuffered(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
async := Async(seq, 0)
for range async {
}
}
}
func BenchmarkAsync_WithMap(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
async := Async(seq, 5)
mapped := MonadMap(async, N.Mul(2))
for range mapped {
}
}
}
func BenchmarkAsync_WithFilter(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
async := Async(seq, 5)
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
for range filtered {
}
}
}
// Example tests for documentation
func ExampleAsync() {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 10)
for v := range async {
fmt.Printf("%d ", v)
}
// Output: 1 2 3 4 5
}
func ExampleAsync_unbuffered() {
seq := From(1, 2, 3)
async := Async(seq, 0)
for v := range async {
fmt.Printf("%d ", v)
}
// Output: 1 2 3
}
func ExampleAsync_earlyTermination() {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
async := Async(seq, 5)
count := 0
for v := range async {
fmt.Printf("%d ", v)
count++
if count >= 3 {
break
}
}
// Output: 1 2 3
}
func ExampleAsync_withMap() {
seq := From(1, 2, 3, 4, 5)
async := Async(seq, 5)
doubled := MonadMap(async, N.Mul(2))
for v := range doubled {
fmt.Printf("%d ", v)
}
// Output: 2 4 6 8 10
}
func ExampleAsync_withFilter() {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
async := Async(seq, 5)
evens := MonadFilter(async, func(x int) bool { return x%2 == 0 })
for v := range evens {
fmt.Printf("%d ", v)
}
// Output: 2 4 6 8 10
}
// TestAsync2_Success tests basic Async2 functionality
func TestAsync2_Success(t *testing.T) {
t.Run("converts Seq2 to async with buffer", func(t *testing.T) {
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
async := Async2(seq, 10)
result := toMap(async)
expected := map[int]string{1: "a", 2: "b", 3: "c"}
assert.Equal(t, expected, result)
})
t.Run("preserves key-value pairs order", func(t *testing.T) {
seq := MonadZip(From("x", "y", "z"), From(10, 20, 30))
async := Async2(seq, 5)
keys := []string{}
values := []int{}
for k, v := range async {
keys = append(keys, k)
values = append(values, v)
}
assert.Equal(t, []string{"x", "y", "z"}, keys)
assert.Equal(t, []int{10, 20, 30}, values)
})
t.Run("works with single pair", func(t *testing.T) {
seq := Of2("key", 42)
async := Async2(seq, 1)
result := toMap(async)
assert.Equal(t, map[string]int{"key": 42}, result)
})
t.Run("works with large Seq2", func(t *testing.T) {
keys := make([]int, 100)
values := make([]string, 100)
for i := range keys {
keys[i] = i
values[i] = fmt.Sprintf("val%d", i)
}
seq := MonadZip(From(keys...), From(values...))
async := Async2(seq, 20)
result := toMap(async)
assert.Equal(t, 100, len(result))
for i := range 100 {
assert.Equal(t, fmt.Sprintf("val%d", i), result[i])
}
})
}
// TestAsync2_BufferSizes tests different buffer sizes
func TestAsync2_BufferSizes(t *testing.T) {
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
async := Async2(seq, 0)
result := toMap(async)
expected := map[int]string{1: "a", 2: "b", 3: "c"}
assert.Equal(t, expected, result)
})
t.Run("negative buffer size treated as 0", func(t *testing.T) {
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
async := Async2(seq, -5)
result := toMap(async)
expected := map[int]string{1: "a", 2: "b", 3: "c"}
assert.Equal(t, expected, result)
})
t.Run("large buffer", func(t *testing.T) {
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
async := Async2(seq, 100)
result := toMap(async)
expected := map[int]string{1: "a", 2: "b", 3: "c"}
assert.Equal(t, expected, result)
})
}
// TestAsync2_Empty tests Async2 with empty sequences
func TestAsync2_Empty(t *testing.T) {
t.Run("empty Seq2", func(t *testing.T) {
seq := MonadZip(Empty[int](), Empty[string]())
async := Async2(seq, 5)
result := toMap(async)
assert.Empty(t, result)
})
}
// TestAsync2_EarlyTermination tests that Async2 properly handles early termination
func TestAsync2_EarlyTermination(t *testing.T) {
t.Run("stops producer when consumer breaks", func(t *testing.T) {
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
async := Async2(seq, 5)
count := 0
for range async {
count++
if count >= 3 {
break
}
}
assert.Equal(t, 3, count)
})
}
// TestAsync2_WithChainedOperations tests Async2 with other operations
func TestAsync2_WithChainedOperations(t *testing.T) {
t.Run("async2 after map", func(t *testing.T) {
seq := MonadZip(From(1, 2, 3), From(10, 20, 30))
mapped := MonadMapWithKey(seq, func(k, v int) int { return k + v })
async := Async2(mapped, 5)
result := toMap(async)
expected := map[int]int{1: 11, 2: 22, 3: 33}
assert.Equal(t, expected, result)
})
}
// TestToSeqPair_Success tests basic ToSeqPair functionality
func TestToSeqPair_Success(t *testing.T) {
t.Run("converts Seq2 to Seq of Pairs", func(t *testing.T) {
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
assert.Equal(t, 3, len(result))
assert.Equal(t, 1, pair.Head(result[0]))
assert.Equal(t, "a", pair.Tail(result[0]))
assert.Equal(t, 2, pair.Head(result[1]))
assert.Equal(t, "b", pair.Tail(result[1]))
assert.Equal(t, 3, pair.Head(result[2]))
assert.Equal(t, "c", pair.Tail(result[2]))
})
t.Run("preserves order", func(t *testing.T) {
seq2 := MonadZip(From("x", "y", "z"), From(10, 20, 30))
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
assert.Equal(t, 3, len(result))
for i, p := range result {
expectedKey := string(rune('x' + i))
expectedVal := (i + 1) * 10
assert.Equal(t, expectedKey, pair.Head(p))
assert.Equal(t, expectedVal, pair.Tail(p))
}
})
t.Run("works with single pair", func(t *testing.T) {
seq2 := Of2("key", 42)
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
assert.Equal(t, 1, len(result))
assert.Equal(t, "key", pair.Head(result[0]))
assert.Equal(t, 42, pair.Tail(result[0]))
})
}
// TestToSeqPair_Empty tests ToSeqPair with empty sequences
func TestToSeqPair_Empty(t *testing.T) {
t.Run("empty Seq2 produces empty Seq", func(t *testing.T) {
seq2 := MonadZip(Empty[int](), Empty[string]())
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
assert.Empty(t, result)
})
}
// TestToSeqPair_WithComplexTypes tests ToSeqPair with complex types
func TestToSeqPair_WithComplexTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("works with struct values", func(t *testing.T) {
seq2 := MonadZip(
From(1, 2, 3),
From(Person{"Alice", 30}, Person{"Bob", 25}, Person{"Charlie", 35}),
)
pairs := ToSeqPair(seq2)
result := toSlice(pairs)
assert.Equal(t, 3, len(result))
assert.Equal(t, 1, pair.Head(result[0]))
assert.Equal(t, Person{"Alice", 30}, pair.Tail(result[0]))
})
}
// TestFromSeqPair_Success tests basic FromSeqPair functionality
func TestFromSeqPair_Success(t *testing.T) {
t.Run("converts Seq of Pairs to Seq2", func(t *testing.T) {
pairs := From(
pair.MakePair(1, "a"),
pair.MakePair(2, "b"),
pair.MakePair(3, "c"),
)
seq2 := FromSeqPair(pairs)
result := toMap(seq2)
expected := map[int]string{1: "a", 2: "b", 3: "c"}
assert.Equal(t, expected, result)
})
t.Run("preserves order", func(t *testing.T) {
pairs := From(
pair.MakePair("x", 10),
pair.MakePair("y", 20),
pair.MakePair("z", 30),
)
seq2 := FromSeqPair(pairs)
keys := []string{}
values := []int{}
for k, v := range seq2 {
keys = append(keys, k)
values = append(values, v)
}
assert.Equal(t, []string{"x", "y", "z"}, keys)
assert.Equal(t, []int{10, 20, 30}, values)
})
t.Run("works with single pair", func(t *testing.T) {
pairs := From(pair.MakePair("key", 42))
seq2 := FromSeqPair(pairs)
result := toMap(seq2)
assert.Equal(t, map[string]int{"key": 42}, result)
})
}
// TestFromSeqPair_Empty tests FromSeqPair with empty sequences
func TestFromSeqPair_Empty(t *testing.T) {
t.Run("empty Seq produces empty Seq2", func(t *testing.T) {
pairs := Empty[Pair[int, string]]()
seq2 := FromSeqPair(pairs)
result := toMap(seq2)
assert.Empty(t, result)
})
}
// TestFromSeqPair_WithComplexTypes tests FromSeqPair with complex types
func TestFromSeqPair_WithComplexTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("works with struct values", func(t *testing.T) {
pairs := From(
pair.MakePair(1, Person{"Alice", 30}),
pair.MakePair(2, Person{"Bob", 25}),
pair.MakePair(3, Person{"Charlie", 35}),
)
seq2 := FromSeqPair(pairs)
result := toMap(seq2)
expected := map[int]Person{
1: {"Alice", 30},
2: {"Bob", 25},
3: {"Charlie", 35},
}
assert.Equal(t, expected, result)
})
}
// TestRoundTrip tests that ToSeqPair and FromSeqPair are inverses
func TestRoundTrip(t *testing.T) {
t.Run("ToSeqPair then FromSeqPair", func(t *testing.T) {
original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
pairs := ToSeqPair(original)
restored := FromSeqPair(pairs)
result := toMap(restored)
expected := map[int]string{1: "a", 2: "b", 3: "c"}
assert.Equal(t, expected, result)
})
t.Run("FromSeqPair then ToSeqPair", func(t *testing.T) {
original := From(
pair.MakePair(1, "a"),
pair.MakePair(2, "b"),
pair.MakePair(3, "c"),
)
seq2 := FromSeqPair(original)
restored := ToSeqPair(seq2)
result := toSlice(restored)
assert.Equal(t, 3, len(result))
assert.Equal(t, 1, pair.Head(result[0]))
assert.Equal(t, "a", pair.Tail(result[0]))
})
}
// Benchmark tests for Async2
func BenchmarkAsync2(b *testing.B) {
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
b.ResetTimer()
for range b.N {
async := Async2(seq, 5)
for range async {
}
}
}
func BenchmarkAsync2_LargeSequence(b *testing.B) {
keys := make([]int, 1000)
values := make([]string, 1000)
for i := range keys {
keys[i] = i
values[i] = fmt.Sprintf("val%d", i)
}
seq := MonadZip(From(keys...), From(values...))
b.ResetTimer()
for range b.N {
async := Async2(seq, 100)
for range async {
}
}
}
// Benchmark tests for FromSeqPair
func BenchmarkFromSeqPair(b *testing.B) {
pairs := From(
pair.MakePair(1, "a"),
pair.MakePair(2, "b"),
pair.MakePair(3, "c"),
pair.MakePair(4, "d"),
pair.MakePair(5, "e"),
)
b.ResetTimer()
for range b.N {
seq2 := FromSeqPair(pairs)
for range seq2 {
}
}
}
func BenchmarkRoundTrip(b *testing.B) {
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
b.ResetTimer()
for range b.N {
pairs := ToSeqPair(seq)
restored := FromSeqPair(pairs)
for range restored {
}
}
}
// Example tests for Async2
func ExampleAsync2() {
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
async := Async2(seq, 10)
for k, v := range async {
fmt.Printf("%d: %s\n", k, v)
}
// Output:
// 1: a
// 2: b
// 3: c
}
func ExampleAsync2_earlyTermination() {
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
async := Async2(seq, 5)
count := 0
for k, v := range async {
fmt.Printf("%d: %s\n", k, v)
count++
if count >= 2 {
break
}
}
// Output:
// 1: a
// 2: b
}
// Example tests for FromSeqPair
func ExampleFromSeqPair() {
pairs := From(
pair.MakePair(1, "a"),
pair.MakePair(2, "b"),
pair.MakePair(3, "c"),
)
seq2 := FromSeqPair(pairs)
for k, v := range seq2 {
fmt.Printf("%d: %s\n", k, v)
}
// Output:
// 1: a
// 2: b
// 3: c
}

View File

@@ -34,6 +34,13 @@ import (
// 3. Filtering to keep only pairs where the boolean (tail) is true
// 4. Extracting the original values (head) from the filtered pairs
//
// Marble Diagram:
//
// Data: --1--2--3--4--5-->
// Selectors: --T--F--T--F--T-->
// Compress
// Output: --1-----3-----5-->
//
// RxJS Equivalent: Similar to combining [zip] with [filter] - https://rxjs.dev/api/operators/zip
//
// Type Parameters:

View File

@@ -21,6 +21,12 @@ package iter
// all elements repeatedly. When the end of the input sequence is reached, it starts over
// from the beginning, continuing this pattern forever.
//
// Marble Diagram:
//
// Input: --1--2--3|
// Cycle
// Output: --1--2--3--1--2--3--1--2--3--> (infinite)
//
// RxJS Equivalent: [repeat] - https://rxjs.dev/api/operators/repeat
//
// WARNING: This creates an INFINITE sequence for non-empty inputs. It must be used with

View File

@@ -23,6 +23,16 @@ import "github.com/IBM/fp-go/v2/option"
// contains at least one element, it returns Some(element). If the iterator is empty,
// it returns None. The function consumes only the first element of the iterator.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// First
// Output: --Some(1)|
//
// Input: --|
// First
// Output: --None|
//
// RxJS Equivalent: [first] - https://rxjs.dev/api/operators/first
//
// Type Parameters:

View File

@@ -82,6 +82,12 @@ func Of2[K, A any](k K, a A) Seq2[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.
//
// Marble Diagram:
//
// Input: --1--2--3-->
// Map(x => x * 2)
// Output: --2--4--6-->
//
// RxJS Equivalent: [map] - https://rxjs.dev/api/operators/map
//
// Example:
@@ -186,6 +192,12 @@ func MapWithKey[K, A, B any](f func(K, A) B) Operator2[K, A, B] {
// MonadFilter returns a sequence containing only elements that satisfy the predicate.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// Filter(x => x % 2 == 0)
// Output: -----2-----4----->
//
// RxJS Equivalent: [filter] - https://rxjs.dev/api/operators/filter
//
// Example:
@@ -293,6 +305,12 @@ func FilterWithKey[K, A any](pred func(K, A) bool) Operator2[K, A, A] {
// MonadFilterMap applies a function that returns an Option to each element,
// keeping only the Some values and unwrapping them.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// FilterMap(x => x % 2 == 0 ? Some(x * 10) : None)
// Output: -----20----40---->
//
// Example:
//
// seq := From(1, 2, 3, 4, 5)
@@ -430,6 +448,12 @@ func FilterMapWithKey[K, A, B any](f func(K, A) Option[B]) Operator2[K, A, B] {
// MonadChain applies a function that returns a sequence to each element and flattens the results.
// This is the monadic bind operation (flatMap).
//
// Marble Diagram:
//
// Input: --1-----2-----3---->
// Chain(x => [x, x*10])
// Output: --1-10--2-20--3-30->
//
// RxJS Equivalent: [mergeMap/flatMap] - https://rxjs.dev/api/operators/mergeMap
//
// Example:
@@ -473,6 +497,12 @@ func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
// Flatten flattens a sequence of sequences into a single sequence.
//
// Marble Diagram:
//
// Input: --[1,2]--[3,4]--[5]-->
// Flatten
// Output: --1-2----3-4----5---->
//
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
//
// Example:
@@ -489,6 +519,14 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
// MonadAp applies a sequence of functions to a sequence of values.
// This is the applicative apply operation.
//
// Marble Diagram:
//
// Functions: --(*2)---(+10)-->
// Values: --5------3------>
// Ap
// Output: --10-6---15-13-->
// (each function applied to each value)
//
// Example:
//
// fns := From(N.Mul(2), N.Add(10))
@@ -577,6 +615,13 @@ func Replicate[A any](n int, a A) Seq[A] {
// MonadReduce reduces a sequence to a single value by applying a function to each element
// and an accumulator, starting with an initial value.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--|
// Reduce((acc, x) => acc + x, 0)
// Output: ------------------15|
// (emits final result only)
//
// RxJS Equivalent: [reduce] - https://rxjs.dev/api/operators/reduce
//
// Example:
@@ -811,6 +856,13 @@ func FoldMapWithKey[K, A, B any](m M.Monoid[B]) func(func(K, A) B) func(Seq2[K,
// MonadFlap applies a fixed value to a sequence of functions.
// This is the dual of MonadAp.
//
// Marble Diagram:
//
// Functions: --(*2)---(+10)-->
// Value: 5 (fixed)
// Flap
// Output: --10-----15----->
//
// Example:
//
// fns := From(N.Mul(2), N.Add(10))
@@ -832,6 +884,12 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
// Prepend returns a function that adds an element to the beginning of a sequence.
//
// Marble Diagram:
//
// Input: -----2--3--4-->
// Prepend(1)
// Output: --1--2--3--4-->
//
// RxJS Equivalent: [startWith] - https://rxjs.dev/api/operators/startWith
//
// Example:
@@ -847,6 +905,12 @@ func Prepend[A any](head A) Operator[A, A] {
// Append returns a function that adds an element to the end of a sequence.
//
// Marble Diagram:
//
// Input: --1--2--3-----|
// Append(4)
// Output: --1--2--3--4--|
//
// RxJS Equivalent: [endWith] - https://rxjs.dev/api/operators/endWith
//
// Example:
@@ -863,6 +927,14 @@ func Append[A any](tail A) Operator[A, A] {
// MonadZip combines two sequences into a sequence of pairs.
// The resulting sequence stops when either input sequence is exhausted.
//
// Marble Diagram:
//
// SeqA: --1--2--3---->
// SeqB: --a--b------->
// Zip
// Output: --(1,a)-(2,b)|
// (stops when shorter sequence ends)
//
// RxJS Equivalent: [zip] - https://rxjs.dev/api/operators/zip
//
// Example:
@@ -1002,3 +1074,138 @@ func ToSeqPair[A, B any](as Seq2[A, B]) Seq[Pair[A, B]] {
}
}
}
// FromSeqPair converts a sequence of Pairs into a key-value sequence.
//
// This function transforms a Seq[Pair[A, B]] (which yields Pair objects when iterated)
// into a Seq2[A, B] (which yields key-value pairs as separate arguments). This is the
// inverse operation of ToSeqPair and is useful when you need to convert from working
// with pairs as first-class values back to the key-value iteration pattern.
//
// # Type Parameters
//
// - A: The type of the first element (key) in each pair
// - B: The type of the second element (value) in each pair
//
// # Parameters
//
// - as: A Seq that yields Pair objects
//
// # Returns
//
// - Seq2[A, B]: A key-value sequence that yields the unpacked pairs
//
// # Example Usage
//
// // Create a sequence of pairs
// pairs := From(
// pair.MakePair("a", 1),
// pair.MakePair("b", 2),
// pair.MakePair("c", 3),
// )
// seq2 := FromSeqPair(pairs)
//
// // Iterate as key-value pairs
// for k, v := range seq2 {
// fmt.Printf("%s: %d\n", k, v)
// }
// // Output:
// // a: 1
// // b: 2
// // c: 3
//
// # Example with Map
//
// pairs := From(
// pair.MakePair(1, 10),
// pair.MakePair(2, 20),
// pair.MakePair(3, 30),
// )
// seq2 := FromSeqPair(pairs)
//
// // Use with Seq2 operations
// mapped := MonadMapWithKey(seq2, func(k, v int) int {
// return k + v
// })
// // yields: 11, 22, 33
//
// # Example - Round-trip conversion
//
// original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
// pairs := ToSeqPair(original)
// restored := FromSeqPair(pairs)
// // restored is equivalent to original
//
// # See Also
//
// - ToSeqPair: Converts Seq2 to Seq of Pairs (inverse operation)
// - MonadZip: Creates key-value sequences from two sequences
// - pair.MakePair: Creates a Pair from two values
// - pair.Unpack: Unpacks a Pair into two values
func FromSeqPair[A, B any](as Seq[Pair[A, B]]) Seq2[A, B] {
return func(yield func(A, B) bool) {
for p := range as {
if !yield(pair.Unpack(p)) {
return
}
}
}
}
// Skip returns an operator that skips the first n elements of a sequence.
//
// This function creates a transformation that discards the first n elements from
// the source sequence and yields all remaining elements. If n is less than or equal
// to 0, all elements are yielded. If n is greater than or equal to the sequence length,
// an empty sequence is returned.
//
// The operation is lazy and only consumes elements from the source sequence as needed.
// The first n elements are consumed and discarded, then subsequent elements are yielded.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--6--7--8-->
// Skip(3)
// Output: -----------4--5--6--7--8-->
//
// RxJS Equivalent: [skip] - https://rxjs.dev/api/operators/skip
//
// Type Parameters:
// - U: The type of elements in the sequence
//
// Parameters:
// - count: The number of elements to skip from the beginning of the sequence
//
// Returns:
// - An Operator that transforms a Seq[U] by skipping the first count elements
//
// Example - Skip first 3 elements:
//
// seq := From(1, 2, 3, 4, 5)
// result := Skip[int](3)(seq)
// // yields: 4, 5
//
// Example - Skip more than available:
//
// seq := From(1, 2)
// result := Skip[int](5)(seq)
// // yields: nothing (empty sequence)
//
// Example - Skip zero or negative:
//
// seq := From(1, 2, 3)
// result := Skip[int](0)(seq)
// // yields: 1, 2, 3 (all elements)
//
// Example - Chaining with other operations:
//
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// result := F.Pipe2(
// seq,
// Skip[int](3),
// MonadFilter(seq, func(x int) bool { return x%2 == 0 }),
// )
// // yields: 4, 6, 8, 10 (skip first 3, then filter evens)
func Skip[U any](count int) Operator[U, U] {
return FilterWithIndex(func(idx int, _ U) bool { return idx >= count })
}

View File

@@ -612,3 +612,440 @@ func TestMapToArrayIdentity(t *testing.T) {
result := mapper(seq)
assert.Equal(t, []string{"a", "b", "c"}, result)
}
// TestSkip tests basic Skip functionality
func TestSkip(t *testing.T) {
t.Run("skips first n elements from sequence", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(Skip[int](3)(seq))
assert.Equal(t, []int{4, 5}, result)
})
t.Run("skips first element", func(t *testing.T) {
seq := From(10, 20, 30)
result := toSlice(Skip[int](1)(seq))
assert.Equal(t, []int{20, 30}, result)
})
t.Run("skips all elements when n equals length", func(t *testing.T) {
seq := From(1, 2, 3)
result := toSlice(Skip[int](3)(seq))
assert.Empty(t, result)
})
t.Run("skips all elements when n exceeds length", func(t *testing.T) {
seq := From(1, 2, 3)
result := toSlice(Skip[int](10)(seq))
assert.Empty(t, result)
})
t.Run("skips from string sequence", func(t *testing.T) {
seq := From("a", "b", "c", "d", "e")
result := toSlice(Skip[string](2)(seq))
assert.Equal(t, []string{"c", "d", "e"}, result)
})
t.Run("skips from single element sequence", func(t *testing.T) {
seq := From(42)
result := toSlice(Skip[int](1)(seq))
assert.Empty(t, result)
})
t.Run("skips from large sequence", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result := toSlice(Skip[int](7)(seq))
assert.Equal(t, []int{8, 9, 10}, result)
})
}
// TestSkipZeroOrNegative tests Skip with zero or negative values
func TestSkipZeroOrNegative(t *testing.T) {
t.Run("returns all elements when n is zero", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(Skip[int](0)(seq))
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("returns all elements when n is negative", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(Skip[int](-1)(seq))
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("returns all elements when n is large negative", func(t *testing.T) {
seq := From("a", "b", "c")
result := toSlice(Skip[string](-100)(seq))
assert.Equal(t, []string{"a", "b", "c"}, result)
})
}
// TestSkipEmpty tests Skip with empty sequences
func TestSkipEmpty(t *testing.T) {
t.Run("returns empty from empty integer sequence", func(t *testing.T) {
seq := Empty[int]()
result := toSlice(Skip[int](5)(seq))
assert.Empty(t, result)
})
t.Run("returns empty from empty string sequence", func(t *testing.T) {
seq := Empty[string]()
result := toSlice(Skip[string](3)(seq))
assert.Empty(t, result)
})
t.Run("returns empty when skipping zero from empty", func(t *testing.T) {
seq := Empty[int]()
result := toSlice(Skip[int](0)(seq))
assert.Empty(t, result)
})
}
// TestSkipWithComplexTypes tests Skip with complex data types
func TestSkipWithComplexTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("skips structs", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
Person{"David", 28},
)
result := toSlice(Skip[Person](2)(seq))
expected := []Person{
{"Charlie", 35},
{"David", 28},
}
assert.Equal(t, expected, result)
})
t.Run("skips pointers", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
p3 := &Person{"Charlie", 35}
seq := From(p1, p2, p3)
result := toSlice(Skip[*Person](1)(seq))
assert.Equal(t, []*Person{p2, p3}, result)
})
t.Run("skips slices", func(t *testing.T) {
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6}, []int{7, 8})
result := toSlice(Skip[[]int](2)(seq))
expected := [][]int{{5, 6}, {7, 8}}
assert.Equal(t, expected, result)
})
}
// TestSkipWithChainedOperations tests Skip with other sequence operations
func TestSkipWithChainedOperations(t *testing.T) {
t.Run("skip after map", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
result := toSlice(Skip[int](2)(mapped))
assert.Equal(t, []int{6, 8, 10}, result)
})
t.Run("skip after filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
result := toSlice(Skip[int](2)(filtered))
assert.Equal(t, []int{6, 8, 10}, result)
})
t.Run("map after skip", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
skipped := Skip[int](2)(seq)
result := toSlice(MonadMap(skipped, N.Mul(10)))
assert.Equal(t, []int{30, 40, 50}, result)
})
t.Run("filter after skip", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
skipped := Skip[int](2)(seq)
result := toSlice(MonadFilter(skipped, func(x int) bool { return x%2 == 0 }))
assert.Equal(t, []int{4, 6, 8}, result)
})
t.Run("skip after chain", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
result := toSlice(Skip[int](3)(chained))
assert.Equal(t, []int{20, 3, 30}, result)
})
t.Run("multiple skips", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
skipped1 := Skip[int](2)(seq)
skipped2 := Skip[int](3)(skipped1)
result := toSlice(skipped2)
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
})
t.Run("skip and take", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
skipped := Skip[int](3)(seq)
taken := Take[int](3)(skipped)
result := toSlice(taken)
assert.Equal(t, []int{4, 5, 6}, result)
})
t.Run("take and skip", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
taken := Take[int](7)(seq)
skipped := Skip[int](2)(taken)
result := toSlice(skipped)
assert.Equal(t, []int{3, 4, 5, 6, 7}, result)
})
}
// TestSkipWithReplicate tests Skip with Replicate
func TestSkipWithReplicate(t *testing.T) {
t.Run("skips from replicated sequence", func(t *testing.T) {
seq := Replicate(10, 42)
result := toSlice(Skip[int](7)(seq))
assert.Equal(t, []int{42, 42, 42}, result)
})
t.Run("skips all from short replicate", func(t *testing.T) {
seq := Replicate(2, "hello")
result := toSlice(Skip[string](5)(seq))
assert.Empty(t, result)
})
t.Run("skips zero from replicate", func(t *testing.T) {
seq := Replicate(3, 100)
result := toSlice(Skip[int](0)(seq))
assert.Equal(t, []int{100, 100, 100}, result)
})
}
// TestSkipWithMakeBy tests Skip with MakeBy
func TestSkipWithMakeBy(t *testing.T) {
t.Run("skips from generated sequence", func(t *testing.T) {
seq := MakeBy(10, func(i int) int { return i * i })
result := toSlice(Skip[int](5)(seq))
assert.Equal(t, []int{25, 36, 49, 64, 81}, result)
})
t.Run("skips more than generated", func(t *testing.T) {
seq := MakeBy(3, func(i int) int { return i + 1 })
result := toSlice(Skip[int](10)(seq))
assert.Empty(t, result)
})
}
// TestSkipWithPrependAppend tests Skip with Prepend and Append
func TestSkipWithPrependAppend(t *testing.T) {
t.Run("skip from prepended sequence", func(t *testing.T) {
seq := From(2, 3, 4, 5)
prepended := Prepend(1)(seq)
result := toSlice(Skip[int](2)(prepended))
assert.Equal(t, []int{3, 4, 5}, result)
})
t.Run("skip from appended sequence", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
result := toSlice(Skip[int](2)(appended))
assert.Equal(t, []int{3, 4}, result)
})
t.Run("skip includes appended element", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
result := toSlice(Skip[int](3)(appended))
assert.Equal(t, []int{4}, result)
})
}
// TestSkipWithFlatten tests Skip with Flatten
func TestSkipWithFlatten(t *testing.T) {
t.Run("skips from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5, 6))
flattened := Flatten(nested)
result := toSlice(Skip[int](3)(flattened))
assert.Equal(t, []int{4, 5, 6}, result)
})
t.Run("skips from flattened with empty inner sequences", func(t *testing.T) {
nested := From(From(1, 2), Empty[int](), From(3, 4))
flattened := Flatten(nested)
result := toSlice(Skip[int](2)(flattened))
assert.Equal(t, []int{3, 4}, result)
})
}
// TestSkipDoesNotConsumeSkippedElements tests that Skip is efficient
func TestSkipDoesNotConsumeSkippedElements(t *testing.T) {
t.Run("processes all elements including skipped", func(t *testing.T) {
callCount := 0
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
callCount++
return x * 2
})
skipped := Skip[int](7)(seq)
result := []int{}
for v := range skipped {
result = append(result, v)
}
assert.Equal(t, []int{16, 18, 20}, result)
// Skip still needs to iterate through skipped elements to count them
assert.Equal(t, 10, callCount, "should process all elements")
})
}
// TestSkipEdgeCases tests edge cases
func TestSkipEdgeCases(t *testing.T) {
t.Run("skip 0 from single element", func(t *testing.T) {
seq := From(42)
result := toSlice(Skip[int](0)(seq))
assert.Equal(t, []int{42}, result)
})
t.Run("skip 1 from single element", func(t *testing.T) {
seq := From(42)
result := toSlice(Skip[int](1)(seq))
assert.Empty(t, result)
})
t.Run("skip large number from small sequence", func(t *testing.T) {
seq := From(1, 2)
result := toSlice(Skip[int](1000000)(seq))
assert.Empty(t, result)
})
t.Run("skip with very large n", func(t *testing.T) {
seq := From(1, 2, 3)
result := toSlice(Skip[int](int(^uint(0) >> 1))(seq)) // max int
assert.Empty(t, result)
})
t.Run("skip all but one", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(Skip[int](4)(seq))
assert.Equal(t, []int{5}, result)
})
}
// Benchmark tests for Skip
func BenchmarkSkip(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
skipped := Skip[int](5)(seq)
for range skipped {
}
}
}
func BenchmarkSkipLargeSequence(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
seq := From(data...)
b.ResetTimer()
for range b.N {
skipped := Skip[int](900)(seq)
for range skipped {
}
}
}
func BenchmarkSkipWithMap(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
mapped := MonadMap(seq, N.Mul(2))
skipped := Skip[int](5)(mapped)
for range skipped {
}
}
}
func BenchmarkSkipWithFilter(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
skipped := Skip[int](2)(filtered)
for range skipped {
}
}
}
// Example tests for documentation
func ExampleSkip() {
seq := From(1, 2, 3, 4, 5)
skipped := Skip[int](3)(seq)
for v := range skipped {
fmt.Printf("%d ", v)
}
// Output: 4 5
}
func ExampleSkip_moreThanAvailable() {
seq := From(1, 2, 3)
skipped := Skip[int](10)(seq)
count := 0
for range skipped {
count++
}
fmt.Printf("Count: %d\n", count)
// Output: Count: 0
}
func ExampleSkip_zero() {
seq := From(1, 2, 3, 4, 5)
skipped := Skip[int](0)(seq)
for v := range skipped {
fmt.Printf("%d ", v)
}
// Output: 1 2 3 4 5
}
func ExampleSkip_withFilter() {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
skipped := Skip[int](2)(evens)
for v := range skipped {
fmt.Printf("%d ", v)
}
// Output: 6 8 10
}
func ExampleSkip_withMap() {
seq := From(1, 2, 3, 4, 5)
doubled := MonadMap(seq, N.Mul(2))
skipped := Skip[int](2)(doubled)
for v := range skipped {
fmt.Printf("%d ", v)
}
// Output: 6 8 10
}
func ExampleSkip_chained() {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result := F.Pipe3(
seq,
Skip[int](3),
Filter(func(x int) bool { return x%2 == 0 }),
toSlice[int],
)
fmt.Println(result)
// Output: [4 6 8 10]
}

View File

@@ -10,6 +10,16 @@ import (
// sequence. If the iterator contains at least one element, it returns Some(element).
// If the iterator is empty, it returns None.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--|
// Last
// Output: -----------------Some(5)|
//
// Input: --|
// Last
// Output: --None|
//
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
//
// Type Parameters:

View File

@@ -28,6 +28,13 @@ import (
//
// This is the monadic form that takes the sequence as the first parameter.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// ChainOptionK(x => x % 2 == 0 ? Some(x * 10) : None)
// Output: -----20----40---->
// (filters and transforms)
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:
@@ -72,6 +79,13 @@ func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
// This is the curried version of [MonadChainOptionK], useful for function composition
// and creating reusable transformations.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// ChainOptionK(x => x > 2 ? Some(x) : None)
// Output: --------3--4--5-->
// (filters out values <= 2)
//
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
//
// Type parameters:

View File

@@ -24,6 +24,13 @@ package iter
//
// The operation is lazy - intermediate values are computed only as they are consumed.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// Scan((acc, x) => acc + x, 0)
// Output: --1--3--6--10-15->
// (running sum)
//
// RxJS Equivalent: [scan] - https://rxjs.dev/api/operators/scan
//
// Scan is useful for:

View File

@@ -27,6 +27,12 @@ import F "github.com/IBM/fp-go/v2/function"
// Once n elements have been yielded, iteration stops immediately without consuming
// the remaining elements from the source.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--6--7--8-->
// Take(3)
// Output: --1--2--3|
//
// RxJS Equivalent: [take] - https://rxjs.dev/api/operators/take
//
// Type Parameters:
@@ -78,3 +84,158 @@ func Take[U any](n int) Operator[U, U] {
}
}
}
// TakeWhile returns an operator that emits elements from a sequence while a predicate is satisfied.
//
// This function creates a transformation that yields elements from the source sequence
// as long as each element satisfies the provided predicate. Once an element fails the
// predicate test, the sequence terminates immediately, and no further elements are
// emitted, even if subsequent elements would satisfy the predicate.
//
// The operation is lazy and only consumes elements from the source sequence as needed.
// Once the predicate returns false, iteration stops immediately without consuming
// the remaining elements from the source.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--2--1-->
// TakeWhile(x < 4)
// Output: --1--2--3|
// (stops at 4)
//
// RxJS Equivalent: [takeWhile] - https://rxjs.dev/api/operators/takeWhile
//
// Type Parameters:
// - U: The type of elements in the sequence
//
// Parameters:
// - p: A predicate function that tests each element. Returns true to continue, false to stop
//
// Returns:
// - An Operator that transforms a Seq[U] by taking elements while the predicate is satisfied
//
// Example - Take while less than threshold:
//
// seq := From(1, 2, 3, 4, 5, 2, 1)
// result := TakeWhile(func(x int) bool { return x < 4 })(seq)
// // yields: 1, 2, 3 (stops at 4, doesn't continue to 2, 1)
//
// Example - Take while condition is met:
//
// seq := From("a", "b", "c", "1", "d", "e")
// isLetter := func(s string) bool { return s >= "a" && s <= "z" }
// result := TakeWhile(isLetter)(seq)
// // yields: "a", "b", "c" (stops at "1")
//
// Example - Take all when predicate always true:
//
// seq := From(2, 4, 6, 8)
// result := TakeWhile(func(x int) bool { return x%2 == 0 })(seq)
// // yields: 2, 4, 6, 8 (all elements satisfy predicate)
//
// Example - Take none when first element fails:
//
// seq := From(5, 1, 2, 3)
// result := TakeWhile(func(x int) bool { return x < 5 })(seq)
// // yields: nothing (first element fails predicate)
//
// Example - Chaining with other operations:
//
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// result := F.Pipe2(
// seq,
// MonadMap(seq, func(x int) int { return x * 2 }),
// TakeWhile(func(x int) bool { return x < 10 }),
// )
// // yields: 2, 4, 6, 8 (stops when doubled value reaches 10)
func TakeWhile[U any](p Predicate[U]) Operator[U, U] {
return func(s Seq[U]) Seq[U] {
return func(yield func(U) bool) {
for u := range s {
if !p(u) || !yield(u) {
return
}
}
}
}
}
// SkipWhile returns an operator that skips elements from a sequence while a predicate is satisfied.
//
// This function creates a transformation that discards elements from the source sequence
// as long as each element satisfies the provided predicate. Once an element fails the
// predicate test, that element and all subsequent elements are yielded, regardless of
// whether they satisfy the predicate.
//
// The operation is lazy and only consumes elements from the source sequence as needed.
// Once the predicate returns false, all remaining elements are yielded without further
// predicate evaluation.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--2--1-->
// SkipWhile(x < 4)
// Output: -----------4--5--2--1-->
// (starts at 4, continues with all)
//
// RxJS Equivalent: [skipWhile] - https://rxjs.dev/api/operators/skipWhile
//
// Type Parameters:
// - U: The type of elements in the sequence
//
// Parameters:
// - p: A predicate function that tests each element. Returns true to skip, false to start yielding
//
// Returns:
// - An Operator that transforms a Seq[U] by skipping elements while the predicate is satisfied
//
// Example - Skip while less than threshold:
//
// seq := From(1, 2, 3, 4, 5, 2, 1)
// result := SkipWhile(func(x int) bool { return x < 4 })(seq)
// // yields: 4, 5, 2, 1 (starts at 4, continues with all remaining)
//
// Example - Skip while condition is met:
//
// seq := From("a", "b", "c", "1", "d", "e")
// isLetter := func(s string) bool { return s >= "a" && s <= "z" }
// result := SkipWhile(isLetter)(seq)
// // yields: "1", "d", "e" (starts at "1", continues with all remaining)
//
// Example - Skip none when first element fails:
//
// seq := From(5, 1, 2, 3)
// result := SkipWhile(func(x int) bool { return x < 5 })(seq)
// // yields: 5, 1, 2, 3 (first element fails predicate, all yielded)
//
// Example - Skip all when predicate always true:
//
// seq := From(2, 4, 6, 8)
// result := SkipWhile(func(x int) bool { return x%2 == 0 })(seq)
// // yields: nothing (all elements satisfy predicate)
//
// Example - Chaining with other operations:
//
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// result := F.Pipe2(
// seq,
// SkipWhile(func(x int) bool { return x < 5 }),
// MonadMap(seq, func(x int) int { return x * 2 }),
// )
// // yields: 10, 12, 14, 16, 18, 20 (skip until 5, then double remaining)
func SkipWhile[U any](p Predicate[U]) Operator[U, U] {
return func(s Seq[U]) Seq[U] {
return func(yield func(U) bool) {
skipping := true
for u := range s {
if skipping && p(u) {
continue
}
skipping = false
if !yield(u) {
return
}
}
}
}
}

View File

@@ -461,3 +461,831 @@ func ExampleTake_chained() {
}
// Output: 4 5 6 7 8
}
// TestSkipWhile tests basic SkipWhile functionality
func TestSkipWhile(t *testing.T) {
t.Run("skips while predicate is true", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 2, 1)
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(seq))
assert.Equal(t, []int{4, 5, 2, 1}, result)
})
t.Run("skips none when first element fails", func(t *testing.T) {
seq := From(5, 1, 2, 3)
result := toSlice(SkipWhile(func(x int) bool { return x < 5 })(seq))
assert.Equal(t, []int{5, 1, 2, 3}, result)
})
t.Run("skips all when predicate always true", func(t *testing.T) {
seq := From(2, 4, 6, 8)
result := toSlice(SkipWhile(func(x int) bool { return x%2 == 0 })(seq))
assert.Empty(t, result)
})
t.Run("skips from string sequence", func(t *testing.T) {
seq := From("a", "b", "c", "1", "d", "e")
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
result := toSlice(SkipWhile(isLetter)(seq))
assert.Equal(t, []string{"1", "d", "e"}, result)
})
t.Run("continues after predicate fails", func(t *testing.T) {
seq := From(1, 2, 3, 4, 1, 2, 3)
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(seq))
assert.Equal(t, []int{4, 1, 2, 3}, result)
})
t.Run("skips single element", func(t *testing.T) {
seq := From(1, 10, 2, 3)
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
assert.Equal(t, []int{10, 2, 3}, result)
})
}
// TestSkipWhileEmpty tests SkipWhile with empty sequences
func TestSkipWhileEmpty(t *testing.T) {
t.Run("returns empty from empty sequence", func(t *testing.T) {
seq := Empty[int]()
result := toSlice(SkipWhile(func(x int) bool { return x > 0 })(seq))
assert.Empty(t, result)
})
t.Run("returns empty when predicate always satisfied", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
assert.Empty(t, result)
})
}
// TestSkipWhileWithComplexTypes tests SkipWhile with complex data types
func TestSkipWhileWithComplexTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("skips structs while condition met", func(t *testing.T) {
seq := From(
Person{"Alice", 25},
Person{"Bob", 30},
Person{"Charlie", 35},
Person{"David", 28},
)
result := toSlice(SkipWhile(func(p Person) bool { return p.Age < 35 })(seq))
expected := []Person{
{"Charlie", 35},
{"David", 28},
}
assert.Equal(t, expected, result)
})
t.Run("skips pointers while condition met", func(t *testing.T) {
p1 := &Person{"Alice", 25}
p2 := &Person{"Bob", 30}
p3 := &Person{"Charlie", 35}
p4 := &Person{"David", 28}
seq := From(p1, p2, p3, p4)
result := toSlice(SkipWhile(func(p *Person) bool { return p.Age < 35 })(seq))
assert.Equal(t, []*Person{p3, p4}, result)
})
t.Run("skips slices while condition met", func(t *testing.T) {
seq := From([]int{1}, []int{1, 2}, []int{1, 2, 3}, []int{1})
result := toSlice(SkipWhile(func(s []int) bool { return len(s) < 3 })(seq))
expected := [][]int{{1, 2, 3}, {1}}
assert.Equal(t, expected, result)
})
}
// TestSkipWhileWithChainedOperations tests SkipWhile with other sequence operations
func TestSkipWhileWithChainedOperations(t *testing.T) {
t.Run("skipWhile after map", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
result := toSlice(SkipWhile(func(x int) bool { return x < 8 })(mapped))
assert.Equal(t, []int{8, 10}, result)
})
t.Run("skipWhile after filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
result := toSlice(SkipWhile(func(x int) bool { return x < 6 })(filtered))
assert.Equal(t, []int{6, 8, 10}, result)
})
t.Run("map after skipWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
result := toSlice(MonadMap(skipped, N.Mul(10)))
assert.Equal(t, []int{40, 50}, result)
})
t.Run("filter after skipWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
result := toSlice(MonadFilter(skipped, func(x int) bool { return x%2 == 0 }))
assert.Equal(t, []int{4, 6, 8}, result)
})
t.Run("skipWhile after chain", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
result := toSlice(SkipWhile(func(x int) bool { return x < 20 })(chained))
assert.Equal(t, []int{20, 3, 30}, result)
})
t.Run("skip after skipWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
skipped1 := SkipWhile(func(x int) bool { return x < 4 })(seq)
skipped2 := Skip[int](2)(skipped1)
result := toSlice(skipped2)
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
})
t.Run("skipWhile after skip", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
skipped := Skip[int](3)(seq)
result := toSlice(SkipWhile(func(x int) bool { return x < 7 })(skipped))
assert.Equal(t, []int{7, 8, 9, 10}, result)
})
t.Run("takeWhile after skipWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
taken := TakeWhile(func(x int) bool { return x < 8 })(skipped)
result := toSlice(taken)
assert.Equal(t, []int{4, 5, 6, 7}, result)
})
t.Run("skipWhile after takeWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
taken := TakeWhile(func(x int) bool { return x < 8 })(seq)
skipped := SkipWhile(func(x int) bool { return x < 4 })(taken)
result := toSlice(skipped)
assert.Equal(t, []int{4, 5, 6, 7}, result)
})
}
// TestSkipWhileWithReplicate tests SkipWhile with Replicate
func TestSkipWhileWithReplicate(t *testing.T) {
t.Run("skips all from replicated sequence", func(t *testing.T) {
seq := Replicate(10, 5)
result := toSlice(SkipWhile(func(x int) bool { return x == 5 })(seq))
assert.Empty(t, result)
})
t.Run("skips none when predicate fails on replicate", func(t *testing.T) {
seq := Replicate(5, 10)
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
assert.Equal(t, []int{10, 10, 10, 10, 10}, result)
})
}
// TestSkipWhileWithMakeBy tests SkipWhile with MakeBy
func TestSkipWhileWithMakeBy(t *testing.T) {
t.Run("skips from generated sequence", func(t *testing.T) {
seq := MakeBy(10, func(i int) int { return i * i })
result := toSlice(SkipWhile(func(x int) bool { return x < 25 })(seq))
assert.Equal(t, []int{25, 36, 49, 64, 81}, result)
})
t.Run("skips all from generated sequence", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i + 1 })
result := toSlice(SkipWhile(func(x int) bool { return x < 100 })(seq))
assert.Empty(t, result)
})
}
// TestSkipWhileWithPrependAppend tests SkipWhile with Prepend and Append
func TestSkipWhileWithPrependAppend(t *testing.T) {
t.Run("skipWhile from prepended sequence", func(t *testing.T) {
seq := From(2, 3, 4, 5)
prepended := Prepend(1)(seq)
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(prepended))
assert.Equal(t, []int{4, 5}, result)
})
t.Run("skipWhile from appended sequence", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(10)(seq)
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(appended))
assert.Equal(t, []int{10}, result)
})
t.Run("skipWhile includes appended element", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
result := toSlice(SkipWhile(func(x int) bool { return x < 3 })(appended))
assert.Equal(t, []int{3, 4}, result)
})
}
// TestSkipWhileWithFlatten tests SkipWhile with Flatten
func TestSkipWhileWithFlatten(t *testing.T) {
t.Run("skips from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5, 6))
flattened := Flatten(nested)
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(flattened))
assert.Equal(t, []int{4, 5, 6}, result)
})
t.Run("skips from flattened with empty inner sequences", func(t *testing.T) {
nested := From(From(1, 2), Empty[int](), From(3, 4))
flattened := Flatten(nested)
result := toSlice(SkipWhile(func(x int) bool { return x < 3 })(flattened))
assert.Equal(t, []int{3, 4}, result)
})
}
// TestSkipWhileDoesNotConsumeEntireSequence tests that SkipWhile is lazy
func TestSkipWhileDoesNotConsumeEntireSequence(t *testing.T) {
t.Run("only consumes needed elements", func(t *testing.T) {
callCount := 0
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
callCount++
return x * 2
})
skipped := SkipWhile(func(x int) bool { return x < 8 })(seq)
result := []int{}
for v := range skipped {
result = append(result, v)
}
assert.Equal(t, []int{8, 10, 12, 14, 16, 18, 20}, result)
// Should process all elements since we iterate through all remaining
assert.Equal(t, 10, callCount, "should process all elements")
})
t.Run("stops early when consumer stops", func(t *testing.T) {
callCount := 0
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, func(x int) bool {
callCount++
return x%2 == 0
})
skipped := SkipWhile(func(x int) bool { return x < 6 })(filtered)
result := []int{}
count := 0
for v := range skipped {
result = append(result, v)
count++
if count == 2 {
break
}
}
assert.Equal(t, []int{6, 8}, result)
// Should stop after getting 2 elements
assert.LessOrEqual(t, callCount, 9, "should not consume all elements")
})
}
// TestSkipWhileEdgeCases tests edge cases
func TestSkipWhileEdgeCases(t *testing.T) {
t.Run("skipWhile with always false predicate", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(SkipWhile(func(x int) bool { return false })(seq))
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("skipWhile with always true predicate", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(SkipWhile(func(x int) bool { return true })(seq))
assert.Empty(t, result)
})
t.Run("skipWhile from single element that passes", func(t *testing.T) {
seq := From(42)
result := toSlice(SkipWhile(func(x int) bool { return x > 0 })(seq))
assert.Empty(t, result)
})
t.Run("skipWhile from single element that fails", func(t *testing.T) {
seq := From(42)
result := toSlice(SkipWhile(func(x int) bool { return x < 0 })(seq))
assert.Equal(t, []int{42}, result)
})
t.Run("skipWhile with complex predicate", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result := toSlice(SkipWhile(func(x int) bool {
return x%2 == 1 || x < 5
})(seq))
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
})
t.Run("skipWhile yields elements that satisfy predicate after first failure", func(t *testing.T) {
seq := From(1, 2, 3, 10, 1, 2, 3)
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
assert.Equal(t, []int{10, 1, 2, 3}, result)
})
}
// Benchmark tests for SkipWhile
func BenchmarkSkipWhile(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
skipped := SkipWhile(func(x int) bool { return x < 6 })(seq)
for range skipped {
}
}
}
func BenchmarkSkipWhileLargeSequence(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
seq := From(data...)
b.ResetTimer()
for range b.N {
skipped := SkipWhile(func(x int) bool { return x < 100 })(seq)
for range skipped {
}
}
}
func BenchmarkSkipWhileWithMap(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
mapped := MonadMap(seq, N.Mul(2))
skipped := SkipWhile(func(x int) bool { return x < 12 })(mapped)
for range skipped {
}
}
}
func BenchmarkSkipWhileWithFilter(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
skipped := SkipWhile(func(x int) bool { return x < 6 })(filtered)
for range skipped {
}
}
}
// Example tests for documentation
func ExampleSkipWhile() {
seq := From(1, 2, 3, 4, 5, 2, 1)
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
for v := range skipped {
fmt.Printf("%d ", v)
}
// Output: 4 5 2 1
}
func ExampleSkipWhile_allSatisfy() {
seq := From(2, 4, 6, 8)
skipped := SkipWhile(func(x int) bool { return x%2 == 0 })(seq)
count := 0
for range skipped {
count++
}
fmt.Printf("Count: %d\n", count)
// Output: Count: 0
}
func ExampleSkipWhile_firstFails() {
seq := From(5, 1, 2, 3)
skipped := SkipWhile(func(x int) bool { return x < 5 })(seq)
for v := range skipped {
fmt.Printf("%d ", v)
}
// Output: 5 1 2 3
}
func ExampleSkipWhile_withMap() {
seq := From(1, 2, 3, 4, 5)
doubled := MonadMap(seq, N.Mul(2))
skipped := SkipWhile(func(x int) bool { return x < 8 })(doubled)
for v := range skipped {
fmt.Printf("%d ", v)
}
// Output: 8 10
}
func ExampleSkipWhile_strings() {
seq := From("a", "b", "c", "1", "d", "e")
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
skipped := SkipWhile(isLetter)(seq)
for v := range skipped {
fmt.Printf("%s ", v)
}
// Output: 1 d e
}
// TestTakeWhile tests basic TakeWhile functionality
func TestTakeWhile(t *testing.T) {
t.Run("takes while predicate is true", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 2, 1)
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(seq))
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("takes all when predicate always true", func(t *testing.T) {
seq := From(2, 4, 6, 8)
result := toSlice(TakeWhile(func(x int) bool { return x%2 == 0 })(seq))
assert.Equal(t, []int{2, 4, 6, 8}, result)
})
t.Run("takes none when first element fails", func(t *testing.T) {
seq := From(5, 1, 2, 3)
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(seq))
assert.Empty(t, result)
})
t.Run("takes from string sequence", func(t *testing.T) {
seq := From("a", "b", "c", "1", "d", "e")
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
result := toSlice(TakeWhile(isLetter)(seq))
assert.Equal(t, []string{"a", "b", "c"}, result)
})
t.Run("takes single element", func(t *testing.T) {
seq := From(1, 10, 2, 3)
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(seq))
assert.Equal(t, []int{1}, result)
})
t.Run("stops at first false predicate", func(t *testing.T) {
seq := From(1, 2, 3, 4, 1, 2, 3)
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(seq))
assert.Equal(t, []int{1, 2, 3}, result)
})
}
// TestTakeWhileEmpty tests TakeWhile with empty sequences
func TestTakeWhileEmpty(t *testing.T) {
t.Run("returns empty from empty sequence", func(t *testing.T) {
seq := Empty[int]()
result := toSlice(TakeWhile(func(x int) bool { return x > 0 })(seq))
assert.Empty(t, result)
})
t.Run("returns empty when predicate never satisfied", func(t *testing.T) {
seq := From(10, 20, 30)
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(seq))
assert.Empty(t, result)
})
}
// TestTakeWhileWithComplexTypes tests TakeWhile with complex data types
func TestTakeWhileWithComplexTypes(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("takes structs while condition met", func(t *testing.T) {
seq := From(
Person{"Alice", 25},
Person{"Bob", 30},
Person{"Charlie", 35},
Person{"David", 28},
)
result := toSlice(TakeWhile(func(p Person) bool { return p.Age < 35 })(seq))
expected := []Person{
{"Alice", 25},
{"Bob", 30},
}
assert.Equal(t, expected, result)
})
t.Run("takes pointers while condition met", func(t *testing.T) {
p1 := &Person{"Alice", 25}
p2 := &Person{"Bob", 30}
p3 := &Person{"Charlie", 35}
seq := From(p1, p2, p3)
result := toSlice(TakeWhile(func(p *Person) bool { return p.Age < 35 })(seq))
assert.Equal(t, []*Person{p1, p2}, result)
})
t.Run("takes slices while condition met", func(t *testing.T) {
seq := From([]int{1}, []int{1, 2}, []int{1, 2, 3}, []int{1})
result := toSlice(TakeWhile(func(s []int) bool { return len(s) < 3 })(seq))
expected := [][]int{{1}, {1, 2}}
assert.Equal(t, expected, result)
})
}
// TestTakeWhileWithChainedOperations tests TakeWhile with other sequence operations
func TestTakeWhileWithChainedOperations(t *testing.T) {
t.Run("takeWhile after map", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
result := toSlice(TakeWhile(func(x int) bool { return x < 8 })(mapped))
assert.Equal(t, []int{2, 4, 6}, result)
})
t.Run("takeWhile after filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
result := toSlice(TakeWhile(func(x int) bool { return x < 7 })(filtered))
assert.Equal(t, []int{2, 4, 6}, result)
})
t.Run("map after takeWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
taken := TakeWhile(func(x int) bool { return x < 4 })(seq)
result := toSlice(MonadMap(taken, N.Mul(10)))
assert.Equal(t, []int{10, 20, 30}, result)
})
t.Run("filter after takeWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
taken := TakeWhile(func(x int) bool { return x < 7 })(seq)
result := toSlice(MonadFilter(taken, func(x int) bool { return x%2 == 0 }))
assert.Equal(t, []int{2, 4, 6}, result)
})
t.Run("takeWhile after chain", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
result := toSlice(TakeWhile(func(x int) bool { return x < 20 })(chained))
assert.Equal(t, []int{1, 10, 2}, result)
})
t.Run("take after takeWhile", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
taken1 := TakeWhile(func(x int) bool { return x < 8 })(seq)
taken2 := Take[int](3)(taken1)
result := toSlice(taken2)
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("takeWhile after take", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
taken := Take[int](7)(seq)
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(taken))
assert.Equal(t, []int{1, 2, 3, 4}, result)
})
}
// TestTakeWhileWithReplicate tests TakeWhile with Replicate
func TestTakeWhileWithReplicate(t *testing.T) {
t.Run("takes from replicated sequence", func(t *testing.T) {
seq := Replicate(10, 5)
result := toSlice(TakeWhile(func(x int) bool { return x == 5 })(seq))
assert.Equal(t, []int{5, 5, 5, 5, 5, 5, 5, 5, 5, 5}, result)
})
t.Run("takes none when predicate fails on replicate", func(t *testing.T) {
seq := Replicate(5, 10)
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(seq))
assert.Empty(t, result)
})
}
// TestTakeWhileWithMakeBy tests TakeWhile with MakeBy
func TestTakeWhileWithMakeBy(t *testing.T) {
t.Run("takes from generated sequence", func(t *testing.T) {
seq := MakeBy(10, func(i int) int { return i * i })
result := toSlice(TakeWhile(func(x int) bool { return x < 25 })(seq))
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
})
t.Run("takes all from generated sequence", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i + 1 })
result := toSlice(TakeWhile(func(x int) bool { return x < 100 })(seq))
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
}
// TestTakeWhileWithPrependAppend tests TakeWhile with Prepend and Append
func TestTakeWhileWithPrependAppend(t *testing.T) {
t.Run("takeWhile from prepended sequence", func(t *testing.T) {
seq := From(2, 3, 4, 5)
prepended := Prepend(1)(seq)
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(prepended))
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("takeWhile from appended sequence", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(10)(seq)
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(appended))
assert.Equal(t, []int{1, 2, 3}, result)
})
t.Run("takeWhile includes appended element", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(appended))
assert.Equal(t, []int{1, 2, 3, 4}, result)
})
}
// TestTakeWhileWithFlatten tests TakeWhile with Flatten
func TestTakeWhileWithFlatten(t *testing.T) {
t.Run("takes from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5, 6))
flattened := Flatten(nested)
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(flattened))
assert.Equal(t, []int{1, 2, 3, 4}, result)
})
t.Run("takes from flattened with empty inner sequences", func(t *testing.T) {
nested := From(From(1, 2), Empty[int](), From(3, 4))
flattened := Flatten(nested)
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(flattened))
assert.Equal(t, []int{1, 2, 3}, result)
})
}
// TestTakeWhileDoesNotConsumeEntireSequence tests that TakeWhile is lazy
func TestTakeWhileDoesNotConsumeEntireSequence(t *testing.T) {
t.Run("only consumes needed elements", func(t *testing.T) {
callCount := 0
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
callCount++
return x * 2
})
taken := TakeWhile(func(x int) bool { return x < 8 })(seq)
result := []int{}
for v := range taken {
result = append(result, v)
}
assert.Equal(t, []int{2, 4, 6}, result)
// Should stop after finding element that fails predicate
assert.LessOrEqual(t, callCount, 5, "should not consume significantly more than needed")
assert.GreaterOrEqual(t, callCount, 4, "should consume at least enough to find failure")
})
t.Run("stops early with filter", func(t *testing.T) {
callCount := 0
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, func(x int) bool {
callCount++
return x%2 == 0
})
taken := TakeWhile(func(x int) bool { return x < 7 })(filtered)
result := []int{}
for v := range taken {
result = append(result, v)
}
assert.Equal(t, []int{2, 4, 6}, result)
// Should stop after finding even number >= 7
assert.LessOrEqual(t, callCount, 9, "should not consume significantly more than needed")
assert.GreaterOrEqual(t, callCount, 7, "should consume at least enough to find 8")
})
}
// TestTakeWhileEdgeCases tests edge cases
func TestTakeWhileEdgeCases(t *testing.T) {
t.Run("takeWhile with always false predicate", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(TakeWhile(func(x int) bool { return false })(seq))
assert.Empty(t, result)
})
t.Run("takeWhile with always true predicate", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
result := toSlice(TakeWhile(func(x int) bool { return true })(seq))
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
t.Run("takeWhile from single element that passes", func(t *testing.T) {
seq := From(42)
result := toSlice(TakeWhile(func(x int) bool { return x > 0 })(seq))
assert.Equal(t, []int{42}, result)
})
t.Run("takeWhile from single element that fails", func(t *testing.T) {
seq := From(42)
result := toSlice(TakeWhile(func(x int) bool { return x < 0 })(seq))
assert.Empty(t, result)
})
t.Run("takeWhile with complex predicate", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
result := toSlice(TakeWhile(func(x int) bool {
return x%2 == 1 || x < 5
})(seq))
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
})
}
// Benchmark tests for TakeWhile
func BenchmarkTakeWhile(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
taken := TakeWhile(func(x int) bool { return x < 6 })(seq)
for range taken {
}
}
}
func BenchmarkTakeWhileLargeSequence(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
seq := From(data...)
b.ResetTimer()
for range b.N {
taken := TakeWhile(func(x int) bool { return x < 100 })(seq)
for range taken {
}
}
}
func BenchmarkTakeWhileWithMap(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
mapped := MonadMap(seq, N.Mul(2))
taken := TakeWhile(func(x int) bool { return x < 12 })(mapped)
for range taken {
}
}
}
func BenchmarkTakeWhileWithFilter(b *testing.B) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b.ResetTimer()
for range b.N {
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
taken := TakeWhile(func(x int) bool { return x < 7 })(filtered)
for range taken {
}
}
}
// Example tests for documentation
func ExampleTakeWhile() {
seq := From(1, 2, 3, 4, 5, 2, 1)
taken := TakeWhile(func(x int) bool { return x < 4 })(seq)
for v := range taken {
fmt.Printf("%d ", v)
}
// Output: 1 2 3
}
func ExampleTakeWhile_allSatisfy() {
seq := From(2, 4, 6, 8)
taken := TakeWhile(func(x int) bool { return x%2 == 0 })(seq)
for v := range taken {
fmt.Printf("%d ", v)
}
// Output: 2 4 6 8
}
func ExampleTakeWhile_firstFails() {
seq := From(5, 1, 2, 3)
taken := TakeWhile(func(x int) bool { return x < 5 })(seq)
count := 0
for range taken {
count++
}
fmt.Printf("Count: %d\n", count)
// Output: Count: 0
}
func ExampleTakeWhile_withMap() {
seq := From(1, 2, 3, 4, 5)
doubled := MonadMap(seq, N.Mul(2))
taken := TakeWhile(func(x int) bool { return x < 8 })(doubled)
for v := range taken {
fmt.Printf("%d ", v)
}
// Output: 2 4 6
}
func ExampleTakeWhile_strings() {
seq := From("a", "b", "c", "1", "d", "e")
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
taken := TakeWhile(isLetter)(seq)
for v := range taken {
fmt.Printf("%s ", v)
}
// Output: a b c
}

View File

@@ -32,6 +32,13 @@ import (
// the number of unique keys encountered. The operation is lazy - elements are processed
// and filtered as they are consumed.
//
// Marble Diagram:
//
// Input: --1--2--3--2--4--1--5-->
// Uniq(identity)
// Output: --1--2--3-----4-----5-->
// (first occurrence only)
//
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
//
// Type Parameters:
@@ -119,6 +126,13 @@ func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
// The operation maintains a map of seen elements internally, so memory usage grows with
// the number of unique elements. Only the first occurrence of each unique element is kept.
//
// Marble Diagram:
//
// Input: --1--2--3--2--4--1--5-->
// StrictUniq
// Output: --1--2--3-----4-----5-->
// (first occurrence only)
//
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
//
// Type Parameters:

View File

@@ -115,10 +115,7 @@ func Inc[T Number](value T) T {
// result := Min(5, 10) // returns 5
// result := Min(3.14, 2.71) // returns 2.71
func Min[A C.Ordered](a, b A) A {
if a < b {
return a
}
return b
return min(a, b)
}
// Max returns the maximum of two ordered values.
@@ -132,10 +129,7 @@ func Min[A C.Ordered](a, b A) A {
// result := Max(5, 10) // returns 10
// result := Max(3.14, 2.71) // returns 3.14
func Max[A C.Ordered](a, b A) A {
if a > b {
return a
}
return b
return max(a, b)
}
// MoreThan is a curried comparison function that checks if a value is more than (greater than) another.

View File

@@ -522,3 +522,199 @@ func MarshalJSON[T any](
},
)
}
// FromNonZero creates a bidirectional codec for non-zero values of comparable types.
// This codec validates that values are not equal to their zero value (e.g., 0 for int,
// "" for string, false for bool, nil for pointers).
//
// The codec uses a refinement (prism) that:
// - Decodes: Validates that the input is not the zero value of type T
// - Encodes: Returns the value unchanged (identity function)
// - Validates: Ensures the value is non-zero/non-default
//
// This is useful for enforcing that required fields have meaningful values rather than
// their default zero values, which often represent "not set" or "missing" states.
//
// Type Parameters:
// - T: A comparable type (must support == and != operators)
//
// Returns:
// - A Type[T, T, T] codec that validates non-zero values
//
// Example:
//
// // Create a codec for non-zero integers
// nonZeroInt := FromNonZero[int]()
//
// // Decode non-zero value succeeds
// result := nonZeroInt.Decode(42)
// // result is Right(42)
//
// // Decode zero value fails
// result := nonZeroInt.Decode(0)
// // result is Left(ValidationError{...})
//
// // Encode is identity
// encoded := nonZeroInt.Encode(42)
// // encoded is 42
//
// // Works with strings
// nonEmptyStr := FromNonZero[string]()
// result := nonEmptyStr.Decode("hello") // Right("hello")
// result = nonEmptyStr.Decode("") // Left(ValidationError{...})
//
// // Works with pointers
// nonNilPtr := FromNonZero[*int]()
// value := 42
// result := nonNilPtr.Decode(&value) // Right(&value)
// result = nonNilPtr.Decode(nil) // Left(ValidationError{...})
//
// Common use cases:
// - Validating required numeric fields are not zero
// - Ensuring string fields are not empty
// - Checking pointers are not nil
// - Validating boolean flags are explicitly set to true
// - Composing with other codecs for multi-stage validation
//
// See Also:
// - NonEmptyString: Specialized version for strings with clearer intent
// - FromRefinement: General function for creating codecs from prisms
func FromNonZero[T comparable]() Type[T, T, T] {
return FromRefinement(prism.FromNonZero[T]())
}
// NonEmptyString creates a bidirectional codec for non-empty strings.
// This codec validates that string values are not empty, providing a type-safe
// way to work with strings that must contain at least one character.
//
// This is a specialized version of FromNonZero[string]() that makes the intent
// clearer when working specifically with strings that must not be empty.
//
// The codec:
// - Decodes: Validates that the input string is not empty ("")
// - Encodes: Returns the string unchanged (identity function)
// - Validates: Ensures the string has length > 0
//
// Note: This codec only checks for empty strings, not whitespace-only strings.
// A string containing only spaces, tabs, or newlines will pass validation.
//
// Returns:
// - A Type[string, string, string] codec that validates non-empty strings
//
// Example:
//
// nonEmpty := NonEmptyString()
//
// // Decode non-empty string succeeds
// result := nonEmpty.Decode("hello")
// // result is Right("hello")
//
// // Decode empty string fails
// result := nonEmpty.Decode("")
// // result is Left(ValidationError{...})
//
// // Whitespace-only strings pass validation
// result := nonEmpty.Decode(" ")
// // result is Right(" ")
//
// // Encode is identity
// encoded := nonEmpty.Encode("world")
// // encoded is "world"
//
// // Compose with other codecs for validation pipelines
// intFromNonEmptyString := Pipe(IntFromString())(nonEmpty)
// result := intFromNonEmptyString.Decode("42") // Right(42)
// result = intFromNonEmptyString.Decode("") // Left(ValidationError{...})
// result = intFromNonEmptyString.Decode("abc") // Left(ValidationError{...})
//
// Common use cases:
// - Validating required string fields (usernames, names, IDs)
// - Ensuring configuration values are provided
// - Validating user input before processing
// - Composing with parsing codecs to validate before parsing
// - Building validation pipelines for string data
//
// See Also:
// - FromNonZero: General version for any comparable type
// - String: Basic string codec without validation
// - IntFromString: Codec for parsing integers from strings
func NonEmptyString() Type[string, string, string] {
return F.Pipe1(
FromRefinement(prism.NonEmptyString()),
WithName[string, string, string]("NonEmptyString"),
)
}
// WithName creates an endomorphism that renames a codec without changing its behavior.
// This function returns a higher-order function that takes a codec and returns a new codec
// with the specified name, while preserving all validation, encoding, and type-checking logic.
//
// This is useful for:
// - Providing more descriptive names for composed codecs
// - Creating domain-specific codec names for better error messages
// - Documenting the purpose of complex codec pipelines
// - Improving debugging and logging output
//
// The renamed codec maintains the same:
// - Type checking behavior (Is function)
// - Validation logic (Validate function)
// - Encoding behavior (Encode function)
//
// Only the name returned by the Name() method changes.
//
// Type Parameters:
// - A: The target type (what we decode to and encode from)
// - O: The output type (what we encode to)
// - I: The input type (what we decode from)
//
// Parameters:
// - name: The new name for the codec
//
// Returns:
// - An Endomorphism[Type[A, O, I]] that renames the codec
//
// Example:
//
// // Create a codec with a generic name
// positiveInt := Pipe[int, int, string, int](
// FromRefinement(prism.FromPredicate(func(n int) bool { return n > 0 })),
// )(IntFromString())
// // positiveInt.Name() returns something like "Pipe(FromRefinement(...), IntFromString)"
//
// // Rename it for clarity
// namedCodec := WithName[int, string, string]("PositiveIntFromString")(positiveInt)
// // namedCodec.Name() returns "PositiveIntFromString"
//
// // Use in a pipeline with F.Pipe
// userAgeCodec := F.Pipe1(
// IntFromString(),
// WithName[int, string, string]("UserAge"),
// )
//
// // Validation errors will show the custom name
// result := userAgeCodec.Decode("invalid")
// // Error context will reference "UserAge" instead of "IntFromString"
//
// Common use cases:
// - Naming composed codecs for better error messages
// - Creating domain-specific codec names (e.g., "EmailAddress", "PhoneNumber")
// - Documenting complex validation pipelines
// - Improving debugging output in logs
// - Making codec composition more readable
//
// Note: This function creates a new codec instance with the same behavior but a different
// name. The original codec is not modified.
//
// See Also:
// - MakeType: For creating codecs with custom names from scratch
// - Pipe: For composing codecs (which generates automatic names)
func WithName[A, O, I any](name string) Endomorphism[Type[A, O, I]] {
return func(codec Type[A, O, I]) Type[A, O, I] {
return MakeType(
name,
codec.Is,
codec.Validate,
codec.Encode,
)
}
}

View File

@@ -23,6 +23,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
@@ -688,6 +689,596 @@ func TestBoolFromString_Integration(t *testing.T) {
})
}
// ---------------------------------------------------------------------------
// FromNonZero
// ---------------------------------------------------------------------------
func TestFromNonZero_Decode_Success(t *testing.T) {
t.Run("int - decodes non-zero value", func(t *testing.T) {
c := FromNonZero[int]()
result := c.Decode(42)
assert.Equal(t, validation.Success(42), result)
})
t.Run("int - decodes negative value", func(t *testing.T) {
c := FromNonZero[int]()
result := c.Decode(-5)
assert.Equal(t, validation.Success(-5), result)
})
t.Run("string - decodes non-empty string", func(t *testing.T) {
c := FromNonZero[string]()
result := c.Decode("hello")
assert.Equal(t, validation.Success("hello"), result)
})
t.Run("string - decodes whitespace string", func(t *testing.T) {
c := FromNonZero[string]()
result := c.Decode(" ")
assert.Equal(t, validation.Success(" "), result)
})
t.Run("bool - decodes true", func(t *testing.T) {
c := FromNonZero[bool]()
result := c.Decode(true)
assert.Equal(t, validation.Success(true), result)
})
t.Run("float64 - decodes non-zero value", func(t *testing.T) {
c := FromNonZero[float64]()
result := c.Decode(3.14)
assert.Equal(t, validation.Success(3.14), result)
})
t.Run("float64 - decodes negative value", func(t *testing.T) {
c := FromNonZero[float64]()
result := c.Decode(-2.5)
assert.Equal(t, validation.Success(-2.5), result)
})
t.Run("pointer - decodes non-nil pointer", func(t *testing.T) {
c := FromNonZero[*int]()
value := 42
result := c.Decode(&value)
assert.True(t, either.IsRight(result))
ptr := either.MonadFold(result, func(validation.Errors) *int { return nil }, func(p *int) *int { return p })
require.NotNil(t, ptr)
assert.Equal(t, 42, *ptr)
})
}
func TestFromNonZero_Decode_Failure(t *testing.T) {
t.Run("int - fails on zero", func(t *testing.T) {
c := FromNonZero[int]()
result := c.Decode(0)
assert.True(t, either.IsLeft(result))
})
t.Run("string - fails on empty string", func(t *testing.T) {
c := FromNonZero[string]()
result := c.Decode("")
assert.True(t, either.IsLeft(result))
})
t.Run("bool - fails on false", func(t *testing.T) {
c := FromNonZero[bool]()
result := c.Decode(false)
assert.True(t, either.IsLeft(result))
})
t.Run("float64 - fails on zero", func(t *testing.T) {
c := FromNonZero[float64]()
result := c.Decode(0.0)
assert.True(t, either.IsLeft(result))
})
t.Run("pointer - fails on nil", func(t *testing.T) {
c := FromNonZero[*int]()
result := c.Decode(nil)
assert.True(t, either.IsLeft(result))
})
}
func TestFromNonZero_Encode(t *testing.T) {
t.Run("int - encodes value unchanged", func(t *testing.T) {
c := FromNonZero[int]()
assert.Equal(t, 42, c.Encode(42))
})
t.Run("string - encodes value unchanged", func(t *testing.T) {
c := FromNonZero[string]()
assert.Equal(t, "hello", c.Encode("hello"))
})
t.Run("bool - encodes value unchanged", func(t *testing.T) {
c := FromNonZero[bool]()
assert.Equal(t, true, c.Encode(true))
})
t.Run("float64 - encodes value unchanged", func(t *testing.T) {
c := FromNonZero[float64]()
assert.Equal(t, 3.14, c.Encode(3.14))
})
t.Run("pointer - encodes value unchanged", func(t *testing.T) {
c := FromNonZero[*int]()
value := 42
ptr := &value
assert.Equal(t, ptr, c.Encode(ptr))
})
t.Run("round-trip: decode then encode", func(t *testing.T) {
c := FromNonZero[int]()
original := 42
result := c.Decode(original)
require.True(t, either.IsRight(result))
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
assert.Equal(t, original, c.Encode(decoded))
})
}
func TestFromNonZero_Name(t *testing.T) {
t.Run("int codec name", func(t *testing.T) {
c := FromNonZero[int]()
assert.Contains(t, c.Name(), "FromRefinement")
assert.Contains(t, c.Name(), "PrismFromNonZero")
})
t.Run("string codec name", func(t *testing.T) {
c := FromNonZero[string]()
assert.Contains(t, c.Name(), "FromRefinement")
assert.Contains(t, c.Name(), "PrismFromNonZero")
})
}
func TestFromNonZero_Integration(t *testing.T) {
t.Run("validates multiple non-zero integers", func(t *testing.T) {
c := FromNonZero[int]()
values := []int{1, -1, 42, -100, 999}
for _, v := range values {
result := c.Decode(v)
require.True(t, either.IsRight(result), "expected success for %d", v)
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
assert.Equal(t, v, decoded)
assert.Equal(t, v, c.Encode(decoded))
}
})
t.Run("rejects zero values", func(t *testing.T) {
c := FromNonZero[int]()
result := c.Decode(0)
assert.True(t, either.IsLeft(result))
})
t.Run("works with custom comparable types", func(t *testing.T) {
type UserID string
c := FromNonZero[UserID]()
result := c.Decode(UserID("user123"))
assert.Equal(t, validation.Success(UserID("user123")), result)
result = c.Decode(UserID(""))
assert.True(t, either.IsLeft(result))
})
}
// ---------------------------------------------------------------------------
// NonEmptyString
// ---------------------------------------------------------------------------
func TestNonEmptyString_Decode_Success(t *testing.T) {
t.Run("decodes non-empty string", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("hello")
assert.Equal(t, validation.Success("hello"), result)
})
t.Run("decodes single character", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("a")
assert.Equal(t, validation.Success("a"), result)
})
t.Run("decodes whitespace string", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode(" ")
assert.Equal(t, validation.Success(" "), result)
})
t.Run("decodes string with newlines", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("\n\t")
assert.Equal(t, validation.Success("\n\t"), result)
})
t.Run("decodes unicode string", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("你好")
assert.Equal(t, validation.Success("你好"), result)
})
t.Run("decodes emoji string", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("🎉")
assert.Equal(t, validation.Success("🎉"), result)
})
t.Run("decodes multiline string", func(t *testing.T) {
c := NonEmptyString()
multiline := "line1\nline2\nline3"
result := c.Decode(multiline)
assert.Equal(t, validation.Success(multiline), result)
})
}
func TestNonEmptyString_Decode_Failure(t *testing.T) {
t.Run("fails on empty string", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("")
assert.True(t, either.IsLeft(result))
})
t.Run("error contains context", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("")
require.True(t, either.IsLeft(result))
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(string) validation.Errors { return nil })
require.NotEmpty(t, errors)
})
}
func TestNonEmptyString_Encode(t *testing.T) {
t.Run("encodes string unchanged", func(t *testing.T) {
c := NonEmptyString()
assert.Equal(t, "hello", c.Encode("hello"))
})
t.Run("encodes unicode string unchanged", func(t *testing.T) {
c := NonEmptyString()
assert.Equal(t, "你好", c.Encode("你好"))
})
t.Run("encodes whitespace string unchanged", func(t *testing.T) {
c := NonEmptyString()
assert.Equal(t, " ", c.Encode(" "))
})
t.Run("round-trip: decode then encode", func(t *testing.T) {
c := NonEmptyString()
original := "test string"
result := c.Decode(original)
require.True(t, either.IsRight(result))
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(s string) string { return s })
assert.Equal(t, original, c.Encode(decoded))
})
}
func TestNonEmptyString_Name(t *testing.T) {
c := NonEmptyString()
assert.Equal(t, c.Name(), "NonEmptyString")
}
func TestNonEmptyString_Integration(t *testing.T) {
t.Run("validates multiple non-empty strings", func(t *testing.T) {
c := NonEmptyString()
strings := []string{"a", "hello", "world", "test123", " spaces ", "🎉"}
for _, s := range strings {
result := c.Decode(s)
require.True(t, either.IsRight(result), "expected success for %q", s)
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(str string) string { return str })
assert.Equal(t, s, decoded)
assert.Equal(t, s, c.Encode(decoded))
}
})
t.Run("rejects empty string", func(t *testing.T) {
c := NonEmptyString()
result := c.Decode("")
assert.True(t, either.IsLeft(result))
})
t.Run("compose with IntFromString", func(t *testing.T) {
// Create a codec that only parses non-empty strings to integers
nonEmptyThenInt := Pipe[string, string](IntFromString())(NonEmptyString())
// Valid non-empty string with integer
result := nonEmptyThenInt.Decode("42")
assert.Equal(t, validation.Success(42), result)
// Empty string fails at NonEmptyString stage
result = nonEmptyThenInt.Decode("")
assert.True(t, either.IsLeft(result))
// Non-empty but invalid integer fails at IntFromString stage
result = nonEmptyThenInt.Decode("abc")
assert.True(t, either.IsLeft(result))
})
t.Run("use in validation pipeline", func(t *testing.T) {
c := NonEmptyString()
// Simulate validating user input
inputs := []struct {
value string
expected bool
}{
{"john_doe", true},
{"", false},
{"a", true},
{"user@example.com", true},
}
for _, input := range inputs {
result := c.Decode(input.value)
if input.expected {
assert.True(t, either.IsRight(result), "expected success for %q", input.value)
} else {
assert.True(t, either.IsLeft(result), "expected failure for %q", input.value)
}
}
})
}
// ---------------------------------------------------------------------------
// WithName
// ---------------------------------------------------------------------------
func TestWithName_BasicFunctionality(t *testing.T) {
t.Run("renames codec without changing behavior", func(t *testing.T) {
original := IntFromString()
renamed := WithName[int, string, string]("CustomIntCodec")(original)
// Name should be changed
assert.Equal(t, "CustomIntCodec", renamed.Name())
assert.NotEqual(t, original.Name(), renamed.Name())
// Behavior should be unchanged
result := renamed.Decode("42")
assert.Equal(t, validation.Success(42), result)
encoded := renamed.Encode(42)
assert.Equal(t, "42", encoded)
})
t.Run("preserves validation logic", func(t *testing.T) {
original := IntFromString()
renamed := WithName[int, string, string]("MyInt")(original)
// Valid input should succeed
result := renamed.Decode("123")
assert.True(t, either.IsRight(result))
// Invalid input should fail
result = renamed.Decode("not a number")
assert.True(t, either.IsLeft(result))
})
t.Run("preserves encoding logic", func(t *testing.T) {
original := BoolFromString()
renamed := WithName[bool, string, string]("CustomBool")(original)
assert.Equal(t, "true", renamed.Encode(true))
assert.Equal(t, "false", renamed.Encode(false))
})
}
func TestWithName_WithComposedCodecs(t *testing.T) {
t.Run("renames composed codec", func(t *testing.T) {
// Create a composed codec
composed := Pipe[string, string](IntFromString())(NonEmptyString())
// Rename it
renamed := WithName[int, string, string]("NonEmptyIntString")(composed)
assert.Equal(t, "NonEmptyIntString", renamed.Name())
// Behavior should be preserved
result := renamed.Decode("42")
assert.Equal(t, validation.Success(42), result)
// Empty string should fail
result = renamed.Decode("")
assert.True(t, either.IsLeft(result))
// Non-numeric should fail
result = renamed.Decode("abc")
assert.True(t, either.IsLeft(result))
})
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
codec := F.Pipe1(
IntFromString(),
WithName[int, string, string]("UserAge"),
)
assert.Equal(t, "UserAge", codec.Name())
result := codec.Decode("25")
assert.Equal(t, validation.Success(25), result)
})
}
func TestWithName_PreservesTypeChecking(t *testing.T) {
t.Run("preserves Is function", func(t *testing.T) {
original := String()
renamed := WithName[string, string, any]("CustomString")(original)
// Should accept string
result := renamed.Is("hello")
assert.True(t, either.IsRight(result))
// Should reject non-string
result = renamed.Is(42)
assert.True(t, either.IsLeft(result))
})
t.Run("preserves complex type checking", func(t *testing.T) {
original := Array(Int())
renamed := WithName[[]int, []int, any]("IntArray")(original)
// Should accept []int
result := renamed.Is([]int{1, 2, 3})
assert.True(t, either.IsRight(result))
// Should reject []string
result = renamed.Is([]string{"a", "b"})
assert.True(t, either.IsLeft(result))
})
}
func TestWithName_RoundTrip(t *testing.T) {
t.Run("maintains round-trip property", func(t *testing.T) {
original := Int64FromString()
renamed := WithName[int64, string, string]("CustomInt64")(original)
testValues := []string{"0", "42", "-100", "9223372036854775807"}
for _, input := range testValues {
result := renamed.Decode(input)
require.True(t, either.IsRight(result), "expected success for %s", input)
decoded := either.MonadFold(result, func(validation.Errors) int64 { return 0 }, func(n int64) int64 { return n })
encoded := renamed.Encode(decoded)
assert.Equal(t, input, encoded)
}
})
}
func TestWithName_ErrorMessages(t *testing.T) {
t.Run("custom name appears in validation context", func(t *testing.T) {
codec := WithName[int, string, string]("PositiveInteger")(IntFromString())
result := codec.Decode("not a number")
require.True(t, either.IsLeft(result))
// The error context should reference the custom name
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(int) validation.Errors { return nil })
require.NotEmpty(t, errors)
// Check that at least one error references our custom name
found := false
for _, err := range errors {
if len(err.Context) > 0 {
for _, ctx := range err.Context {
if ctx.Type == "PositiveInteger" {
found = true
break
}
}
}
}
assert.True(t, found, "expected custom name 'PositiveInteger' in error context")
})
}
func TestWithName_MultipleRenames(t *testing.T) {
t.Run("can rename multiple times", func(t *testing.T) {
codec := IntFromString()
renamed1 := WithName[int, string, string]("FirstName")(codec)
assert.Equal(t, "FirstName", renamed1.Name())
renamed2 := WithName[int, string, string]("SecondName")(renamed1)
assert.Equal(t, "SecondName", renamed2.Name())
// Behavior should still work
result := renamed2.Decode("42")
assert.Equal(t, validation.Success(42), result)
})
}
func TestWithName_WithDifferentTypes(t *testing.T) {
t.Run("works with string codec", func(t *testing.T) {
codec := WithName[string, string, string]("Username")(NonEmptyString())
assert.Equal(t, "Username", codec.Name())
result := codec.Decode("john_doe")
assert.Equal(t, validation.Success("john_doe"), result)
})
t.Run("works with bool codec", func(t *testing.T) {
codec := WithName[bool, string, string]("IsActive")(BoolFromString())
assert.Equal(t, "IsActive", codec.Name())
result := codec.Decode("true")
assert.Equal(t, validation.Success(true), result)
})
t.Run("works with URL codec", func(t *testing.T) {
codec := WithName[*url.URL, string, string]("WebsiteURL")(URL())
assert.Equal(t, "WebsiteURL", codec.Name())
result := codec.Decode("https://example.com")
assert.True(t, either.IsRight(result))
})
t.Run("works with array codec", func(t *testing.T) {
codec := WithName[[]int, []int, any]("Numbers")(Array(Int()))
assert.Equal(t, "Numbers", codec.Name())
result := codec.Decode([]int{1, 2, 3})
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
})
}
func TestWithName_AsDecoderEncoder(t *testing.T) {
t.Run("AsDecoder returns decoder interface", func(t *testing.T) {
codec := WithName[int, string, string]("MyInt")(IntFromString())
decoder := codec.AsDecoder()
result := decoder.Decode("42")
assert.Equal(t, validation.Success(42), result)
})
t.Run("AsEncoder returns encoder interface", func(t *testing.T) {
codec := WithName[int, string, string]("MyInt")(IntFromString())
encoder := codec.AsEncoder()
encoded := encoder.Encode(42)
assert.Equal(t, "42", encoded)
})
}
func TestWithName_Integration(t *testing.T) {
t.Run("domain-specific codec names", func(t *testing.T) {
// Create domain-specific codecs with meaningful names
emailCodec := WithName[string, string, string]("EmailAddress")(NonEmptyString())
phoneCodec := WithName[string, string, string]("PhoneNumber")(NonEmptyString())
ageCodec := WithName[int, string, string]("Age")(IntFromString())
// Test email
result := emailCodec.Decode("user@example.com")
assert.True(t, either.IsRight(result))
assert.Equal(t, "EmailAddress", emailCodec.Name())
// Test phone
result = phoneCodec.Decode("+1234567890")
assert.True(t, either.IsRight(result))
assert.Equal(t, "PhoneNumber", phoneCodec.Name())
// Test age
ageResult := ageCodec.Decode("25")
assert.True(t, either.IsRight(ageResult))
assert.Equal(t, "Age", ageCodec.Name())
})
t.Run("naming complex validation pipelines", func(t *testing.T) {
// Create a complex codec and give it a clear name
positiveIntCodec := F.Pipe2(
NonEmptyString(),
Pipe[string, string](IntFromString()),
WithName[int, string, string]("PositiveIntegerFromString"),
)
assert.Equal(t, "PositiveIntegerFromString", positiveIntCodec.Name())
result := positiveIntCodec.Decode("42")
assert.True(t, either.IsRight(result))
result = positiveIntCodec.Decode("")
assert.True(t, either.IsLeft(result))
})
}
// ---------------------------------------------------------------------------
// MarshalJSON
// ---------------------------------------------------------------------------
@@ -773,7 +1364,7 @@ func TestIntFromString_PipeComposition(t *testing.T) {
func(n int) int { return n },
"PositiveInt",
)
positiveIntCodec := Pipe[string, string, int, int](
positiveIntCodec := Pipe[string, string](
FromRefinement(positiveIntPrism),
)(IntFromString())

View File

@@ -22,6 +22,7 @@ import (
"github.com/IBM/fp-go/v2/endomorphism"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/functor"
)
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
@@ -909,6 +910,83 @@ func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S
}
}
// ModifyF transforms a value through a lens using a function that returns a value in a functor context.
//
// This is the functorial version of Modify, allowing transformations that produce effects
// (like Option, Either, IO, etc.) while updating the focused value. The functor's map operation
// is used to apply the lens's setter to the transformed value, preserving the computational context.
//
// This function corresponds to modifyF from monocle-ts, enabling effectful updates through lenses.
//
// # Type Parameters
//
// - S: Structure type
// - A: Focus type (the value being transformed)
// - HKTA: Higher-kinded type containing the transformed value (e.g., Option[A], Either[E, A])
// - HKTS: Higher-kinded type containing the updated structure (e.g., Option[S], Either[E, S])
//
// # Parameters
//
// - fmap: A functor map operation that transforms A to S within the functor context
//
// # Returns
//
// - A curried function that takes:
// 1. A transformation function (A → HKTA)
// 2. A Lens[S, A]
// 3. A structure S
// And returns the updated structure in the functor context (HKTS)
//
// # Example Usage
//
// type Person struct {
// Name string
// Age int
// }
//
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, age int) Person { p.Age = age; return p },
// )
//
// // Validate age is positive, returning Option
// validateAge := func(age int) option.Option[int] {
// if age > 0 {
// return option.Some(age)
// }
// return option.None[int]()
// }
//
// // Create a modifier that validates while updating
// modifyAge := lens.ModifyF[Person, int](option.Functor[int, Person]().Map)
//
// person := Person{Name: "Alice", Age: 30}
// result := modifyAge(validateAge)(ageLens)(person)
// // result is Some(Person{Name: "Alice", Age: 30})
//
// invalidResult := modifyAge(func(age int) option.Option[int] {
// return option.None[int]()
// })(ageLens)(person)
// // invalidResult is None[Person]()
//
// # See Also
//
// - Modify: Non-functorial version for simple transformations
// - functor.Functor: The functor interface used for mapping
func ModifyF[S, A, HKTA, HKTS any](
fmap functor.MapType[A, S, HKTA, HKTS],
) func(func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
return func(f func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
return func(sa Lens[S, A]) func(S) HKTS {
return func(s S) HKTS {
return fmap(func(a A) S {
return sa.Set(a)(s)
})(f(sa.Get(s)))
}
}
}
}
// IMap transforms the focus type of a lens using an isomorphism.
//
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.

View File

@@ -16,6 +16,7 @@
package lens
import (
"errors"
"testing"
EQ "github.com/IBM/fp-go/v2/eq"
@@ -937,3 +938,367 @@ func TestMakeLensWithEq_WithNilState_MultipleOperations(t *testing.T) {
assert.NotNil(t, street4)
assert.Equal(t, "", street4.name)
}
// TestModifyF_Success tests ModifyF with a simple Maybe-like functor for successful transformations
func TestModifyF_Success(t *testing.T) {
// Define a simple Maybe type for testing
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
none := func() Maybe[int] {
return Maybe[int]{value: nil}
}
// Functor map for Maybe
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("transforms value with successful result", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
// Function that returns Some for positive values
validatePositive := func(n int) Maybe[int] {
if n > 0 {
return some(n * 2)
}
return none()
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: 5, Foo: "test"}
result := modifyAge(validatePositive)(ageLens)(person)
assert.NotNil(t, result.value)
updated := *result.value
assert.Equal(t, 10, updated.Value)
assert.Equal(t, "test", updated.Foo)
})
t.Run("preserves structure with identity transformation", func(t *testing.T) {
type MaybeStr struct {
value *string
}
someStr := func(s string) MaybeStr {
return MaybeStr{value: &s}
}
maybeStrMap := func(f func(string) Street) func(MaybeStr) struct{ value *Street } {
return func(ma MaybeStr) struct{ value *Street } {
if ma.value == nil {
return struct{ value *Street }{value: nil}
}
result := f(*ma.value)
return struct{ value *Street }{value: &result}
}
}
nameLens := MakeLens(
func(s Street) string { return s.name },
func(s Street, name string) Street { s.name = name; return s },
)
identity := func(s string) MaybeStr {
return someStr(s)
}
modifyName := ModifyF[Street, string](maybeStrMap)
street := Street{num: 1, name: "Main"}
result := modifyName(identity)(nameLens)(street)
assert.NotNil(t, result.value)
assert.Equal(t, street, *result.value)
})
}
// TestModifyF_Failure tests ModifyF with failures
func TestModifyF_Failure(t *testing.T) {
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
none := func() Maybe[int] {
return Maybe[int]{value: nil}
}
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("returns None when transformation fails", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
validatePositive := func(n int) Maybe[int] {
if n > 0 {
return some(n)
}
return none()
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: -5, Foo: "test"}
result := modifyAge(validatePositive)(ageLens)(person)
assert.Nil(t, result.value)
})
}
// TestModifyF_WithResult tests ModifyF with Result/Either-like functor
func TestModifyF_WithResult(t *testing.T) {
type Result[A any] struct {
value *A
err error
}
ok := func(a int) Result[int] {
return Result[int]{value: &a, err: nil}
}
fail := func(e error) Result[int] {
return Result[int]{value: nil, err: e}
}
resultMap := func(f func(int) Inner) func(Result[int]) Result[Inner] {
return func(r Result[int]) Result[Inner] {
if r.err != nil {
return Result[Inner]{value: nil, err: r.err}
}
result := f(*r.value)
return Result[Inner]{value: &result, err: nil}
}
}
t.Run("returns success for valid transformation", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
validateAge := func(n int) Result[int] {
if n >= 0 && n <= 150 {
return ok(n + 1)
}
return fail(errors.New("age out of range"))
}
modifyAge := ModifyF[Inner, int](resultMap)
person := Inner{Value: 30, Foo: "test"}
result := modifyAge(validateAge)(ageLens)(person)
assert.Nil(t, result.err)
assert.NotNil(t, result.value)
assert.Equal(t, 31, result.value.Value)
assert.Equal(t, "test", result.value.Foo)
})
t.Run("returns error for failed validation", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
validateAge := func(n int) Result[int] {
if n >= 0 && n <= 150 {
return ok(n)
}
return fail(errors.New("age out of range"))
}
modifyAge := ModifyF[Inner, int](resultMap)
person := Inner{Value: 200, Foo: "test"}
result := modifyAge(validateAge)(ageLens)(person)
assert.NotNil(t, result.err)
assert.Equal(t, "age out of range", result.err.Error())
assert.Nil(t, result.value)
})
}
// TestModifyF_EdgeCases tests edge cases for ModifyF
func TestModifyF_EdgeCases(t *testing.T) {
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("handles zero values", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
identity := func(n int) Maybe[int] {
return some(n)
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: 0, Foo: ""}
result := modifyAge(identity)(ageLens)(person)
assert.NotNil(t, result.value)
assert.Equal(t, person, *result.value)
})
t.Run("works with composed lenses", func(t *testing.T) {
innerLens := MakeLens(
Outer.GetInner,
Outer.SetInner,
)
valueLens := MakeLensRef(
(*Inner).GetValue,
(*Inner).SetValue,
)
composedLens := Compose[Outer](valueLens)(innerLens)
maybeMapOuter := func(f func(int) Outer) func(Maybe[int]) Maybe[Outer] {
return func(ma Maybe[int]) Maybe[Outer] {
if ma.value == nil {
return Maybe[Outer]{value: nil}
}
result := f(*ma.value)
return Maybe[Outer]{value: &result}
}
}
validatePositive := func(n int) Maybe[int] {
if n > 0 {
return some(n * 2)
}
return Maybe[int]{value: nil}
}
modifyValue := ModifyF[Outer, int](maybeMapOuter)
outer := Outer{inner: &Inner{Value: 5, Foo: "test"}}
result := modifyValue(validatePositive)(composedLens)(outer)
assert.NotNil(t, result.value)
assert.Equal(t, 10, result.value.inner.Value)
assert.Equal(t, "test", result.value.inner.Foo)
})
}
// TestModifyF_Integration tests integration scenarios
func TestModifyF_Integration(t *testing.T) {
type Maybe[A any] struct {
value *A
}
some := func(a int) Maybe[int] {
return Maybe[int]{value: &a}
}
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
return func(ma Maybe[int]) Maybe[Inner] {
if ma.value == nil {
return Maybe[Inner]{value: nil}
}
result := f(*ma.value)
return Maybe[Inner]{value: &result}
}
}
t.Run("chains multiple ModifyF operations", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
increment := func(n int) Maybe[int] {
return some(n + 1)
}
modifyAge := ModifyF[Inner, int](maybeMap)
person := Inner{Value: 5, Foo: "test"}
// Apply transformation twice
result1 := modifyAge(increment)(ageLens)(person)
assert.NotNil(t, result1.value)
result2 := modifyAge(increment)(ageLens)(*result1.value)
assert.NotNil(t, result2.value)
assert.Equal(t, 7, result2.value.Value)
})
t.Run("combines with regular Modify", func(t *testing.T) {
ageLens := MakeLens(
func(p Inner) int { return p.Value },
func(p Inner, age int) Inner { p.Value = age; return p },
)
// First use regular Modify
person := Inner{Value: 5, Foo: "test"}
modified := F.Pipe2(
ageLens,
Modify[Inner](func(n int) int { return n * 2 }),
func(endoFn func(Inner) Inner) Inner {
return endoFn(person)
},
)
assert.Equal(t, 10, modified.Value)
// Then use ModifyF with validation
validateRange := func(n int) Maybe[int] {
if n >= 0 && n <= 100 {
return some(n)
}
return Maybe[int]{value: nil}
}
modifyAge := ModifyF[Inner, int](maybeMap)
result := modifyAge(validateRange)(ageLens)(modified)
assert.NotNil(t, result.value)
})
}

View File

@@ -16,6 +16,7 @@
package generic
import (
"maps"
"sort"
F "github.com/IBM/fp-go/v2/function"
@@ -301,13 +302,8 @@ func unionLast[M ~map[K]V, K comparable, V any](left, right M) M {
result := make(M, lenLeft+lenRight)
for k, v := range left {
result[k] = v
}
for k, v := range right {
result[k] = v
}
maps.Copy(result, left)
maps.Copy(result, right)
return result
}