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

Compare commits

..

4 Commits

Author SHA1 Message Date
Dr. Carsten Leue
21b517d388 fix: better doc and NonEmptyString
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-09 14:51:39 +02:00
Dr. Carsten Leue
0df62c0031 fix: unroll array Fold and FoldMap
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-09 11:11:00 +02:00
Dr. Carsten Leue
57318e2d1d fix: make use of Empty lazy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-08 15:00:39 +02:00
Dr. Carsten Leue
2b937d3e93 doc: add marble diagrams
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-30 10:15:27 +02:00
22 changed files with 2051 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -748,6 +748,8 @@ func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
//
// # See Also
//
// See Also:
//
// - Ask: Returns the entire context as the value
// - Map: Transforms the value after extraction
//

View File

@@ -4,10 +4,6 @@ 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=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,6 +82,12 @@ func Of2[K, A any](k K, a A) Seq2[K, A] {
// MonadMap transforms each element in a sequence using the provided function.
// This is the monadic version that takes the sequence as the first parameter.
//
// Marble Diagram:
//
// Input: --1--2--3-->
// Map(x => x * 2)
// Output: --2--4--6-->
//
// RxJS Equivalent: [map] - https://rxjs.dev/api/operators/map
//
// Example:
@@ -186,6 +192,12 @@ func MapWithKey[K, A, B any](f func(K, A) B) Operator2[K, A, B] {
// MonadFilter returns a sequence containing only elements that satisfy the predicate.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// Filter(x => x % 2 == 0)
// Output: -----2-----4----->
//
// RxJS Equivalent: [filter] - https://rxjs.dev/api/operators/filter
//
// Example:
@@ -293,6 +305,12 @@ func FilterWithKey[K, A any](pred func(K, A) bool) Operator2[K, A, A] {
// MonadFilterMap applies a function that returns an Option to each element,
// keeping only the Some values and unwrapping them.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5-->
// FilterMap(x => x % 2 == 0 ? Some(x * 10) : None)
// Output: -----20----40---->
//
// Example:
//
// seq := From(1, 2, 3, 4, 5)
@@ -430,6 +448,12 @@ func FilterMapWithKey[K, A, B any](f func(K, A) Option[B]) Operator2[K, A, B] {
// MonadChain applies a function that returns a sequence to each element and flattens the results.
// This is the monadic bind operation (flatMap).
//
// Marble Diagram:
//
// Input: --1-----2-----3---->
// Chain(x => [x, x*10])
// Output: --1-10--2-20--3-30->
//
// RxJS Equivalent: [mergeMap/flatMap] - https://rxjs.dev/api/operators/mergeMap
//
// Example:
@@ -473,6 +497,12 @@ func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
// Flatten flattens a sequence of sequences into a single sequence.
//
// Marble Diagram:
//
// Input: --[1,2]--[3,4]--[5]-->
// Flatten
// Output: --1-2----3-4----5---->
//
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
//
// Example:
@@ -489,6 +519,14 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
// MonadAp applies a sequence of functions to a sequence of values.
// This is the applicative apply operation.
//
// Marble Diagram:
//
// Functions: --(*2)---(+10)-->
// Values: --5------3------>
// Ap
// Output: --10-6---15-13-->
// (each function applied to each value)
//
// Example:
//
// fns := From(N.Mul(2), N.Add(10))
@@ -577,6 +615,13 @@ func Replicate[A any](n int, a A) Seq[A] {
// MonadReduce reduces a sequence to a single value by applying a function to each element
// and an accumulator, starting with an initial value.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--|
// Reduce((acc, x) => acc + x, 0)
// Output: ------------------15|
// (emits final result only)
//
// RxJS Equivalent: [reduce] - https://rxjs.dev/api/operators/reduce
//
// Example:
@@ -811,6 +856,13 @@ func FoldMapWithKey[K, A, B any](m M.Monoid[B]) func(func(K, A) B) func(Seq2[K,
// MonadFlap applies a fixed value to a sequence of functions.
// This is the dual of MonadAp.
//
// Marble Diagram:
//
// Functions: --(*2)---(+10)-->
// Value: 5 (fixed)
// Flap
// Output: --10-----15----->
//
// Example:
//
// fns := From(N.Mul(2), N.Add(10))
@@ -832,6 +884,12 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
// Prepend returns a function that adds an element to the beginning of a sequence.
//
// Marble Diagram:
//
// Input: -----2--3--4-->
// Prepend(1)
// Output: --1--2--3--4-->
//
// RxJS Equivalent: [startWith] - https://rxjs.dev/api/operators/startWith
//
// Example:
@@ -847,6 +905,12 @@ func Prepend[A any](head A) Operator[A, A] {
// Append returns a function that adds an element to the end of a sequence.
//
// Marble Diagram:
//
// Input: --1--2--3-----|
// Append(4)
// Output: --1--2--3--4--|
//
// RxJS Equivalent: [endWith] - https://rxjs.dev/api/operators/endWith
//
// Example:
@@ -863,6 +927,14 @@ func Append[A any](tail A) Operator[A, A] {
// MonadZip combines two sequences into a sequence of pairs.
// The resulting sequence stops when either input sequence is exhausted.
//
// Marble Diagram:
//
// SeqA: --1--2--3---->
// SeqB: --a--b------->
// Zip
// Output: --(1,a)-(2,b)|
// (stops when shorter sequence ends)
//
// RxJS Equivalent: [zip] - https://rxjs.dev/api/operators/zip
//
// Example:
@@ -1090,6 +1162,12 @@ func FromSeqPair[A, B any](as Seq[Pair[A, B]]) Seq2[A, B] {
// 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:

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,12 @@ import F "github.com/IBM/fp-go/v2/function"
// Once n elements have been yielded, iteration stops immediately without consuming
// the remaining elements from the source.
//
// Marble Diagram:
//
// Input: --1--2--3--4--5--6--7--8-->
// Take(3)
// Output: --1--2--3|
//
// RxJS Equivalent: [take] - https://rxjs.dev/api/operators/take
//
// Type Parameters:
@@ -90,6 +96,13 @@ func Take[U any](n int) Operator[U, U] {
// 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:
@@ -158,6 +171,13 @@ func TakeWhile[U any](p Predicate[U]) Operator[U, U] {
// 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:

View File

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

View File

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

View File

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

View File

@@ -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
}