mirror of
https://github.com/IBM/fp-go.git
synced 2026-04-22 20:33:06 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b288003f93 | |||
| aed19f6edf | |||
| 45cc0a7fc1 | |||
| 21b517d388 | |||
| 0df62c0031 | |||
| 57318e2d1d | |||
| 2b937d3e93 | |||
| 747a1794e5 | |||
| c754cacf1f | |||
| d357b32847 | |||
| a3af003e74 | |||
| c81235827b |
+8
-9
@@ -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
|
||||
|
||||
|
||||
+647
-22
@@ -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)
|
||||
@@ -890,3 +1510,8 @@ func Extend[A, B any](f func([]A) B) Operator[A, B] {
|
||||
func Extract[A any](as []A) A {
|
||||
return G.Extract(as)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func UpdateAt[T any](i int, v T) func([]T) Option[[]T] {
|
||||
return G.UpdateAt[[]T](i, v)
|
||||
}
|
||||
|
||||
+220
-3
@@ -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() {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,3 +489,16 @@ func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
|
||||
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAt[GT ~[]T, T any](i int, v T) func(GT) O.Option[GT] {
|
||||
none := O.None[GT]()
|
||||
if i < 0 {
|
||||
return F.Constant1[GT](none)
|
||||
}
|
||||
return func(g GT) O.Option[GT] {
|
||||
if i >= len(g) {
|
||||
return none
|
||||
}
|
||||
return O.Of(array.UnsafeUpdateAt(g, i, v))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# fp-go/v2 Reference for Claude Code
|
||||
|
||||
fp-go/v2 (`github.com/IBM/fp-go/v2`) is a typed functional programming library for Go 1.24+. It provides Option, Either/Result, IO, and Effect monads with data-last, curried APIs designed for pipeline composition via `Pipe` and `Flow`. The library follows Haskell/fp-ts conventions adapted for Go generics with explicit arity-numbered functions (e.g., `Pipe3`, `Flow2`). Two module families exist: **standard** (struct-based monads, full FP toolkit) and **idiomatic** (Go-native `(value, error)` tuples, zero-alloc, 2-32x faster).
|
||||
|
||||
## Import Conventions
|
||||
|
||||
| Alias | Package |
|
||||
|-------|---------|
|
||||
| `F` | `github.com/IBM/fp-go/v2/function` |
|
||||
| `O` | `github.com/IBM/fp-go/v2/option` |
|
||||
| `E` | `github.com/IBM/fp-go/v2/either` |
|
||||
| `R` | `github.com/IBM/fp-go/v2/result` |
|
||||
| `A` | `github.com/IBM/fp-go/v2/array` |
|
||||
| `IO` | `github.com/IBM/fp-go/v2/io` |
|
||||
| `IOR` | `github.com/IBM/fp-go/v2/ioresult` |
|
||||
| `IOE` | `github.com/IBM/fp-go/v2/ioeither` |
|
||||
| `RIO` | `github.com/IBM/fp-go/v2/context/readerioresult` |
|
||||
| `EFF` | `github.com/IBM/fp-go/v2/effect` |
|
||||
| `P` | `github.com/IBM/fp-go/v2/pair` |
|
||||
| `T` | `github.com/IBM/fp-go/v2/tuple` |
|
||||
| `N` | `github.com/IBM/fp-go/v2/number` |
|
||||
| `S` | `github.com/IBM/fp-go/v2/string` |
|
||||
| `B` | `github.com/IBM/fp-go/v2/boolean` |
|
||||
| `L` | `github.com/IBM/fp-go/v2/optics/lens` |
|
||||
| `PR` | `github.com/IBM/fp-go/v2/optics/prism` |
|
||||
|
||||
**Idiomatic variants** (tuple-based, zero-alloc):
|
||||
|
||||
| Alias | Package |
|
||||
|-------|---------|
|
||||
| `IR` | `github.com/IBM/fp-go/v2/idiomatic/result` |
|
||||
| `IO_` | `github.com/IBM/fp-go/v2/idiomatic/option` |
|
||||
| `IIR` | `github.com/IBM/fp-go/v2/idiomatic/ioresult` |
|
||||
| `IRR` | `github.com/IBM/fp-go/v2/idiomatic/context/readerresult` |
|
||||
| `IRO` | `github.com/IBM/fp-go/v2/idiomatic/readerioresult` |
|
||||
|
||||
## Monad Selection
|
||||
|
||||
- **Pure value** -- use the value directly, no wrapper needed
|
||||
- **May be absent** -- `Option[A]` (struct-based) or `(A, bool)` (idiomatic)
|
||||
- **Can fail with `error`** -- `Result[A]` = `Either[error, A]`
|
||||
- Need custom error type E -- use `Either[E, A]` instead
|
||||
- **Lazy + can fail** -- `IOResult[A]` = `func() Either[error, A]`
|
||||
- Idiomatic: `func() (A, error)`
|
||||
- **Needs `context.Context` + lazy + can fail** -- `ReaderIOResult[A]` via `context/readerioresult`
|
||||
- Type: `func(context.Context) func() Either[error, A]`
|
||||
- Idiomatic: `func(context.Context) (A, error)` via `idiomatic/context/readerresult`
|
||||
- **Typed DI + context + lazy + can fail** -- `Effect[C, A]` via `effect` package
|
||||
- Type: `func(C) func(context.Context) func() Either[error, A]`
|
||||
- C is your dependency/config struct; context.Context is handled internally
|
||||
- **Performance-critical** -- prefer `idiomatic/` variants throughout
|
||||
|
||||
## Standard vs Idiomatic
|
||||
|
||||
| Aspect | Standard | Idiomatic |
|
||||
|--------|----------|-----------|
|
||||
| Representation | `Either[error, A]` struct | `(A, error)` tuple |
|
||||
| Performance | Baseline | 2-32x faster, zero allocs |
|
||||
| Custom error types | `Either[E, A]` for any E | error only |
|
||||
| Do-notation | Full support | Full support |
|
||||
| FP toolkit | Complete | Complete |
|
||||
| Go interop | Requires `Unwrap`/`Eitherize` | Native `(val, err)` |
|
||||
|
||||
**Rule of thumb**: Use idiomatic for production code and hot paths. Use standard when you need custom error types (`Either[E, A]`) or when composing with packages that use the standard types.
|
||||
|
||||
## Core Types
|
||||
|
||||
```go
|
||||
// function package
|
||||
type Void = struct{}
|
||||
var VOID Void = struct{}{}
|
||||
|
||||
// option
|
||||
type Option[A any] struct { /* Some/None */ }
|
||||
|
||||
// either
|
||||
type Either[E, A any] struct { /* Left/Right */ }
|
||||
|
||||
// result (specialized Either)
|
||||
type Result[A any] = Either[error, A]
|
||||
|
||||
// io
|
||||
type IO[A any] = func() A
|
||||
|
||||
// ioresult
|
||||
type IOResult[A any] = IO[Result[A]] // = func() Either[error, A]
|
||||
|
||||
// context/readerioresult
|
||||
type ReaderIOResult[A any] = func(context.Context) func() Either[error, A]
|
||||
|
||||
// effect
|
||||
type Effect[C, A any] = func(C) func(context.Context) func() Either[error, A]
|
||||
type Kleisli[C, A, B any] = func(A) Effect[C, B]
|
||||
|
||||
// idiomatic equivalents
|
||||
type IOResult[A any] = func() (A, error)
|
||||
type ReaderResult[A any] = func(context.Context) (A, error)
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Data-last**: Configuration/behavior params come first, data comes last. This enables partial application and pipeline composition.
|
||||
|
||||
2. **Type parameter ordering**: Non-inferrable type params come first. Example: `Ap[B, E, A]` -- B cannot be inferred, so it leads. `Map[A, B]` -- both usually inferred.
|
||||
|
||||
3. **Composition direction**:
|
||||
- `F.Flow1/2/3/.../N` -- left-to-right (use this for pipelines)
|
||||
- `Compose` -- right-to-left (mathematical convention; avoid in pipelines)
|
||||
|
||||
4. **Pipe vs Flow**:
|
||||
- `F.Pipe3(value, f1, f2, f3)` -- apply data to a pipeline immediately
|
||||
- `F.Flow3(f1, f2, f3)` -- create a reusable pipeline (returns a function)
|
||||
|
||||
5. **Arity-numbered functions**: `Pipe1` through `Pipe20`, `Flow1` through `Flow20`. Choose the number matching your operation count.
|
||||
|
||||
6. **Naming conventions**:
|
||||
- `Chain` = flatMap/bind (`A -> F[B]`, flattens)
|
||||
- `Map` = fmap (`A -> B`, lifts into context)
|
||||
- `Ap` = applicative apply (apply wrapped function to wrapped value)
|
||||
- `ChainFirst` / `Tap` = execute for side effects, keep original value
|
||||
- `ChainEitherK` = lift pure `func(A) Either[E, B]` into monadic chain
|
||||
- `Of` = pure/return (lift value into monad)
|
||||
- `Fold` = catamorphism (handle both cases)
|
||||
- `Left` / `Right` = Either constructors
|
||||
- `Some` / `None` = Option constructors
|
||||
|
||||
7. **Prefer `result` over `either`** unless you need a custom error type E. `Result[A]` = `Either[error, A]`.
|
||||
|
||||
8. **Wrapping Go functions**:
|
||||
- `result.Eitherize1(fn)` wraps `func(X) (Y, error)` into `func(X) Result[Y]`
|
||||
- `result.Eitherize2(fn)` wraps `func(X, Y) (Z, error)` into `func(X, Y) Result[Z]`
|
||||
- Variants up to `Eitherize15`
|
||||
|
||||
9. **Use `function.Void` / `function.VOID`** instead of `struct{}` / `struct{}{}`.
|
||||
|
||||
10. **Go 1.24+ required** (generic type aliases).
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pipeline with Pipe
|
||||
```go
|
||||
result := F.Pipe3(
|
||||
inputValue,
|
||||
R.Map(transform),
|
||||
R.Chain(validate),
|
||||
R.Fold(onError, onSuccess),
|
||||
)
|
||||
```
|
||||
|
||||
### Reusable pipeline with Flow
|
||||
```go
|
||||
pipeline := F.Flow3(
|
||||
R.Map(normalize),
|
||||
R.Chain(validate),
|
||||
R.Map(format),
|
||||
)
|
||||
output := pipeline(R.Of(input))
|
||||
```
|
||||
|
||||
### Wrapping Go error functions
|
||||
```go
|
||||
safeParseInt := R.Eitherize1(strconv.Atoi)
|
||||
// safeParseInt: func(string) Result[int]
|
||||
result := safeParseInt("42") // Right(42)
|
||||
```
|
||||
|
||||
### Effect with DI
|
||||
```go
|
||||
type Deps struct { DB *sql.DB }
|
||||
|
||||
fetchUser := EFF.Eitherize(func(deps Deps, ctx context.Context) (*User, error) {
|
||||
return deps.DB.QueryRowContext(ctx, "SELECT ...").Scan(...)
|
||||
})
|
||||
// fetchUser: Effect[Deps, *User]
|
||||
|
||||
// Execute:
|
||||
val, err := EFF.RunSync(EFF.Provide[*User](myDeps)(fetchUser))(ctx)
|
||||
```
|
||||
|
||||
### Effect composition
|
||||
```go
|
||||
pipeline := F.Pipe1(
|
||||
fetchUser,
|
||||
EFF.Map[Deps](func(u *User) string { return u.Name }),
|
||||
)
|
||||
```
|
||||
|
||||
### Do-notation (building up state)
|
||||
```go
|
||||
type State struct { X int; Y string }
|
||||
|
||||
result := F.Pipe3(
|
||||
R.Do(State{}),
|
||||
R.Bind(
|
||||
func(x int) func(State) State {
|
||||
return func(s State) State { s.X = x; return s }
|
||||
},
|
||||
func(s State) Result[int] { return R.Of(42) },
|
||||
),
|
||||
R.Let(
|
||||
func(y string) func(State) State {
|
||||
return func(s State) State { s.Y = y; return s }
|
||||
},
|
||||
func(s State) string { return fmt.Sprintf("val=%d", s.X) },
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Optics (Lens)
|
||||
```go
|
||||
type Person struct { Name string; Age int }
|
||||
|
||||
nameLens := L.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
name := nameLens.Get(person) // get
|
||||
updated := nameLens.Set("Bob")(person) // set (returns new Person)
|
||||
modified := L.Modify(strings.ToUpper)(nameLens)(person) // modify
|
||||
```
|
||||
|
||||
### Option handling
|
||||
```go
|
||||
result := F.Pipe3(
|
||||
O.Some(42),
|
||||
O.Map(func(x int) int { return x * 2 }),
|
||||
O.GetOrElse(F.Constant(0)),
|
||||
)
|
||||
```
|
||||
|
||||
### Idiomatic IOResult
|
||||
```go
|
||||
readFile := func() ([]byte, error) { return os.ReadFile("config.json") }
|
||||
// This IS an idiomatic IOResult[[]byte] -- just a func() ([]byte, error)
|
||||
|
||||
parsed := IIR.Map(parseConfig)(readFile)
|
||||
config, err := parsed()
|
||||
```
|
||||
|
||||
### ReaderIOResult (context-dependent IO)
|
||||
```go
|
||||
// Eitherize1 wraps func(context.Context, T0) (R, error) -> func(T0) ReaderIOResult[R]
|
||||
fetchURL := RIO.Eitherize1(func(ctx context.Context, url string) ([]byte, error) {
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil { return nil, err }
|
||||
defer resp.Body.Close()
|
||||
return iolib.ReadAll(resp.Body)
|
||||
})
|
||||
// fetchURL: func(string) ReaderIOResult[[]byte]
|
||||
result := fetchURL("https://example.com")(ctx)() // execute
|
||||
```
|
||||
|
||||
## Deeper Documentation
|
||||
|
||||
- `fp-go-cookbook.md` -- migration recipes and "how do I X in fp-go?"
|
||||
- `fp-go-core-patterns.md` -- core types, operations, and composition details
|
||||
- `fp-go-mastery.md` -- advanced FP techniques, architecture, and Effect system
|
||||
- `fp-go-full-reference.md` -- complete API inventory across all packages
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
//
|
||||
|
||||
+602
-21
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
package array
|
||||
|
||||
import "slices"
|
||||
|
||||
func Of[GA ~[]A, A any](a A) GA {
|
||||
return GA{a}
|
||||
}
|
||||
@@ -197,3 +199,9 @@ func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
}
|
||||
return ras
|
||||
}
|
||||
|
||||
func UnsafeUpdateAt[GT ~[]T, T any](as GT, i int, v T) GT {
|
||||
c := slices.Clone(as)
|
||||
c[i] = v
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+92
-33
@@ -16,10 +16,10 @@
|
||||
package iter
|
||||
|
||||
import (
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
)
|
||||
|
||||
// Async converts a synchronous sequence into an asynchronous buffered sequence.
|
||||
// AsyncBuf 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.
|
||||
//
|
||||
@@ -57,7 +57,7 @@ import (
|
||||
//
|
||||
// // Create an async sequence with a buffer of 10
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// async := Async(seq, 10)
|
||||
// async := AsyncBuf(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for v := range async {
|
||||
@@ -67,7 +67,7 @@ import (
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// async := Async(seq, 5)
|
||||
// async := AsyncBuf(seq, 5)
|
||||
//
|
||||
// // Stop after 3 elements - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
@@ -83,7 +83,7 @@ import (
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// seq := From(1, 2, 3)
|
||||
// async := Async(seq, 0)
|
||||
// async := AsyncBuf(seq, 0)
|
||||
//
|
||||
// // Producer and consumer are synchronized
|
||||
// for v := range async {
|
||||
@@ -95,32 +95,48 @@ import (
|
||||
// - 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
|
||||
}
|
||||
}
|
||||
}
|
||||
func AsyncBuf[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
return MergeBuf(A.Of(input), bufSize)
|
||||
}
|
||||
|
||||
// Async2 converts a synchronous key-value sequence into an asynchronous buffered sequence.
|
||||
// Async converts a synchronous sequence into an asynchronous sequence using a default buffer size.
|
||||
// This is a convenience wrapper around AsyncBuf that uses a default buffer size of 8.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - input: The source sequence to be consumed asynchronously
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns a goroutine that consumes the input sequence
|
||||
// - Elements are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - The channel is closed when the input sequence is exhausted
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// async := Async(seq)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for v := range async {
|
||||
// fmt.Println(v) // Prints: 1, 2, 3, 4, 5
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - AsyncBuf: Async with custom buffer size
|
||||
// - Async2: Asynchronous sequence for key-value sequences
|
||||
// - Merge: Merges multiple sequences concurrently
|
||||
func Async[T any](input Seq[T]) Seq[T] {
|
||||
return AsyncBuf(input, defaultBufferSize)
|
||||
}
|
||||
|
||||
// Async2Buf 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.
|
||||
//
|
||||
@@ -158,7 +174,7 @@ func Async[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
//
|
||||
// // 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)
|
||||
// async := Async2Buf(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for k, v := range async {
|
||||
@@ -172,7 +188,7 @@ func Async[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
// async := Async2(seq, 5)
|
||||
// async := Async2Buf(seq, 5)
|
||||
//
|
||||
// // Stop after 2 pairs - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
@@ -190,6 +206,49 @@ func Async[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
// - 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))
|
||||
func Async2Buf[K, V any](input Seq2[K, V], bufSize int) Seq2[K, V] {
|
||||
return FromSeqPair(AsyncBuf(ToSeqPair(input), bufSize))
|
||||
}
|
||||
|
||||
// Async2 converts a synchronous key-value sequence into an asynchronous sequence using a default buffer size.
|
||||
// This is a convenience wrapper around Async2Buf that uses a default buffer size of 8.
|
||||
// It's the Seq2 variant of Async, providing the same asynchronous behavior for key-value sequences.
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// Returns:
|
||||
// - Seq2[K, V]: A new key-value sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - 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 with goroutine cleanup
|
||||
// - The channel is closed when the input sequence is exhausted
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// async := Async2(seq)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // 1: a
|
||||
// // 2: b
|
||||
// // 3: c
|
||||
//
|
||||
// See Also:
|
||||
// - Async2Buf: Async2 with custom buffer size
|
||||
// - Async: Asynchronous sequence for single-value sequences
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
func Async2[K, V any](input Seq2[K, V]) Seq2[K, V] {
|
||||
return Async2Buf(input, defaultBufferSize)
|
||||
}
|
||||
|
||||
@@ -30,21 +30,21 @@ import (
|
||||
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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(seq, 1)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
@@ -55,7 +55,7 @@ func TestAsync_Success(t *testing.T) {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
async := Async(seq, 20)
|
||||
async := AsyncBuf(seq, 20)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, data, result)
|
||||
})
|
||||
@@ -65,42 +65,42 @@ func TestAsync_Success(t *testing.T) {
|
||||
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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
@@ -110,21 +110,21 @@ func TestAsync_BufferSizes(t *testing.T) {
|
||||
func TestAsync_Empty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := Async(seq, 5)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(seq, 0)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
@@ -145,7 +145,7 @@ func TestAsync_EarlyTermination(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
async := Async(seq, 10)
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
// Consume only 5 elements
|
||||
count := 0
|
||||
@@ -168,7 +168,7 @@ func TestAsync_EarlyTermination(t *testing.T) {
|
||||
|
||||
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)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
@@ -183,7 +183,7 @@ func TestAsync_EarlyTermination(t *testing.T) {
|
||||
|
||||
t.Run("early termination with unbuffered channel", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 0)
|
||||
async := AsyncBuf(seq, 0)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
@@ -210,7 +210,7 @@ func TestAsync_WithComplexTypes(t *testing.T) {
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
async := Async(seq, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
@@ -225,14 +225,14 @@ func TestAsync_WithComplexTypes(t *testing.T) {
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
async := Async(seq, 3)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(seq, 2)
|
||||
result := toSlice(async)
|
||||
expected := [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||||
assert.Equal(t, expected, result)
|
||||
@@ -243,7 +243,7 @@ func TestAsync_WithComplexTypes(t *testing.T) {
|
||||
m2 := map[string]int{"b": 2}
|
||||
m3 := map[string]int{"c": 3}
|
||||
seq := From(m1, m2, m3)
|
||||
async := Async(seq, 3)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []map[string]int{m1, m2, m3}, result)
|
||||
})
|
||||
@@ -254,14 +254,14 @@ 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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
@@ -270,14 +270,14 @@ func TestAsync_WithChainedOperations(t *testing.T) {
|
||||
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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
@@ -288,15 +288,15 @@ func TestAsync_WithChainedOperations(t *testing.T) {
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
async := Async(chained, 10)
|
||||
async := AsyncBuf(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)
|
||||
async1 := AsyncBuf(seq, 3)
|
||||
async2 := AsyncBuf(async1, 2)
|
||||
result := toSlice(async2)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
@@ -314,26 +314,26 @@ func TestAsync_Concurrency(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async := Async(seq, 10)
|
||||
|
||||
|
||||
async := AsyncBuf(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)
|
||||
|
||||
async := AsyncBuf(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())
|
||||
})
|
||||
}
|
||||
@@ -342,28 +342,28 @@ func TestAsync_Concurrency(t *testing.T) {
|
||||
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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(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)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
@@ -374,7 +374,7 @@ 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)
|
||||
async := AsyncBuf(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
@@ -388,7 +388,7 @@ func BenchmarkAsync_LargeSequence(b *testing.B) {
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 100)
|
||||
async := AsyncBuf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
@@ -398,7 +398,7 @@ 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)
|
||||
async := AsyncBuf(seq, 1)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
@@ -408,7 +408,7 @@ 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)
|
||||
async := AsyncBuf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ 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)
|
||||
async := AsyncBuf(seq, 0)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
@@ -428,7 +428,7 @@ 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)
|
||||
async := AsyncBuf(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
for range mapped {
|
||||
}
|
||||
@@ -439,7 +439,7 @@ 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)
|
||||
async := AsyncBuf(seq, 5)
|
||||
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
for range filtered {
|
||||
}
|
||||
@@ -449,7 +449,7 @@ func BenchmarkAsync_WithFilter(b *testing.B) {
|
||||
// Example tests for documentation
|
||||
func ExampleAsync() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 10)
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
@@ -459,7 +459,7 @@ func ExampleAsync() {
|
||||
|
||||
func ExampleAsync_unbuffered() {
|
||||
seq := From(1, 2, 3)
|
||||
async := Async(seq, 0)
|
||||
async := AsyncBuf(seq, 0)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
@@ -469,7 +469,7 @@ func ExampleAsync_unbuffered() {
|
||||
|
||||
func ExampleAsync_earlyTermination() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := Async(seq, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for v := range async {
|
||||
@@ -484,7 +484,7 @@ func ExampleAsync_earlyTermination() {
|
||||
|
||||
func ExampleAsync_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
doubled := MonadMap(async, N.Mul(2))
|
||||
|
||||
for v := range doubled {
|
||||
@@ -495,7 +495,7 @@ func ExampleAsync_withMap() {
|
||||
|
||||
func ExampleAsync_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := Async(seq, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
evens := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
|
||||
for v := range evens {
|
||||
@@ -508,7 +508,7 @@ func ExampleAsync_withFilter() {
|
||||
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)
|
||||
async := Async2Buf(seq, 10)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
@@ -516,22 +516,22 @@ func TestAsync2_Success(t *testing.T) {
|
||||
|
||||
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)
|
||||
|
||||
async := Async2Buf(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)
|
||||
async := Async2Buf(seq, 1)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
@@ -544,7 +544,7 @@ func TestAsync2_Success(t *testing.T) {
|
||||
values[i] = fmt.Sprintf("val%d", i)
|
||||
}
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
async := Async2(seq, 20)
|
||||
async := Async2Buf(seq, 20)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, 100, len(result))
|
||||
for i := range 100 {
|
||||
@@ -557,7 +557,7 @@ func TestAsync2_Success(t *testing.T) {
|
||||
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)
|
||||
async := Async2Buf(seq, 0)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
@@ -565,7 +565,7 @@ func TestAsync2_BufferSizes(t *testing.T) {
|
||||
|
||||
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)
|
||||
async := Async2Buf(seq, -5)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
@@ -573,7 +573,7 @@ func TestAsync2_BufferSizes(t *testing.T) {
|
||||
|
||||
t.Run("large buffer", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2(seq, 100)
|
||||
async := Async2Buf(seq, 100)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
@@ -584,7 +584,7 @@ func TestAsync2_BufferSizes(t *testing.T) {
|
||||
func TestAsync2_Empty(t *testing.T) {
|
||||
t.Run("empty Seq2", func(t *testing.T) {
|
||||
seq := MonadZip(Empty[int](), Empty[string]())
|
||||
async := Async2(seq, 5)
|
||||
async := Async2Buf(seq, 5)
|
||||
result := toMap(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
@@ -594,8 +594,8 @@ func TestAsync2_Empty(t *testing.T) {
|
||||
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)
|
||||
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for range async {
|
||||
count++
|
||||
@@ -603,7 +603,7 @@ func TestAsync2_EarlyTermination(t *testing.T) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
@@ -613,7 +613,7 @@ 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)
|
||||
async := Async2Buf(mapped, 5)
|
||||
result := toMap(async)
|
||||
expected := map[int]int{1: 11, 2: 22, 3: 33}
|
||||
assert.Equal(t, expected, result)
|
||||
@@ -626,7 +626,7 @@ func TestToSeqPair_Success(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]))
|
||||
@@ -640,7 +640,7 @@ func TestToSeqPair_Success(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))
|
||||
@@ -654,7 +654,7 @@ func TestToSeqPair_Success(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]))
|
||||
@@ -685,7 +685,7 @@ func TestToSeqPair_WithComplexTypes(t *testing.T) {
|
||||
)
|
||||
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]))
|
||||
@@ -702,7 +702,7 @@ func TestFromSeqPair_Success(t *testing.T) {
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
@@ -714,14 +714,14 @@ func TestFromSeqPair_Success(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
@@ -730,7 +730,7 @@ func TestFromSeqPair_Success(t *testing.T) {
|
||||
pairs := From(pair.MakePair("key", 42))
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
}
|
||||
@@ -760,7 +760,7 @@ func TestFromSeqPair_WithComplexTypes(t *testing.T) {
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
|
||||
expected := map[int]Person{
|
||||
1: {"Alice", 30},
|
||||
2: {"Bob", 25},
|
||||
@@ -777,7 +777,7 @@ func TestRoundTrip(t *testing.T) {
|
||||
pairs := ToSeqPair(original)
|
||||
restored := FromSeqPair(pairs)
|
||||
result := toMap(restored)
|
||||
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
@@ -791,7 +791,7 @@ func TestRoundTrip(t *testing.T) {
|
||||
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]))
|
||||
@@ -803,7 +803,7 @@ 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)
|
||||
async := Async2Buf(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
@@ -819,7 +819,7 @@ func BenchmarkAsync2_LargeSequence(b *testing.B) {
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async2(seq, 100)
|
||||
async := Async2Buf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
@@ -856,7 +856,7 @@ func BenchmarkRoundTrip(b *testing.B) {
|
||||
// Example tests for Async2
|
||||
func ExampleAsync2() {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2(seq, 10)
|
||||
async := Async2Buf(seq, 10)
|
||||
|
||||
for k, v := range async {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
@@ -869,7 +869,7 @@ func ExampleAsync2() {
|
||||
|
||||
func ExampleAsync2_earlyTermination() {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
async := Async2(seq, 5)
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for k, v := range async {
|
||||
@@ -901,5 +901,3 @@ func ExampleFromSeqPair() {
|
||||
// 2: b
|
||||
// 3: c
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
@@ -471,8 +495,34 @@ func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// ConcatMap is an alias for Chain that emphasizes sequential concatenation.
|
||||
// It maps each element to a sequence and concatenates the results in order.
|
||||
//
|
||||
// Unlike concurrent operations, ConcatMap preserves the order of elements:
|
||||
// it fully processes each input element (yielding all elements from f(a))
|
||||
// before moving to the next input element.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// result := ConcatMap(func(x int) Seq[int] {
|
||||
// return From(x, x*10)
|
||||
// })(seq)
|
||||
// // yields: 1, 10, 2, 20, 3, 30 (order preserved)
|
||||
//
|
||||
//go:inline
|
||||
func ConcatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// 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:
|
||||
@@ -486,9 +536,22 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
return MonadChain(mma, F.Identity[Seq[A]])
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ConcatAll[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
return Flatten(mma)
|
||||
}
|
||||
|
||||
// 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 +640,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 +881,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 +909,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 +930,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 +952,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:
|
||||
@@ -1079,3 +1176,61 @@ func FromSeqPair[A, B any](as Seq[Pair[A, B]]) Seq2[A, B] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 })
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
// 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 (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBufferSize = 8
|
||||
)
|
||||
|
||||
// MergeBuf merges multiple sequences concurrently into a single sequence.
|
||||
// It spawns a goroutine for each input sequence and merges their elements through
|
||||
// a buffered channel, allowing concurrent production from all sources. The output
|
||||
// order is non-deterministic and depends on the timing of concurrent producers.
|
||||
//
|
||||
// This function is useful for combining results from multiple concurrent operations,
|
||||
// processing data from multiple sources in parallel, or implementing fan-in patterns
|
||||
// where multiple producers feed into a single consumer.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - iterables: A slice of sequences to merge. If empty, returns an empty sequence.
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
// A buffer of 0 creates an unbuffered channel requiring synchronization.
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from all input sequences in non-deterministic order
|
||||
//
|
||||
// Behavior:
|
||||
// - Spawns one goroutine per input sequence to produce elements concurrently
|
||||
// - Elements from different sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// all producer goroutines are signaled to stop and cleaned up
|
||||
// - The output channel is closed when all input sequences are exhausted
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // MergeBuf three sequences concurrently
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// seq3 := From(7, 8, 9)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2, seq3}, 10)
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// seq1 := From(1, 2, 3, 4, 5)
|
||||
// seq2 := From(6, 7, 8, 9, 10)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2}, 5)
|
||||
//
|
||||
// // Stop after 3 elements - all producer goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 3 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Unbuffered Channel:
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2}, 0)
|
||||
//
|
||||
// // Producers and consumer are synchronized
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
// - From: Creates a sequence from values
|
||||
// - MonadChain: Sequentially chains sequences (deterministic order)
|
||||
func MergeBuf[T any](iterables []Seq[T], bufSize int) Seq[T] {
|
||||
return F.Pipe2(
|
||||
iterables,
|
||||
slices.Values,
|
||||
MergeAll[T](bufSize),
|
||||
)
|
||||
}
|
||||
|
||||
// Merge merges multiple sequences concurrently into a single sequence using a default buffer size.
|
||||
// This is a convenience wrapper around MergeBuf that uses a default buffer size of 8.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - iterables: A slice of sequences to merge. If empty, returns an empty sequence.
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from all input sequences in non-deterministic order
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns one goroutine per input sequence to produce elements concurrently
|
||||
// - Elements from different sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// seq3 := From(7, 8, 9)
|
||||
// merged := Merge([]Seq[int]{seq1, seq2, seq3})
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - MergeBuf: Merge with custom buffer size
|
||||
// - MergeAll: Merges a sequence of sequences
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func Merge[T any](iterables []Seq[T]) Seq[T] {
|
||||
return MergeBuf(iterables, defaultBufferSize)
|
||||
}
|
||||
|
||||
// MergeMonoid creates a Monoid for merging sequences concurrently.
|
||||
// The monoid combines two sequences by merging them concurrently with the specified
|
||||
// buffer size, and uses an empty sequence as the identity element.
|
||||
//
|
||||
// A Monoid is an algebraic structure with an associative binary operation (concat)
|
||||
// and an identity element (empty). For sequences, the concat operation merges two
|
||||
// sequences concurrently, and the identity is an empty sequence.
|
||||
//
|
||||
// This is useful for functional composition patterns where you need to combine
|
||||
// multiple sequences using monoid operations like Reduce, FoldMap, or when working
|
||||
// with monadic operations that require a monoid instance.
|
||||
//
|
||||
// Marble Diagram (Concurrent Merging):
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Merge: --1-4-2-5-3-6--|
|
||||
// (non-deterministic order)
|
||||
//
|
||||
// Marble Diagram (vs ConcatMonoid):
|
||||
//
|
||||
// MergeMonoid (concurrent):
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1-4-2-5-3-6--|
|
||||
// (elements interleaved)
|
||||
//
|
||||
// ConcatMonoid (sequential):
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1--2--3--4--5--6--|
|
||||
// (deterministic order)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - bufSize: The buffer size for the internal channel used during merging.
|
||||
// This buffer size will be used for all merge operations performed by the monoid.
|
||||
// Negative values are treated as 0 (unbuffered).
|
||||
//
|
||||
// Returns:
|
||||
// - Monoid[Seq[T]]: A monoid instance with:
|
||||
// - Concat: Merges two sequences concurrently using Merge
|
||||
// - Empty: Returns an empty sequence
|
||||
//
|
||||
// Properties:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(a, b), c) = concat(a, concat(b, c))
|
||||
// Note: Due to concurrent execution, element order may vary between equivalent expressions
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a monoid for merging integer sequences
|
||||
// monoid := MergeMonoid[int](10)
|
||||
//
|
||||
// // Use with Reduce to merge multiple sequences
|
||||
// sequences := []Seq[int]{
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// }
|
||||
// merged := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty)
|
||||
// // merged contains all elements from all sequences (order non-deterministic)
|
||||
//
|
||||
// Example with Empty Identity:
|
||||
//
|
||||
// monoid := MergeMonoid[int](5)
|
||||
// seq := From(1, 2, 3)
|
||||
//
|
||||
// // Merging with empty is identity
|
||||
// result1 := monoid.Concat(monoid.Empty, seq) // same as seq
|
||||
// result2 := monoid.Concat(seq, monoid.Empty) // same as seq
|
||||
//
|
||||
// Example with FoldMap:
|
||||
//
|
||||
// // Convert each number to a sequence and merge all results
|
||||
// monoid := MergeMonoid[int](10)
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, monoid)
|
||||
// // result contains: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
//
|
||||
// See Also:
|
||||
// - Merge: The underlying merge function
|
||||
// - MergeAll: Merges multiple sequences at once
|
||||
// - Empty: Creates an empty sequence
|
||||
func MergeMonoid[T any](bufSize int) M.Monoid[Seq[T]] {
|
||||
return M.MakeMonoid(
|
||||
func(l, r Seq[T]) Seq[T] {
|
||||
return MergeBuf(A.From(l, r), bufSize)
|
||||
},
|
||||
Empty[T](),
|
||||
)
|
||||
}
|
||||
|
||||
// MergeAll creates an operator that flattens and merges a sequence of sequences concurrently.
|
||||
// It takes a sequence of sequences (Seq[Seq[T]]) and produces a single flat sequence (Seq[T])
|
||||
// by spawning a goroutine for each inner sequence as it arrives, merging all their elements
|
||||
// through a buffered channel. This enables dynamic concurrent processing where inner sequences
|
||||
// can be produced and consumed concurrently.
|
||||
//
|
||||
// Unlike Merge which takes a pre-defined slice of sequences, MergeAll processes sequences
|
||||
// dynamically as they are produced by the outer sequence. This makes it ideal for scenarios
|
||||
// where the number of sequences isn't known upfront or where sequences are generated on-the-fly.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the inner sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[Seq[T], T]: A function that takes a sequence of sequences and returns a flat sequence
|
||||
//
|
||||
// Behavior:
|
||||
// - Spawns one goroutine for the outer sequence to iterate and spawn inner producers
|
||||
// - Spawns one goroutine per inner sequence as it arrives from the outer sequence
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating, all goroutines are cleaned up
|
||||
// - The output channel is closed when both the outer sequence and all inner sequences are exhausted
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a sequence of sequences dynamically
|
||||
// outer := From(
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// )
|
||||
// mergeAll := MergeAll[int](10)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// Example with Dynamic Generation:
|
||||
//
|
||||
// // Generate sequences on-the-fly
|
||||
// outer := Map(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// })(From(1, 2, 3))
|
||||
// mergeAll := MergeAll[int](10)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// outer := From(
|
||||
// From(1, 2, 3, 4, 5),
|
||||
// From(6, 7, 8, 9, 10),
|
||||
// From(11, 12, 13, 14, 15),
|
||||
// )
|
||||
// mergeAll := MergeAll[int](5)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Stop after 5 elements - all goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 5 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Chain:
|
||||
//
|
||||
// // Use with Chain to flatten nested sequences
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := Chain(func(n int) Seq[int] {
|
||||
// return From(n, n*10)
|
||||
// })(numbers)
|
||||
// // This is equivalent to: MergeAll[int](0)(Map(...)(numbers))
|
||||
//
|
||||
// See Also:
|
||||
// - Merge: Merges a pre-defined slice of sequences
|
||||
// - Chain: Sequentially flattens sequences (deterministic order)
|
||||
// - Flatten: Flattens nested sequences sequentially
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func MergeAll[T any](bufSize int) Operator[Seq[T], T] {
|
||||
buf := N.Max(bufSize, 0)
|
||||
|
||||
return func(s Seq[Seq[T]]) Seq[T] {
|
||||
|
||||
return func(yield func(T) bool) {
|
||||
|
||||
ch := make(chan T, buf)
|
||||
done := make(chan Void)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Outer producer: iterates the outer Seq and spawns an inner
|
||||
// goroutine for each inner Seq it emits.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s(func(inner Seq[T]) bool {
|
||||
select {
|
||||
case <-done:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(seq Seq[T]) {
|
||||
defer wg.Done()
|
||||
seq(func(v T) bool {
|
||||
select {
|
||||
case ch <- v:
|
||||
return true
|
||||
case <-done:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}(inner)
|
||||
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
// Close ch once the outer producer and all inner producers finish.
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
// On exit, signal cancellation and drain so no producer blocks
|
||||
// forever on `ch <- v`.
|
||||
defer func() {
|
||||
close(done)
|
||||
for range ch {
|
||||
}
|
||||
}()
|
||||
|
||||
for v := range ch {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MergeMapBuf applies a function that returns a sequence to each element and merges the results concurrently.
|
||||
// This is the concurrent version of Chain (flatMap), where each mapped sequence is processed in parallel
|
||||
// rather than sequentially. It combines Map and MergeAll into a single operation.
|
||||
//
|
||||
// Unlike Chain which processes sequences sequentially (deterministic order), MergeMapBuf spawns a goroutine
|
||||
// for each mapped sequence and merges their elements concurrently through a buffered channel. This makes
|
||||
// it ideal for I/O-bound operations, parallel data processing, or when the order of results doesn't matter.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input sequence
|
||||
// - B: The type of elements in the output sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms each input element into a sequence of output elements
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that takes a sequence of A and returns a flat sequence of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Applies f to each element in the input sequence to produce inner sequences
|
||||
// - Spawns one goroutine per inner sequence to produce elements concurrently
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating, all goroutines are cleaned up
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Comparison with Chain:
|
||||
// - Chain: Sequential processing, deterministic order, no concurrency overhead
|
||||
// - MergeMapBuf: Concurrent processing, non-deterministic order, better for I/O-bound tasks
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Expand each number into a sequence concurrently
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, 10)
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// Example with I/O Operations:
|
||||
//
|
||||
// // Fetch data concurrently for each ID
|
||||
// fetchData := MergeMapBuf(func(id int) Seq[string] {
|
||||
// // Simulate I/O operation
|
||||
// data := fetchFromAPI(id)
|
||||
// return From(data...)
|
||||
// }, 20)
|
||||
// ids := From(1, 2, 3, 4, 5)
|
||||
// results := fetchData(ids)
|
||||
//
|
||||
// // All fetches happen concurrently
|
||||
// for data := range results {
|
||||
// fmt.Println(data)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, 5)
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Stop after 5 elements - all goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 5 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Unbuffered Channel:
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*2)
|
||||
// }, 0)
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Producers and consumer are synchronized
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - Chain: Sequential version (deterministic order)
|
||||
// - MergeAll: Merges pre-existing sequences concurrently
|
||||
// - Map: Transforms elements without flattening
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func MergeMapBuf[A, B any](f func(A) Seq[B], bufSize int) Operator[A, B] {
|
||||
return F.Flow2(
|
||||
Map(f),
|
||||
MergeAll[B](bufSize),
|
||||
)
|
||||
}
|
||||
|
||||
// MergeMap applies a function that returns a sequence to each element and merges the results concurrently using a default buffer size.
|
||||
// This is a convenience wrapper around MergeMapBuf that uses a default buffer size of 8.
|
||||
// It's the concurrent version of Chain (flatMap), where each mapped sequence is processed in parallel.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input sequence
|
||||
// - B: The type of elements in the output sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms each input element into a sequence of output elements
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that takes a sequence of A and returns a flat sequence of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Applies f to each element in the input sequence to produce inner sequences
|
||||
// - Spawns one goroutine per inner sequence to produce elements concurrently
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Comparison with Chain:
|
||||
// - Chain: Sequential processing, deterministic order, no concurrency overhead
|
||||
// - MergeMap: Concurrent processing, non-deterministic order, better for I/O-bound tasks
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Expand each number into a sequence concurrently
|
||||
// expand := MergeMap(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// })
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - MergeMapBuf: MergeMap with custom buffer size
|
||||
// - Chain: Sequential version (deterministic order)
|
||||
// - MergeAll: Merges pre-existing sequences concurrently
|
||||
// - Map: Transforms elements without flattening
|
||||
func MergeMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return MergeMapBuf(f, defaultBufferSize)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+134
-1
@@ -21,7 +21,13 @@ import (
|
||||
)
|
||||
|
||||
// Monoid returns a Monoid instance for Seq[T].
|
||||
// The monoid's concat operation concatenates sequences, and the empty value is an empty sequence.
|
||||
// The monoid's concat operation concatenates sequences sequentially, and the empty value is an empty sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Concat: --1--2--3--4--5--6--|
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -35,3 +41,130 @@ import (
|
||||
func Monoid[T any]() M.Monoid[Seq[T]] {
|
||||
return G.Monoid[Seq[T]]()
|
||||
}
|
||||
|
||||
// ConcatMonoid returns a Monoid instance for Seq[T] that concatenates sequences sequentially.
|
||||
// This is an alias for Monoid that makes the sequential concatenation behavior explicit.
|
||||
//
|
||||
// A Monoid is an algebraic structure with an associative binary operation (concat)
|
||||
// and an identity element (empty). For sequences, the concat operation appends one
|
||||
// sequence after another in deterministic order, and the identity is an empty sequence.
|
||||
//
|
||||
// This monoid is useful for functional composition patterns where you need to combine
|
||||
// multiple sequences sequentially using monoid operations like Reduce, FoldMap, or when
|
||||
// working with monadic operations that require a monoid instance.
|
||||
//
|
||||
// Marble Diagram (Sequential Concatenation):
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Concat: --1--2--3--4--5--6--|
|
||||
// (deterministic order)
|
||||
//
|
||||
// Marble Diagram (vs MergeMonoid):
|
||||
//
|
||||
// ConcatMonoid:
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1--2--3--4--5--6--|
|
||||
//
|
||||
// MergeMonoid:
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1-4-2-5-3-6--|
|
||||
// (non-deterministic)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Returns:
|
||||
// - Monoid[Seq[T]]: A monoid instance with:
|
||||
// - Concat: Appends sequences sequentially (deterministic order)
|
||||
// - Empty: Returns an empty sequence
|
||||
//
|
||||
// Properties:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(a, b), c) = concat(a, concat(b, c))
|
||||
// - Deterministic: Elements always appear in the order of the input sequences
|
||||
//
|
||||
// Comparison with MergeMonoid:
|
||||
//
|
||||
// ConcatMonoid and MergeMonoid serve different purposes:
|
||||
//
|
||||
// - ConcatMonoid: Sequential concatenation
|
||||
//
|
||||
// - Order: Deterministic - elements from first sequence, then second, etc.
|
||||
//
|
||||
// - Concurrency: No concurrency - sequences are processed one after another
|
||||
//
|
||||
// - Performance: Lower overhead, no goroutines or channels
|
||||
//
|
||||
// - Use when: Order matters, no I/O operations, or simplicity is preferred
|
||||
//
|
||||
// - MergeMonoid: Concurrent merging
|
||||
//
|
||||
// - Order: Non-deterministic - elements interleaved based on timing
|
||||
//
|
||||
// - Concurrency: Spawns goroutines for each sequence
|
||||
//
|
||||
// - Performance: Better for I/O-bound operations, higher overhead for CPU-bound
|
||||
//
|
||||
// - Use when: Order doesn't matter, parallel I/O, or concurrent processing needed
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a monoid for concatenating integer sequences
|
||||
// monoid := ConcatMonoid[int]()
|
||||
//
|
||||
// // Use with Reduce to concatenate multiple sequences
|
||||
// sequences := []Seq[int]{
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// }
|
||||
// concatenated := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty)
|
||||
// // yields: 1, 2, 3, 4, 5, 6, 7, 8, 9 (deterministic order)
|
||||
//
|
||||
// Example with Empty Identity:
|
||||
//
|
||||
// monoid := ConcatMonoid[int]()
|
||||
// seq := From(1, 2, 3)
|
||||
//
|
||||
// // Concatenating with empty is identity
|
||||
// result1 := monoid.Concat(monoid.Empty, seq) // same as seq
|
||||
// result2 := monoid.Concat(seq, monoid.Empty) // same as seq
|
||||
//
|
||||
// Example with FoldMap:
|
||||
//
|
||||
// // Convert each number to a sequence and concatenate all results
|
||||
// monoid := ConcatMonoid[int]()
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, monoid)
|
||||
// // yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (deterministic order)
|
||||
//
|
||||
// Example Comparing ConcatMonoid vs MergeMonoid:
|
||||
//
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
//
|
||||
// // ConcatMonoid: Sequential, deterministic
|
||||
// concatMonoid := ConcatMonoid[int]()
|
||||
// concat := concatMonoid.Concat(seq1, seq2)
|
||||
// // Always yields: 1, 2, 3, 4, 5, 6
|
||||
//
|
||||
// // MergeMonoid: Concurrent, non-deterministic
|
||||
// mergeMonoid := MergeMonoid[int](10)
|
||||
// merged := mergeMonoid.Concat(seq1, seq2)
|
||||
// // May yield: 1, 4, 2, 5, 3, 6 (order varies)
|
||||
//
|
||||
// See Also:
|
||||
// - Monoid: The base monoid function (alias)
|
||||
// - MergeMonoid: Concurrent merging monoid
|
||||
// - MonadChain: Sequential flattening of sequences
|
||||
// - Empty: Creates an empty sequence
|
||||
//
|
||||
//go:inline
|
||||
func ConcatMonoid[T any]() M.Monoid[Seq[T]] {
|
||||
return Monoid[T]()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConcatMonoid_Identity(t *testing.T) {
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
result := monoid.Concat(monoid.Empty(), seq)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
result := monoid.Concat(seq, monoid.Empty())
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_Associativity(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2)
|
||||
seq2 := From(3, 4)
|
||||
seq3 := From(5, 6)
|
||||
|
||||
// (a + b) + c
|
||||
left := monoid.Concat(monoid.Concat(seq1, seq2), seq3)
|
||||
leftResult := slices.Collect(left)
|
||||
|
||||
// a + (b + c)
|
||||
right := monoid.Concat(seq1, monoid.Concat(seq2, seq3))
|
||||
rightResult := slices.Collect(right)
|
||||
|
||||
assert.Equal(t, leftResult, rightResult)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, leftResult)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_DeterministicOrder(t *testing.T) {
|
||||
t.Run("concatenates in deterministic order", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
seq3 := From(7, 8, 9)
|
||||
|
||||
result := monoid.Concat(monoid.Concat(seq1, seq2), seq3)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Order is always deterministic
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, collected)
|
||||
})
|
||||
|
||||
t.Run("multiple runs produce same order", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
// Run multiple times
|
||||
results := make([][]int, 5)
|
||||
for i := range 5 {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
results[i] = slices.Collect(result)
|
||||
}
|
||||
|
||||
// All results should be identical
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
for i, result := range results {
|
||||
assert.Equal(t, expected, result, "run %d should match", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithReduce(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
}
|
||||
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, collected)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithFoldMap(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
numbers := From(1, 2, 3)
|
||||
|
||||
result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
return From(n, n*10, n*100)
|
||||
}, monoid)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Deterministic order: each number's expansion in sequence
|
||||
assert.Equal(t, []int{1, 10, 100, 2, 20, 200, 3, 30, 300}, collected)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_ComparisonWithMergeMonoid(t *testing.T) {
|
||||
t.Run("ConcatMonoid is deterministic", func(t *testing.T) {
|
||||
concatMonoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := concatMonoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Always the same order
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, collected)
|
||||
})
|
||||
|
||||
t.Run("MergeMonoid may be non-deterministic", func(t *testing.T) {
|
||||
mergeMonoid := MergeMonoid[int](10)
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := mergeMonoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Contains all elements but order may vary
|
||||
assert.ElementsMatch(t, []int{1, 2, 3, 4, 5, 6}, collected)
|
||||
// Note: We can't assert exact order as it's non-deterministic
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_EmptySequences(t *testing.T) {
|
||||
t.Run("concatenating empty sequences", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
empty1 := Empty[int]()
|
||||
empty2 := Empty[int]()
|
||||
|
||||
result := monoid.Concat(empty1, empty2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("concatenating with empty in middle", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2)
|
||||
empty := Empty[int]()
|
||||
seq2 := From(3, 4)
|
||||
|
||||
result := monoid.Concat(monoid.Concat(seq1, empty), seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
monoid := ConcatMonoid[Person]()
|
||||
seq1 := From(Person{"Alice", 30}, Person{"Bob", 25})
|
||||
seq2 := From(Person{"Charlie", 35}, Person{"Diana", 28})
|
||||
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Charlie", 35},
|
||||
{"Diana", 28},
|
||||
}
|
||||
assert.Equal(t, expected, collected)
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_TwoSequences(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3, 4, 5)
|
||||
seq2 := From(6, 7, 8, 9, 10)
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_Reduce(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
From(10, 11, 12),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_VsMergeMonoid(b *testing.B) {
|
||||
seq1 := From(1, 2, 3, 4, 5)
|
||||
seq2 := From(6, 7, 8, 9, 10)
|
||||
|
||||
b.Run("ConcatMonoid", func(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MergeMonoid", func(b *testing.B) {
|
||||
monoid := MergeMonoid[int](10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_identity() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
// Left identity
|
||||
result1 := monoid.Concat(monoid.Empty(), seq)
|
||||
for v := range result1 {
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
// Right identity
|
||||
result2 := monoid.Concat(seq, monoid.Empty())
|
||||
for v := range result2 {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_reduce() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
}
|
||||
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
// 7
|
||||
// 8
|
||||
// 9
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_comparison() {
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
// ConcatMonoid: Sequential, deterministic
|
||||
concatMonoid := ConcatMonoid[int]()
|
||||
concat := concatMonoid.Concat(seq1, seq2)
|
||||
fmt.Println("ConcatMonoid (always same order):")
|
||||
for v := range concat {
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
// MergeMonoid: Concurrent, non-deterministic
|
||||
// Note: Output order may vary in actual runs
|
||||
mergeMonoid := MergeMonoid[int](10)
|
||||
merged := mergeMonoid.Concat(seq1, seq2)
|
||||
fmt.Println("\nMergeMonoid (order may vary):")
|
||||
collected := slices.Collect(merged)
|
||||
// Sort for consistent test output
|
||||
slices.Sort(collected)
|
||||
for _, v := range collected {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// ConcatMonoid (always same order):
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
//
|
||||
// MergeMonoid (order may vary):
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
)
|
||||
|
||||
// AsTraversal converts a iso to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(I.Iso[S, A]) R {
|
||||
return func(sa I.Iso[S, A]) R {
|
||||
saSet := fmap(sa.ReverseGet)
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return F.Flow3(
|
||||
sa.Get,
|
||||
f,
|
||||
saSet,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,5 @@ import (
|
||||
)
|
||||
|
||||
func AsTraversal[E, S, A any]() func(L.Lens[S, A]) T.Traversal[E, S, A] {
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.MonadMap[E, A, S])
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.Map[E, A, S])
|
||||
}
|
||||
|
||||
@@ -16,19 +16,24 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// AsTraversal converts a lens to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(L.Lens[S, A]) R {
|
||||
return func(sa L.Lens[S, A]) R {
|
||||
return func(f func(a A) HKTA) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return fmap(f(sa.Get(s)), func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
})
|
||||
return F.Pipe1(
|
||||
f(sa.Get(s)),
|
||||
fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,5 +60,5 @@ import (
|
||||
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
|
||||
// // Apply operations across all configs using the traversal
|
||||
func AsTraversal[S, A any]() func(Lens[S, A]) T.Traversal[S, A] {
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.Map[A, S])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/traversal/generic"
|
||||
)
|
||||
|
||||
// Compose composes a lens with a traversal to create a new traversal.
|
||||
//
|
||||
// This function allows you to focus deeper into a data structure by first using
|
||||
// a lens to access a field, then using a traversal to access multiple values within
|
||||
// that field. The result is a traversal that can operate on all the nested values.
|
||||
//
|
||||
// The composition follows the pattern: Lens[S, A] → Traversal[A, B] → Traversal[S, B]
|
||||
// where the lens focuses on field A within structure S, and the traversal focuses on
|
||||
// multiple B values within A.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The outer structure type
|
||||
// - A: The intermediate field type (target of the lens)
|
||||
// - B: The final focus type (targets of the traversal)
|
||||
//
|
||||
// Parameters:
|
||||
// - t: A traversal that focuses on B values within A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Traversal[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// LT "github.com/IBM/fp-go/v2/optics/lens/traversal"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// type Team struct {
|
||||
// Name string
|
||||
// Members []string
|
||||
// }
|
||||
//
|
||||
// // Lens to access the Members field
|
||||
// membersLens := lens.MakeLens(
|
||||
// func(t Team) []string { return t.Members },
|
||||
// func(t Team, m []string) Team { t.Members = m; return t },
|
||||
// )
|
||||
//
|
||||
// // Traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[string]()
|
||||
//
|
||||
// // Compose lens with traversal to access all member names
|
||||
// memberTraversal := F.Pipe1(
|
||||
// membersLens,
|
||||
// LT.Compose[Team, []string, string](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// team := Team{Name: "Engineering", Members: []string{"Alice", "Bob"}}
|
||||
// // Uppercase all member names
|
||||
// updated := memberTraversal(strings.ToUpper)(team)
|
||||
// // updated.Members: ["ALICE", "BOB"]
|
||||
//
|
||||
// See Also:
|
||||
// - Lens: A functional reference to a subpart of a data structure
|
||||
// - Traversal: A functional reference to multiple subparts
|
||||
// - traversal.Compose: Composes two traversals
|
||||
func Compose[S, A, B any](t Traversal[A, B, A, B]) func(Lens[S, A]) Traversal[S, B, S, B] {
|
||||
return G.Compose[S, A, B, S, A, B](
|
||||
I.Map,
|
||||
)(t)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
Name string
|
||||
Members []string
|
||||
}
|
||||
|
||||
type Company struct {
|
||||
Name string
|
||||
Teams []Team
|
||||
}
|
||||
|
||||
func TestCompose_Success(t *testing.T) {
|
||||
t.Run("composes lens with array traversal to modify nested values", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob", "charlie"},
|
||||
}
|
||||
|
||||
// Act - uppercase all member names
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
expected := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"ALICE", "BOB", "CHARLIE"},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal on empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal to transform numbers", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Stats struct {
|
||||
Name string
|
||||
Scores []int
|
||||
}
|
||||
|
||||
scoresLens := lens.MakeLens(
|
||||
func(s Stats) []int { return s.Scores },
|
||||
func(s Stats, scores []int) Stats {
|
||||
s.Scores = scores
|
||||
return s
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
|
||||
scoreTraversal := F.Pipe1(
|
||||
scoresLens,
|
||||
Compose[Stats, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
stats := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{10, 20, 30},
|
||||
}
|
||||
|
||||
// Act - double all scores
|
||||
result := scoreTraversal(func(n int) int { return n * 2 })(stats)
|
||||
|
||||
// Assert
|
||||
expected := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{20, 40, 60},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_Integration(t *testing.T) {
|
||||
t.Run("composes multiple lenses and traversals", func(t *testing.T) {
|
||||
// Arrange - nested structure with Company -> Teams -> Members
|
||||
teamsLens := lens.MakeLens(
|
||||
func(c Company) []Team { return c.Teams },
|
||||
func(c Company, teams []Team) Company {
|
||||
c.Teams = teams
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
// First compose: Company -> []Team -> Team
|
||||
teamArrayTraversal := AI.FromArray[Team]()
|
||||
companyToTeamTraversal := F.Pipe1(
|
||||
teamsLens,
|
||||
Compose[Company, []Team, Team](teamArrayTraversal),
|
||||
)
|
||||
|
||||
// Second compose: Team -> []string -> string
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
memberArrayTraversal := AI.FromArray[string]()
|
||||
teamToMemberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](memberArrayTraversal),
|
||||
)
|
||||
|
||||
company := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"alice", "bob"}},
|
||||
{Name: "Design", Members: []string{"charlie", "diana"}},
|
||||
},
|
||||
}
|
||||
|
||||
// Act - uppercase all members in all teams
|
||||
// First traverse to teams, then for each team traverse to members
|
||||
result := companyToTeamTraversal(func(team Team) Team {
|
||||
return teamToMemberTraversal(strings.ToUpper)(team)
|
||||
})(company)
|
||||
|
||||
// Assert
|
||||
expected := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"ALICE", "BOB"}},
|
||||
{Name: "Design", Members: []string{"CHARLIE", "DIANA"}},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_EdgeCases(t *testing.T) {
|
||||
t.Run("preserves structure name when modifying members", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert - Name should be unchanged
|
||||
assert.Equal(t, "Engineering", result.Name)
|
||||
assert.Equal(t, AR.From("ALICE"), result.Members)
|
||||
})
|
||||
|
||||
t.Run("handles identity transformation", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob"},
|
||||
}
|
||||
|
||||
// Act - apply identity function
|
||||
result := memberTraversal(F.Identity[string])(team)
|
||||
|
||||
// Assert - should be unchanged
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/generic"
|
||||
TG "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
func Compose[S, A, B, HKTS, HKTA, HKTB any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
lensTrav := G.AsTraversal[Traversal[S, A, HKTS, HKTA]](fmap)
|
||||
|
||||
return func(ab Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
return F.Flow2(
|
||||
lensTrav,
|
||||
TG.Compose[
|
||||
Traversal[A, B, HKTA, HKTB],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, B, HKTS, HKTB],
|
||||
](ab),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package array
|
||||
|
||||
import (
|
||||
OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
G "github.com/IBM/fp-go/v2/optics/optional/array/generic"
|
||||
)
|
||||
|
||||
// At creates an Optional that focuses on the element at a specific index in an array.
|
||||
//
|
||||
// This function returns an Optional that can get and set the element at the given index.
|
||||
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
|
||||
// (the array is returned unchanged). This follows the Optional laws where operations
|
||||
// on non-existent values have no effect.
|
||||
//
|
||||
// The Optional provides safe array access without panicking on invalid indices, making
|
||||
// it ideal for functional transformations where you want to modify array elements only
|
||||
// when they exist.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - An Optional that focuses on the element at the specified index
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
|
||||
// )
|
||||
//
|
||||
// numbers := []int{10, 20, 30, 40}
|
||||
//
|
||||
// // Create an optional focusing on index 1
|
||||
// second := OA.At[int](1)
|
||||
//
|
||||
// // Get the element at index 1
|
||||
// value := second.GetOption(numbers)
|
||||
// // value: option.Some(20)
|
||||
//
|
||||
// // Set the element at index 1
|
||||
// updated := second.Set(25)(numbers)
|
||||
// // updated: []int{10, 25, 30, 40}
|
||||
//
|
||||
// // Out of bounds access returns None
|
||||
// outOfBounds := OA.At[int](10)
|
||||
// value = outOfBounds.GetOption(numbers)
|
||||
// // value: option.None[int]()
|
||||
//
|
||||
// // Out of bounds set is a no-op
|
||||
// unchanged := outOfBounds.Set(99)(numbers)
|
||||
// // unchanged: []int{10, 20, 30, 40} (original array)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.Lookup: Gets an element at an index, returning an Option
|
||||
// - AR.UpdateAt: Updates an element at an index, returning an Option
|
||||
// - OP.Optional: The Optional optic type
|
||||
func At[A any](idx int) OP.Optional[[]A, A] {
|
||||
return G.At[[]A](idx)
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package array
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAt_GetOption tests the GetOption functionality
|
||||
func TestAt_GetOption(t *testing.T) {
|
||||
t.Run("returns Some for valid index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(20), result)
|
||||
})
|
||||
|
||||
t.Run("returns Some for first element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("returns Some for last element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(30), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for negative index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for out of bounds index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty array", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for nil array", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_Set tests the Set functionality
|
||||
func TestAt_Set(t *testing.T) {
|
||||
t.Run("updates element at valid index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
result := optional.Set(25)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 25, 30, 40}, result)
|
||||
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("updates first element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(5)(numbers)
|
||||
|
||||
assert.Equal(t, []int{5, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("updates last element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
result := optional.Set(35)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 20, 35}, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for negative index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for out of bounds index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for empty array", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for nil array", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw1_GetSetNoOp tests Optional Law 1: GetSet Law (No-op on None)
|
||||
// If GetOption(s) returns None, then Set(a)(s) must return s unchanged (no-op).
|
||||
func TestAt_OptionalLaw1_GetSetNoOp(t *testing.T) {
|
||||
t.Run("out of bounds index - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("negative index - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("empty array - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("nil array - set is no-op", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw2_SetGet tests Optional Law 2: SetGet Law (Get what you Set)
|
||||
// If GetOption(s) returns Some(_), then GetOption(Set(a)(s)) must return Some(a).
|
||||
func TestAt_OptionalLaw2_SetGet(t *testing.T) {
|
||||
t.Run("set then get returns the set value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
// Verify GetOption returns Some (precondition)
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
// Set a new value
|
||||
newValue := 25
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
// GetOption on updated should return Some(newValue)
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("set first element then get", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := 5
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("set last element then get", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := 35
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("multiple indices satisfy law", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40, 50}
|
||||
|
||||
for i := range 5 {
|
||||
optional := At[int](i)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := i * 100
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw3_SetSet tests Optional Law 3: SetSet Law (Last Set Wins)
|
||||
// Setting twice is the same as setting once with the final value.
|
||||
// Formally: Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
func TestAt_OptionalLaw3_SetSet(t *testing.T) {
|
||||
eqSlice := EQ.FromEquals(func(a, b []int) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range len(a) {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
t.Run("setting twice equals setting once with final value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
// Set twice: first to 25, then to 99
|
||||
setTwice := F.Pipe2(
|
||||
numbers,
|
||||
optional.Set(25),
|
||||
optional.Set(99),
|
||||
)
|
||||
|
||||
// Set once with final value
|
||||
setOnce := optional.Set(99)(numbers)
|
||||
|
||||
assert.True(t, eqSlice.Equals(setTwice, setOnce))
|
||||
})
|
||||
|
||||
t.Run("multiple sets - last one wins", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
// Set multiple times
|
||||
result := F.Pipe4(
|
||||
numbers,
|
||||
optional.Set(1),
|
||||
optional.Set(2),
|
||||
optional.Set(3),
|
||||
optional.Set(4),
|
||||
)
|
||||
|
||||
// Should equal setting once with final value
|
||||
expected := optional.Set(4)(numbers)
|
||||
|
||||
assert.True(t, eqSlice.Equals(result, expected))
|
||||
})
|
||||
|
||||
t.Run("set twice on out of bounds - both no-ops", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
// Set twice on out of bounds
|
||||
setTwice := F.Pipe2(
|
||||
numbers,
|
||||
optional.Set(25),
|
||||
optional.Set(99),
|
||||
)
|
||||
|
||||
// Set once on out of bounds
|
||||
setOnce := optional.Set(99)(numbers)
|
||||
|
||||
// Both should be no-ops, returning original
|
||||
assert.True(t, eqSlice.Equals(setTwice, numbers))
|
||||
assert.True(t, eqSlice.Equals(setOnce, numbers))
|
||||
assert.True(t, eqSlice.Equals(setTwice, setOnce))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_EdgeCases tests edge cases and boundary conditions
|
||||
func TestAt_EdgeCases(t *testing.T) {
|
||||
t.Run("single element array", func(t *testing.T) {
|
||||
numbers := []int{42}
|
||||
optional := At[int](0)
|
||||
|
||||
// Get
|
||||
assert.Equal(t, O.Some(42), optional.GetOption(numbers))
|
||||
|
||||
// Set
|
||||
updated := optional.Set(99)(numbers)
|
||||
assert.Equal(t, []int{99}, updated)
|
||||
|
||||
// Out of bounds
|
||||
outOfBounds := At[int](1)
|
||||
assert.Equal(t, O.None[int](), outOfBounds.GetOption(numbers))
|
||||
assert.Equal(t, numbers, outOfBounds.Set(99)(numbers))
|
||||
})
|
||||
|
||||
t.Run("large array", func(t *testing.T) {
|
||||
numbers := make([]int, 1000)
|
||||
for i := range 1000 {
|
||||
numbers[i] = i
|
||||
}
|
||||
|
||||
optional := At[int](500)
|
||||
|
||||
// Get
|
||||
assert.Equal(t, O.Some(500), optional.GetOption(numbers))
|
||||
|
||||
// Set
|
||||
updated := optional.Set(9999)(numbers)
|
||||
assert.Equal(t, 9999, updated[500])
|
||||
assert.Equal(t, 500, numbers[500]) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// String array
|
||||
strings := []string{"a", "b", "c"}
|
||||
strOptional := At[string](1)
|
||||
assert.Equal(t, O.Some("b"), strOptional.GetOption(strings))
|
||||
assert.Equal(t, []string{"a", "x", "c"}, strOptional.Set("x")(strings))
|
||||
|
||||
// Bool array
|
||||
bools := []bool{true, false, true}
|
||||
boolOptional := At[bool](1)
|
||||
assert.Equal(t, O.Some(false), boolOptional.GetOption(bools))
|
||||
assert.Equal(t, []bool{true, true, true}, boolOptional.Set(true)(bools))
|
||||
})
|
||||
|
||||
t.Run("preserves array capacity", func(t *testing.T) {
|
||||
numbers := make([]int, 3, 10)
|
||||
numbers[0], numbers[1], numbers[2] = 10, 20, 30
|
||||
|
||||
optional := At[int](1)
|
||||
updated := optional.Set(25)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 25, 30}, updated)
|
||||
assert.Equal(t, 3, len(updated))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_Integration tests integration scenarios
|
||||
func TestAt_Integration(t *testing.T) {
|
||||
t.Run("multiple optionals on same array", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
|
||||
first := At[int](0)
|
||||
second := At[int](1)
|
||||
third := At[int](2)
|
||||
|
||||
// Update multiple indices
|
||||
result := F.Pipe3(
|
||||
numbers,
|
||||
first.Set(1),
|
||||
second.Set(2),
|
||||
third.Set(3),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 40}, result)
|
||||
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("chaining operations", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](1)
|
||||
|
||||
// Get, verify, set, get again
|
||||
original := optional.GetOption(numbers)
|
||||
assert.Equal(t, O.Some(20), original)
|
||||
|
||||
updated := optional.Set(25)(numbers)
|
||||
newValue := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(25), newValue)
|
||||
|
||||
// Original still unchanged
|
||||
assert.Equal(t, O.Some(20), optional.GetOption(numbers))
|
||||
})
|
||||
|
||||
t.Run("conditional update based on current value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](1)
|
||||
|
||||
// Get current value and conditionally update
|
||||
result := F.Pipe1(
|
||||
optional.GetOption(numbers),
|
||||
O.Fold(
|
||||
func() []int { return numbers },
|
||||
func(current int) []int {
|
||||
if current > 15 {
|
||||
return optional.Set(current * 2)(numbers)
|
||||
}
|
||||
return numbers
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{10, 40, 30}, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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 generic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array/generic"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// At creates an Optional that focuses on the element at a specific index in an array.
|
||||
//
|
||||
// This function returns an Optional that can get and set the element at the given index.
|
||||
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
|
||||
// (the array is returned unchanged). This follows the Optional laws where operations
|
||||
// on non-existent values have no effect.
|
||||
//
|
||||
// The Optional provides safe array access without panicking on invalid indices, making
|
||||
// it ideal for functional transformations where you want to modify array elements only
|
||||
// when they exist.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - An Optional that focuses on the element at the specified index
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
|
||||
// )
|
||||
//
|
||||
// numbers := []int{10, 20, 30, 40}
|
||||
//
|
||||
// // Create an optional focusing on index 1
|
||||
// second := OA.At[int](1)
|
||||
//
|
||||
// // Get the element at index 1
|
||||
// value := second.GetOption(numbers)
|
||||
// // value: option.Some(20)
|
||||
//
|
||||
// // Set the element at index 1
|
||||
// updated := second.Set(25)(numbers)
|
||||
// // updated: []int{10, 25, 30, 40}
|
||||
//
|
||||
// // Out of bounds access returns None
|
||||
// outOfBounds := OA.At[int](10)
|
||||
// value = outOfBounds.GetOption(numbers)
|
||||
// // value: option.None[int]()
|
||||
//
|
||||
// // Out of bounds set is a no-op
|
||||
// unchanged := outOfBounds.Set(99)(numbers)
|
||||
// // unchanged: []int{10, 20, 30, 40} (original array)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.Lookup: Gets an element at an index, returning an Option
|
||||
// - AR.UpdateAt: Updates an element at an index, returning an Option
|
||||
// - OP.Optional: The Optional optic type
|
||||
func At[GA ~[]A, A any](idx int) OP.Optional[GA, A] {
|
||||
lookup := AR.Lookup[GA](idx)
|
||||
return OP.MakeOptionalCurriedWithName(
|
||||
lookup,
|
||||
func(a A) func(GA) GA {
|
||||
update := AR.UpdateAt[GA](idx, a)
|
||||
return func(as GA) GA {
|
||||
return F.Pipe2(
|
||||
as,
|
||||
update,
|
||||
O.GetOrElse(lazy.Of(as)),
|
||||
)
|
||||
}
|
||||
},
|
||||
fmt.Sprintf("At[%d]", idx),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package optional
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fof pointed.OfType[S, HKTS],
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Optional[S, A]) R {
|
||||
return func(sa Optional[S, A]) R {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.GetOption,
|
||||
O.Fold(
|
||||
lazy.Of(fof(s)),
|
||||
F.Flow2(
|
||||
f,
|
||||
fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,8 +310,10 @@ func TestAsTraversal(t *testing.T) {
|
||||
return Identity[Option[int]]{Value: s}
|
||||
}
|
||||
|
||||
fmap := func(ia Identity[int], f func(int) Option[int]) Identity[Option[int]] {
|
||||
return Identity[Option[int]]{Value: f(ia.Value)}
|
||||
fmap := func(f func(int) Option[int]) func(Identity[int]) Identity[Option[int]] {
|
||||
return func(ia Identity[int]) Identity[Option[int]] {
|
||||
return Identity[Option[int]]{Value: f(ia.Value)}
|
||||
}
|
||||
}
|
||||
|
||||
type TraversalFunc func(func(int) Identity[int]) func(Option[int]) Identity[Option[int]]
|
||||
|
||||
@@ -17,6 +17,9 @@ package prism
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
@@ -58,24 +61,23 @@ import (
|
||||
// higher-kinded types and applicative functors. Most users will work
|
||||
// directly with prisms rather than converting them to traversals.
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fof func(S) HKTS,
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
fof pointed.OfType[S, HKTS],
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Prism[S, A]) R {
|
||||
return func(sa Prism[S, A]) R {
|
||||
return func(f func(a A) HKTA) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.GetOption,
|
||||
O.Fold(
|
||||
// If prism doesn't match, return the original value lifted into HKTS
|
||||
F.Nullary2(F.Constant(s), fof),
|
||||
// If prism matches, apply f to the extracted value and map back
|
||||
func(a A) HKTS {
|
||||
return fmap(f(a), func(a A) S {
|
||||
return prismModify(F.Constant1[A](a), sa, s)
|
||||
})
|
||||
},
|
||||
lazy.Of(fof(s)),
|
||||
F.Flow2(
|
||||
f,
|
||||
fmap(func(a A) S {
|
||||
return Set[S](a)(sa)(s)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ import (
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array for the identity [Monoid]
|
||||
func FromArray[E, A any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
|
||||
func FromArray[A, E any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
|
||||
return AR.FromArray[[]A](m)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,51 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array for the identity monad
|
||||
// FromArray creates a traversal for array elements using the Identity functor.
|
||||
//
|
||||
// This is a specialized version of the generic FromArray that uses the Identity
|
||||
// functor, which provides the simplest possible computational context (no context).
|
||||
// This makes it ideal for straightforward array transformations where you want to
|
||||
// modify elements directly without additional effects.
|
||||
//
|
||||
// The Identity functor means that operations are applied directly to values without
|
||||
// wrapping them in any additional structure. This results in clean, efficient
|
||||
// traversals that simply map functions over array elements.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - A: The element type within the array
|
||||
//
|
||||
// Returns:
|
||||
// - A Traversal that can transform all elements in an array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for integer arrays
|
||||
// arrayTraversal := TI.FromArray[[]int, int]()
|
||||
//
|
||||
// // Compose with identity traversal
|
||||
// traversal := F.Pipe1(
|
||||
// T.Id[[]int, []int](),
|
||||
// T.Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Double all numbers in the array
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// doubled := traversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // doubled: []int{2, 4, 6, 8, 10}
|
||||
//
|
||||
// See Also:
|
||||
// - AR.FromArray: Generic version with configurable functor
|
||||
// - I.Of: Identity functor's pure/of operation
|
||||
// - I.Map: Identity functor's map operation
|
||||
// - I.Ap: Identity functor's applicative operation
|
||||
func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
|
||||
return AR.FromArray[GA](
|
||||
I.Of[GA],
|
||||
@@ -29,3 +73,75 @@ func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
|
||||
I.Ap[GA, A],
|
||||
)
|
||||
}
|
||||
|
||||
// At creates a function that focuses a traversal on a specific array index using the Identity functor.
|
||||
//
|
||||
// This is a specialized version of the generic At that uses the Identity functor,
|
||||
// providing the simplest computational context for array element access. It transforms
|
||||
// a traversal focusing on an array into a traversal focusing on the element at the
|
||||
// specified index.
|
||||
//
|
||||
// The Identity functor means operations are applied directly without additional wrapping,
|
||||
// making this ideal for straightforward element modifications. If the index is out of
|
||||
// bounds, the traversal focuses on zero elements (no-op).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - S: The source type of the outer traversal
|
||||
// - A: The element type within the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a traversal on arrays into a traversal on a specific element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Hobbies []string
|
||||
// }
|
||||
//
|
||||
// // Create a traversal focusing on hobbies
|
||||
// hobbiesTraversal := T.Id[Person, []string]()
|
||||
//
|
||||
// // Focus on the second hobby (index 1)
|
||||
// secondHobby := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TI.At[[]string, Person, string](1),
|
||||
// )
|
||||
//
|
||||
// // Modify the second hobby
|
||||
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding", "gaming"}}
|
||||
// updated := secondHobby(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // updated.Hobbies: []string{"reading", "coding!", "gaming"}
|
||||
//
|
||||
// // Out of bounds index is a no-op
|
||||
// outOfBounds := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TI.At[[]string, Person, string](10),
|
||||
// )
|
||||
// unchanged := outOfBounds(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // unchanged.Hobbies: []string{"reading", "coding", "gaming"} (no change)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.At: Generic version with configurable functor
|
||||
// - I.Of: Identity functor's pure/of operation
|
||||
// - I.Map: Identity functor's map operation
|
||||
func At[GA ~[]A, S, A any](idx int) func(G.Traversal[S, GA, S, GA]) G.Traversal[S, A, S, A] {
|
||||
return AR.At[GA, S, A, S](
|
||||
I.Of[GA],
|
||||
I.Map[A, GA],
|
||||
)(idx)
|
||||
}
|
||||
|
||||
@@ -16,19 +16,105 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
AR "github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
OA "github.com/IBM/fp-go/v2/optics/optional/array/generic"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array
|
||||
func FromArray[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
fof pointed.OfType[GB, HKTRB],
|
||||
fmap functor.MapType[GB, func(B) GB, HKTRB, HKTAB],
|
||||
fap apply.ApType[HKTB, HKTRB, HKTAB],
|
||||
) G.Traversal[GA, A, HKTRB, HKTB] {
|
||||
return func(f func(A) HKTB) func(s GA) HKTRB {
|
||||
return func(s GA) HKTRB {
|
||||
return AR.MonadTraverse(fof, fmap, fap, s, f)
|
||||
}
|
||||
return func(f func(A) HKTB) func(GA) HKTRB {
|
||||
return AR.Traverse[GA](fof, fmap, fap, f)
|
||||
}
|
||||
}
|
||||
|
||||
// At creates a function that focuses a traversal on a specific array index.
|
||||
//
|
||||
// This function takes an index and returns a function that transforms a traversal
|
||||
// focusing on an array into a traversal focusing on the element at that index.
|
||||
// It works by:
|
||||
// 1. Creating an Optional that focuses on the array element at the given index
|
||||
// 2. Converting that Optional into a Traversal
|
||||
// 3. Composing it with the original traversal
|
||||
//
|
||||
// If the index is out of bounds, the traversal will focus on zero elements (no-op),
|
||||
// following the Optional laws where operations on non-existent values have no effect.
|
||||
//
|
||||
// This is particularly useful when you have a nested structure containing arrays
|
||||
// and want to traverse to a specific element within those arrays.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - S: The source type of the outer traversal
|
||||
// - A: The element type within the array
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTGA: Higher-kinded type for GA (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift GA into the higher-kinded type HKTGA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA and produce HKTGA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an index and returns a traversal transformer
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TA "github.com/IBM/fp-go/v2/optics/traversal/array/generic"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Hobbies []string
|
||||
// }
|
||||
//
|
||||
// // Create a traversal focusing on the hobbies array
|
||||
// hobbiesTraversal := T.Id[Person, []string]()
|
||||
//
|
||||
// // Focus on the first hobby (index 0)
|
||||
// firstHobby := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TA.At[[]string, Person, string](
|
||||
// identity.Of[[]string],
|
||||
// identity.Map[string, []string],
|
||||
// )(0),
|
||||
// )
|
||||
//
|
||||
// // Modify the first hobby
|
||||
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding"}}
|
||||
// updated := firstHobby(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // updated.Hobbies: []string{"reading!", "coding"}
|
||||
//
|
||||
// See Also:
|
||||
// - OA.At: Creates an Optional focusing on an array element
|
||||
// - optional.AsTraversal: Converts an Optional to a Traversal
|
||||
// - G.Compose: Composes two traversals
|
||||
func At[GA ~[]A, S, A, HKTS, HKTGA, HKTA any](
|
||||
fof pointed.OfType[GA, HKTGA],
|
||||
fmap functor.MapType[A, GA, HKTA, HKTGA],
|
||||
) func(int) func(G.Traversal[S, GA, HKTS, HKTGA]) G.Traversal[S, A, HKTS, HKTA] {
|
||||
return F.Flow3(
|
||||
OA.At[GA],
|
||||
optional.AsTraversal[G.Traversal[GA, A, HKTGA, HKTA]](fof, fmap),
|
||||
G.Compose[
|
||||
G.Traversal[GA, A, HKTGA, HKTA],
|
||||
G.Traversal[S, GA, HKTS, HKTGA],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ package generic
|
||||
import (
|
||||
AR "github.com/IBM/fp-go/v2/array/generic"
|
||||
C "github.com/IBM/fp-go/v2/constant"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -47,7 +52,7 @@ func FromTraversable[
|
||||
}
|
||||
|
||||
// FoldMap maps each target to a `Monoid` and combines the result
|
||||
func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return F.Flow2(
|
||||
F.Pipe1(
|
||||
@@ -61,13 +66,84 @@ func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.
|
||||
|
||||
// Fold maps each target to a `Monoid` and combines the result
|
||||
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
return FoldMap[A, S](F.Identity[A])(sa)
|
||||
return FoldMap[S](F.Identity[A])(sa)
|
||||
}
|
||||
|
||||
// GetAll gets all the targets of a traversal
|
||||
func GetAll[GA ~[]A, S, A any](s S) func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
|
||||
fmap := FoldMap[GA, S](AR.Of[GA, A])
|
||||
fmap := FoldMap[S](AR.Of[GA, A])
|
||||
return func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
|
||||
return fmap(sa)(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter creates a function that filters the targets of a traversal based on a predicate.
|
||||
//
|
||||
// This function allows you to refine a traversal to only focus on values that satisfy
|
||||
// a given predicate. It works by converting the predicate into a prism, then converting
|
||||
// that prism into a traversal, and finally composing it with the original traversal.
|
||||
//
|
||||
// The filtering is selective: when modifying values through the filtered traversal,
|
||||
// only values that satisfy the predicate will be transformed. Values that don't
|
||||
// satisfy the predicate remain unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The focus type (the values being filtered)
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a predicate and returns an endomorphism on traversals
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[int]()
|
||||
// baseTraversal := F.Pipe1(
|
||||
// Id[[]int, []int](),
|
||||
// Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Filter to only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filteredTraversal := F.Pipe1(
|
||||
// baseTraversal,
|
||||
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
|
||||
// )
|
||||
//
|
||||
// // Double only positive numbers
|
||||
// numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // result: [-2, -1, 0, 2, 4, 6]
|
||||
//
|
||||
// See Also:
|
||||
// - prism.FromPredicate: Creates a prism from a predicate
|
||||
// - prism.AsTraversal: Converts a prism to a traversal
|
||||
// - Compose: Composes two traversals
|
||||
func Filter[
|
||||
S, HKTS, A, HKTA any](
|
||||
fof pointed.OfType[A, HKTA],
|
||||
fmap functor.MapType[A, A, HKTA, HKTA],
|
||||
) func(predicate.Predicate[A]) endomorphism.Endomorphism[Traversal[S, A, HKTS, HKTA]] {
|
||||
return F.Flow3(
|
||||
prism.FromPredicate,
|
||||
prism.AsTraversal[Traversal[A, A, HKTA, HKTA]](fof, fmap),
|
||||
Compose[
|
||||
Traversal[A, A, HKTA, HKTA],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, A, HKTS, HKTA]],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,46 +18,110 @@ package traversal
|
||||
import (
|
||||
C "github.com/IBM/fp-go/v2/constant"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/identity"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// Id is the identity constructor of a traversal
|
||||
func Id[S, A any]() G.Traversal[S, S, A, A] {
|
||||
func Id[S, A any]() Traversal[S, S, A, A] {
|
||||
return F.Identity[func(S) A]
|
||||
}
|
||||
|
||||
// Modify applies a transformation function to a traversal
|
||||
func Modify[S, A any](f func(A) A) func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
return func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
return sa(f)
|
||||
}
|
||||
func Modify[S, A any](f Endomorphism[A]) func(Traversal[S, A, S, A]) Endomorphism[S] {
|
||||
return identity.Flap[Endomorphism[S]](f)
|
||||
}
|
||||
|
||||
// Set sets a constant value for all values of the traversal
|
||||
func Set[S, A any](a A) func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
func Set[S, A any](a A) func(Traversal[S, A, S, A]) Endomorphism[S] {
|
||||
return Modify[S](F.Constant1[A](a))
|
||||
}
|
||||
|
||||
// FoldMap maps each target to a `Monoid` and combines the result
|
||||
func FoldMap[M, S, A any](f func(A) M) func(sa G.Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return G.FoldMap[M, S](f)
|
||||
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return G.FoldMap[S](f)
|
||||
}
|
||||
|
||||
// Fold maps each target to a `Monoid` and combines the result
|
||||
func Fold[S, A any](sa G.Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
return G.Fold(sa)
|
||||
}
|
||||
|
||||
// GetAll gets all the targets of a traversal
|
||||
func GetAll[S, A any](s S) func(sa G.Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
|
||||
func GetAll[A, S any](s S) func(sa Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
|
||||
return G.GetAll[[]A](s)
|
||||
}
|
||||
|
||||
// Compose composes two traversables
|
||||
func Compose[
|
||||
S, A, B, HKTS, HKTA, HKTB any](ab G.Traversal[A, B, HKTA, HKTB]) func(sa G.Traversal[S, A, HKTS, HKTA]) G.Traversal[S, B, HKTS, HKTB] {
|
||||
S, HKTS, A, B, HKTA, HKTB any](ab Traversal[A, B, HKTA, HKTB]) func(Traversal[S, A, HKTS, HKTA]) Traversal[S, B, HKTS, HKTB] {
|
||||
return G.Compose[
|
||||
G.Traversal[A, B, HKTA, HKTB],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
G.Traversal[S, B, HKTS, HKTB]](ab)
|
||||
Traversal[A, B, HKTA, HKTB],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, B, HKTS, HKTB]](ab)
|
||||
}
|
||||
|
||||
// Filter creates a function that filters the targets of a traversal based on a predicate.
|
||||
//
|
||||
// This function allows you to refine a traversal to only focus on values that satisfy
|
||||
// a given predicate. It works by converting the predicate into a prism, then converting
|
||||
// that prism into a traversal, and finally composing it with the original traversal.
|
||||
//
|
||||
// The filtering is selective: when modifying values through the filtered traversal,
|
||||
// only values that satisfy the predicate will be transformed. Values that don't
|
||||
// satisfy the predicate remain unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The focus type (the values being filtered)
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a predicate and returns an endomorphism on traversals
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[int]()
|
||||
// baseTraversal := F.Pipe1(
|
||||
// Id[[]int, []int](),
|
||||
// Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Filter to only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filteredTraversal := F.Pipe1(
|
||||
// baseTraversal,
|
||||
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
|
||||
// )
|
||||
//
|
||||
// // Double only positive numbers
|
||||
// numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // result: [-2, -1, 0, 2, 4, 6]
|
||||
//
|
||||
// See Also:
|
||||
// - prism.FromPredicate: Creates a prism from a predicate
|
||||
// - prism.AsTraversal: Converts a prism to a traversal
|
||||
// - Compose: Composes two traversals
|
||||
func Filter[S, HKTS, A, HKTA any](
|
||||
fof pointed.OfType[A, HKTA],
|
||||
fmap functor.MapType[A, A, HKTA, HKTA],
|
||||
) func(Predicate[A]) Endomorphism[Traversal[S, A, HKTS, HKTA]] {
|
||||
return G.Filter[S, HKTS](fof, fmap)
|
||||
}
|
||||
|
||||
@@ -32,14 +32,14 @@ func TestGetAll(t *testing.T) {
|
||||
|
||||
as := AR.From(1, 2, 3)
|
||||
|
||||
tr := AT.FromArray[[]int, int](AR.Monoid[int]())
|
||||
tr := AT.FromArray[int](AR.Monoid[int]())
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, C.Const[[]int, []int]](),
|
||||
Compose[[]int, []int, int, C.Const[[]int, []int]](tr),
|
||||
Compose[[]int, C.Const[[]int, []int], []int, int](tr),
|
||||
)
|
||||
|
||||
getall := GetAll[[]int, int](as)(sa)
|
||||
getall := GetAll[int](as)(sa)
|
||||
|
||||
assert.Equal(t, AR.From(1, 2, 3), getall)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func TestFold(t *testing.T) {
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, C.Const[int, []int]](),
|
||||
Compose[[]int, []int, int, C.Const[int, []int]](tr),
|
||||
Compose[[]int, C.Const[int, []int], []int, int](tr),
|
||||
)
|
||||
|
||||
folded := Fold(sa)(as)
|
||||
@@ -70,10 +70,245 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, int, []int](tr),
|
||||
Compose[[]int, []int](tr),
|
||||
)
|
||||
|
||||
res := sa(utils.Double)(as)
|
||||
|
||||
assert.Equal(t, AR.From(2, 4, 6), res)
|
||||
}
|
||||
|
||||
func TestFilter_Success(t *testing.T) {
|
||||
t.Run("filters and modifies only matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only positive numbers
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act - double only positive numbers
|
||||
result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{-2, -1, 0, 2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("filters even numbers and triples them", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5, 6}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only even numbers
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(func(n int) int { return n * 3 })(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{1, 6, 3, 12, 5, 18}, result)
|
||||
})
|
||||
|
||||
t.Run("filters strings by length", func(t *testing.T) {
|
||||
// Arrange
|
||||
words := []string{"a", "ab", "abc", "abcd", "abcde"}
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]string, []string](),
|
||||
Compose[[]string, []string, []string, string](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter strings with length > 2
|
||||
longerThanTwo := func(s string) bool { return len(s) > 2 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]string, []string, string, string](F.Identity[string], F.Identity[func(string) string])(longerThanTwo),
|
||||
)
|
||||
|
||||
// Act - convert to uppercase
|
||||
result := filteredTraversal(func(s string) string {
|
||||
return s + "!"
|
||||
})(words)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []string{"a", "ab", "abc!", "abcd!", "abcde!"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_EdgeCases(t *testing.T) {
|
||||
t.Run("empty array returns empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("no elements match predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-5, -4, -3, -2, -1}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert - all elements unchanged
|
||||
assert.Equal(t, []int{-5, -4, -3, -2, -1}, result)
|
||||
})
|
||||
|
||||
t.Run("all elements match predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert - all elements doubled
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("single element matching", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{42}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{84}, result)
|
||||
})
|
||||
|
||||
t.Run("single element not matching", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-42}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{-42}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Integration(t *testing.T) {
|
||||
t.Run("multiple filters composed", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only even numbers, then only those > 4
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
greaterThanFour := N.MoreThan(4)
|
||||
|
||||
filteredTraversal := F.Pipe2(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(greaterThanFour),
|
||||
)
|
||||
|
||||
// Act - add 100 to matching elements
|
||||
result := filteredTraversal(func(n int) int { return n + 100 })(numbers)
|
||||
|
||||
// Assert - only 6, 8, 10 should be modified
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 106, 7, 108, 9, 110}, result)
|
||||
})
|
||||
|
||||
t.Run("filter with identity transformation", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
)
|
||||
|
||||
// Act - identity transformation
|
||||
result := filteredTraversal(F.Identity[int])(numbers)
|
||||
|
||||
// Assert - array unchanged
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package traversal
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = G.Traversal[S, A, HKTS, HKTA]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user