1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-17 00:53:55 +02:00

Compare commits

...

12 Commits

Author SHA1 Message Date
Dr. Carsten Leue
9fd5b90138 fix: add codec and implement some helpers along the way
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-16 23:21:10 +01:00
Dr. Carsten Leue
cdc2041d8e fix: implement ReadIO consistently
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 13:07:19 +01:00
Dr. Carsten Leue
777fff9a5a fix: implement ReadIO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 12:24:46 +01:00
Carsten Leue
8acea9043f fix: refactor circuitbreaker (#152)
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 11:36:32 +01:00
Dr. Carsten Leue
c6445ac021 fix: better tests and docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-14 12:09:01 +01:00
Dr. Carsten Leue
840ffbb51d fix: documentation and missing tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-13 20:27:46 +01:00
Dr. Carsten Leue
380ba2853c fix: update profunctor docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-13 16:15:37 +01:00
Dr. Carsten Leue
c18e5e2107 fix: add monoid to state
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 23:03:57 +01:00
Dr. Carsten Leue
89766bdb26 fix: add monoid to state
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 23:03:37 +01:00
Dr. Carsten Leue
21d116d325 fix: implement monoid for Pair
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 22:53:19 +01:00
Dr. Carsten Leue
7f2e76dd94 fix: implement monoid for Pair
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 22:32:33 +01:00
Dr. Carsten Leue
77965a12ff fix: implement monoid for Pair
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 22:32:04 +01:00
161 changed files with 22971 additions and 1189 deletions

1
v2/.bobignore Normal file
View File

@@ -0,0 +1 @@
reflect\reflect.go

View File

@@ -14,6 +14,8 @@ This document explains the key design decisions and principles behind fp-go's AP
fp-go follows the **"data last"** principle, where the data being operated on is always the last parameter in a function. This design choice enables powerful function composition and partial application patterns.
This principle is deeply rooted in functional programming tradition, particularly in **Haskell's design philosophy**. Haskell functions are automatically curried and follow the data-last convention, making function composition natural and elegant. For example, Haskell's `map` function has the signature `(a -> b) -> [a] -> [b]`, where the transformation function comes before the list.
### What is "Data Last"?
In the "data last" style, functions are structured so that:
@@ -31,6 +33,8 @@ The "data last" principle enables:
3. **Point-Free Style**: Write transformations without explicitly mentioning the data
4. **Reusability**: Create reusable transformation pipelines
This design aligns with Haskell's approach where all functions are curried by default, enabling elegant composition patterns that have proven effective over decades of functional programming practice.
### Examples
#### Basic Transformation
@@ -181,8 +185,18 @@ result := O.MonadMap(O.Some("hello"), strings.ToUpper)
The data-last currying pattern is well-documented in the functional programming community:
#### Haskell Design Philosophy
- [Haskell Wiki - Currying](https://wiki.haskell.org/Currying) - Comprehensive explanation of currying in Haskell
- [Learn You a Haskell - Higher Order Functions](http://learnyouahaskell.com/higher-order-functions) - Introduction to currying and partial application
- [Haskell's Prelude](https://hackage.haskell.org/package/base/docs/Prelude.html) - Standard library showing data-last convention throughout
#### General Functional Programming
- [Mostly Adequate Guide - Ch. 4: Currying](https://mostly-adequate.gitbook.io/mostly-adequate-guide/ch04) - Excellent introduction with clear examples
- [Curry and Function Composition](https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983) by Eric Elliott
- [Why Curry Helps](https://hughfdjackson.com/javascript/why-curry-helps/) - Practical benefits of currying
#### Related Libraries
- [fp-ts Documentation](https://gcanti.github.io/fp-ts/) - TypeScript library that inspired fp-go's design
- [fp-ts Issue #1238](https://github.com/gcanti/fp-ts/issues/1238) - Real-world examples of data-last refactoring
## Kleisli and Operator Types

View File

@@ -446,6 +446,7 @@ func process() IOResult[string] {
## 📚 Documentation
- **[Design Decisions](./DESIGN.md)** - Key design principles and patterns explained
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
- **[Code Samples](./samples/)** - Practical examples and use cases
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases

View File

@@ -514,6 +514,83 @@ func Push[A any](a A) Operator[A, A] {
return G.Push[Operator[A, A]](a)
}
// Concat concatenates two arrays, appending the provided array to the end of the input array.
// This is a curried function that takes an array to append and returns a function that
// takes the base array and returns the concatenated result.
//
// The function creates a new array containing all elements from the base array followed
// by all elements from the appended array. Neither input array is modified.
//
// Type Parameters:
// - A: The type of elements in the arrays
//
// Parameters:
// - as: The array to append to the end of the base array
//
// Returns:
// - A function that takes a base array and returns a new array with `as` appended to its end
//
// Behavior:
// - Creates a new array with length equal to the sum of both input arrays
// - Copies all elements from the base array first
// - Appends all elements from the `as` array at the end
// - Returns the base array unchanged if `as` is empty
// - Returns `as` unchanged if the base array is empty
// - Does not modify either input array
//
// Example:
//
// base := []int{1, 2, 3}
// toAppend := []int{4, 5, 6}
// result := array.Concat(toAppend)(base)
// // result: []int{1, 2, 3, 4, 5, 6}
// // base: []int{1, 2, 3} (unchanged)
// // toAppend: []int{4, 5, 6} (unchanged)
//
// Example with empty arrays:
//
// base := []int{1, 2, 3}
// empty := []int{}
// result := array.Concat(empty)(base)
// // result: []int{1, 2, 3}
//
// Example with strings:
//
// words1 := []string{"hello", "world"}
// words2 := []string{"foo", "bar"}
// result := array.Concat(words2)(words1)
// // result: []string{"hello", "world", "foo", "bar"}
//
// Example with functional composition:
//
// numbers := []int{1, 2, 3}
// result := F.Pipe2(
// numbers,
// array.Map(N.Mul(2)),
// array.Concat([]int{10, 20}),
// )
// // result: []int{2, 4, 6, 10, 20}
//
// Use cases:
// - Combining multiple arrays into one
// - Building arrays incrementally
// - Implementing array-based data structures (queues, buffers)
// - Merging results from multiple operations
// - Creating array pipelines with functional composition
//
// Performance:
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
// - Space complexity: O(n + m) for the new array
// - Optimized to avoid allocation when one array is empty
//
// Note: This function is immutable - it creates a new array rather than modifying
// the input arrays. For appending a single element, consider using Append or Push.
//
//go:inline
func Concat[A any](as []A) Operator[A, A] {
return F.Bind2nd(array.Concat[[]A, A], as)
}
// MonadFlap applies a value to an array of functions, producing an array of results.
// This is the monadic version that takes both parameters.
//
@@ -622,3 +699,128 @@ func Prepend[A any](head A) Operator[A, A] {
func Reverse[A any](as []A) []A {
return G.Reverse(as)
}
// Extend applies a function to every suffix of an array, creating a new array of results.
// This is the comonad extend operation for arrays.
//
// The function f is applied to progressively smaller suffixes of the input array:
// - f(as[0:]) for the first element
// - f(as[1:]) for the second element
// - f(as[2:]) for the third element
// - and so on...
//
// Type Parameters:
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// Parameters:
// - f: A function that takes an array suffix and returns a value
//
// Returns:
// - A function that transforms an array of A into an array of B
//
// Behavior:
// - Creates a new array with the same length as the input
// - For each position i, applies f to the suffix starting at i
// - Returns an empty array if the input is empty
//
// Example:
//
// // Sum all elements from current position to end
// sumSuffix := array.Extend(func(as []int) int {
// return array.Reduce(func(acc, x int) int { return acc + x }, 0)(as)
// })
// result := sumSuffix([]int{1, 2, 3, 4})
// // result: []int{10, 9, 7, 4}
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
//
// Example with length:
//
// // Get remaining length at each position
// lengths := array.Extend(array.Size[int])
// result := lengths([]int{10, 20, 30})
// // result: []int{3, 2, 1}
//
// Example with head:
//
// // Duplicate each element (extract head of each suffix)
// duplicate := array.Extend(func(as []int) int {
// return F.Pipe1(as, array.Head[int], O.GetOrElse(F.Constant(0)))
// })
// result := duplicate([]int{1, 2, 3})
// // result: []int{1, 2, 3}
//
// Use cases:
// - Computing cumulative or rolling operations
// - Implementing sliding window algorithms
// - Creating context-aware transformations
// - Building comonadic computations
//
// Comonad laws:
// - Left identity: Extend(Extract) == Identity
// - Right identity: Extract ∘ Extend(f) == f
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
//
//go:inline
func Extend[A, B any](f func([]A) B) Operator[A, B] {
return func(as []A) []B {
return G.MakeBy[[]B](len(as), func(i int) B { return f(as[i:]) })
}
}
// Extract returns the first element of an array, or a zero value if empty.
// This is the comonad extract operation for arrays.
//
// Extract is the dual of the monadic return/of operation. While Of wraps a value
// in a context, Extract unwraps a value from its context.
//
// Type Parameters:
// - A: The type of elements in the array
//
// Parameters:
// - as: The input array
//
// Returns:
// - The first element if the array is non-empty, otherwise the zero value of type A
//
// Behavior:
// - Returns as[0] if the array has at least one element
// - Returns the zero value of A if the array is empty
// - Does not modify the input array
//
// Example:
//
// result := array.Extract([]int{1, 2, 3})
// // result: 1
//
// Example with empty array:
//
// result := array.Extract([]int{})
// // result: 0 (zero value for int)
//
// Example with strings:
//
// result := array.Extract([]string{"hello", "world"})
// // result: "hello"
//
// Example with empty string array:
//
// result := array.Extract([]string{})
// // result: "" (zero value for string)
//
// Use cases:
// - Extracting the current focus from a comonadic context
// - Getting the head element with a default zero value
// - Implementing comonad-based computations
//
// Comonad laws:
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
//
// Note: For a safer alternative that handles empty arrays explicitly,
// consider using Head which returns an Option[A].
//
//go:inline
func Extract[A any](as []A) A {
return G.Extract(as)
}

View File

@@ -474,3 +474,631 @@ func TestReverseProperties(t *testing.T) {
}
})
}
// TestExtract tests the Extract function
func TestExtract(t *testing.T) {
t.Run("Extract from non-empty array", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extract(input)
assert.Equal(t, 1, result)
})
t.Run("Extract from single element array", func(t *testing.T) {
input := []string{"hello"}
result := Extract(input)
assert.Equal(t, "hello", result)
})
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
input := []int{}
result := Extract(input)
assert.Equal(t, 0, result)
})
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
input := []string{}
result := Extract(input)
assert.Equal(t, "", result)
})
t.Run("Extract does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extract(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extract with floats", func(t *testing.T) {
input := []float64{3.14, 2.71, 1.41}
result := Extract(input)
assert.Equal(t, 3.14, result)
})
t.Run("Extract with structs", func(t *testing.T) {
type Person struct {
Name string
Age int
}
input := []Person{
{"Alice", 30},
{"Bob", 25},
}
result := Extract(input)
assert.Equal(t, Person{"Alice", 30}, result)
})
}
// TestExtractComonadLaws tests comonad laws for Extract
func TestExtractComonadLaws(t *testing.T) {
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
value := 42
result := Extract(Of(value))
assert.Equal(t, value, result)
})
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
extended := Extend(f)(input)
result := Extract(extended)
expected := f(input)
assert.Equal(t, expected, result)
})
}
// TestExtend tests the Extend function
func TestExtend(t *testing.T) {
t.Run("Extend with sum of suffixes", func(t *testing.T) {
input := []int{1, 2, 3, 4}
sumSuffix := Extend(func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
assert.Equal(t, expected, result)
})
t.Run("Extend with length of suffixes", func(t *testing.T) {
input := []int{10, 20, 30}
lengths := Extend(Size[int])
result := lengths(input)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Extend with head extraction", func(t *testing.T) {
input := []int{1, 2, 3}
duplicate := Extend(func(as []int) int {
return F.Pipe2(as, Head[int], O.GetOrElse(F.Constant(0)))
})
result := duplicate(input)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
t.Run("Extend with empty array", func(t *testing.T) {
input := []int{}
result := Extend(Size[int])(input)
assert.Equal(t, []int{}, result)
})
t.Run("Extend with single element", func(t *testing.T) {
input := []string{"hello"}
result := Extend(func(as []string) int { return len(as) })(input)
expected := []int{1}
assert.Equal(t, expected, result)
})
t.Run("Extend does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extend(Size[int])(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extend with string concatenation", func(t *testing.T) {
input := []string{"a", "b", "c"}
concat := Extend(func(as []string) string {
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
})
result := concat(input)
expected := []string{"abc", "bc", "c"}
assert.Equal(t, expected, result)
})
t.Run("Extend with max of suffixes", func(t *testing.T) {
input := []int{3, 1, 4, 1, 5}
maxSuffix := Extend(func(as []int) int {
if len(as) == 0 {
return 0
}
max := as[0]
for _, v := range as[1:] {
if v > max {
max = v
}
}
return max
})
result := maxSuffix(input)
expected := []int{5, 5, 5, 5, 5}
assert.Equal(t, expected, result)
})
}
// TestExtendComonadLaws tests comonad laws for Extend
func TestExtendComonadLaws(t *testing.T) {
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extend(Extract[int])(input)
assert.Equal(t, input, result)
})
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
result := F.Pipe2(input, Extend(f), Extract[int])
expected := f(input)
assert.Equal(t, expected, result)
})
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
input := []int{1, 2, 3}
// f: sum of array
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// g: length of array
g := func(as []int) int {
return len(as)
}
// Left side: Extend(f) ∘ Extend(g)
left := F.Pipe2(input, Extend(g), Extend(f))
// Right side: Extend(f ∘ Extend(g))
right := Extend(func(as []int) int {
return f(Extend(g)(as))
})(input)
assert.Equal(t, left, right)
})
}
// TestExtendComposition tests Extend with other array operations
func TestExtendComposition(t *testing.T) {
t.Run("Extend after Map", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Map(N.Mul(2)),
Extend(func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}),
)
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
assert.Equal(t, expected, result)
})
t.Run("Map after Extend", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Extend(Size[int]),
Map(N.Mul(10)),
)
expected := []int{30, 20, 10}
assert.Equal(t, expected, result)
})
t.Run("Extend with Filter", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6}
result := F.Pipe2(
input,
Filter(func(n int) bool { return n%2 == 0 }),
Extend(Size[int]),
)
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
assert.Equal(t, expected, result)
})
}
// TestExtendUseCases demonstrates practical use cases for Extend
func TestExtendUseCases(t *testing.T) {
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
runningSum := Extend(func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := runningSum(input)
expected := []int{15, 14, 12, 9, 5}
assert.Equal(t, expected, result)
})
t.Run("Sliding window average", func(t *testing.T) {
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
windowAvg := Extend(func(as []float64) float64 {
if len(as) == 0 {
return 0
}
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
return sum / float64(len(as))
})
result := windowAvg(input)
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
assert.Equal(t, expected, result)
})
t.Run("Check if suffix is sorted", func(t *testing.T) {
input := []int{1, 2, 3, 2, 1}
isSorted := Extend(func(as []int) bool {
for i := 1; i < len(as); i++ {
if as[i] < as[i-1] {
return false
}
}
return true
})
result := isSorted(input)
expected := []bool{false, false, false, false, true}
assert.Equal(t, expected, result)
})
t.Run("Count remaining elements", func(t *testing.T) {
events := []string{"start", "middle", "end"}
remaining := Extend(Size[string])
result := remaining(events)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
}
// TestConcat tests the Concat function
func TestConcat(t *testing.T) {
t.Run("Concat two non-empty arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
result := Concat(toAppend)(base)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result)
})
t.Run("Concat with empty array to append", func(t *testing.T) {
base := []int{1, 2, 3}
empty := []int{}
result := Concat(empty)(base)
assert.Equal(t, base, result)
})
t.Run("Concat to empty base array", func(t *testing.T) {
empty := []int{}
toAppend := []int{1, 2, 3}
result := Concat(toAppend)(empty)
assert.Equal(t, toAppend, result)
})
t.Run("Concat two empty arrays", func(t *testing.T) {
empty1 := []int{}
empty2 := []int{}
result := Concat(empty2)(empty1)
assert.Equal(t, []int{}, result)
})
t.Run("Concat strings", func(t *testing.T) {
words1 := []string{"hello", "world"}
words2 := []string{"foo", "bar"}
result := Concat(words2)(words1)
expected := []string{"hello", "world", "foo", "bar"}
assert.Equal(t, expected, result)
})
t.Run("Concat single element arrays", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
result := Concat(arr2)(arr1)
expected := []int{1, 2}
assert.Equal(t, expected, result)
})
t.Run("Does not modify original arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
baseCopy := []int{1, 2, 3}
toAppendCopy := []int{4, 5, 6}
_ = Concat(toAppend)(base)
assert.Equal(t, baseCopy, base)
assert.Equal(t, toAppendCopy, toAppend)
})
t.Run("Concat with floats", func(t *testing.T) {
arr1 := []float64{1.1, 2.2}
arr2 := []float64{3.3, 4.4}
result := Concat(arr2)(arr1)
expected := []float64{1.1, 2.2, 3.3, 4.4}
assert.Equal(t, expected, result)
})
t.Run("Concat with structs", func(t *testing.T) {
type Person struct {
Name string
Age int
}
arr1 := []Person{{"Alice", 30}, {"Bob", 25}}
arr2 := []Person{{"Charlie", 35}}
result := Concat(arr2)(arr1)
expected := []Person{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}
assert.Equal(t, expected, result)
})
t.Run("Concat large arrays", func(t *testing.T) {
arr1 := MakeBy(500, F.Identity[int])
arr2 := MakeBy(500, func(i int) int { return i + 500 })
result := Concat(arr2)(arr1)
assert.Equal(t, 1000, len(result))
assert.Equal(t, 0, result[0])
assert.Equal(t, 499, result[499])
assert.Equal(t, 500, result[500])
assert.Equal(t, 999, result[999])
})
t.Run("Concat multiple times", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
arr3 := []int{3}
result := F.Pipe2(
arr1,
Concat(arr2),
Concat(arr3),
)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
}
// TestConcatComposition tests Concat with other array operations
func TestConcatComposition(t *testing.T) {
t.Run("Concat after Map", func(t *testing.T) {
numbers := []int{1, 2, 3}
result := F.Pipe2(
numbers,
Map(N.Mul(2)),
Concat([]int{10, 20}),
)
expected := []int{2, 4, 6, 10, 20}
assert.Equal(t, expected, result)
})
t.Run("Map after Concat", func(t *testing.T) {
arr1 := []int{1, 2}
arr2 := []int{3, 4}
result := F.Pipe2(
arr1,
Concat(arr2),
Map(N.Mul(2)),
)
expected := []int{2, 4, 6, 8}
assert.Equal(t, expected, result)
})
t.Run("Concat with Filter", func(t *testing.T) {
arr1 := []int{1, 2, 3, 4}
arr2 := []int{5, 6, 7, 8}
result := F.Pipe2(
arr1,
Concat(arr2),
Filter(func(n int) bool { return n%2 == 0 }),
)
expected := []int{2, 4, 6, 8}
assert.Equal(t, expected, result)
})
t.Run("Concat with Reduce", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := F.Pipe2(
arr1,
Concat(arr2),
Reduce(func(acc, x int) int { return acc + x }, 0),
)
expected := 21 // 1+2+3+4+5+6
assert.Equal(t, expected, result)
})
t.Run("Concat with Reverse", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := F.Pipe2(
arr1,
Concat(arr2),
Reverse[int],
)
expected := []int{6, 5, 4, 3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Concat with Flatten", func(t *testing.T) {
arr1 := [][]int{{1, 2}, {3, 4}}
arr2 := [][]int{{5, 6}}
result := F.Pipe2(
arr1,
Concat(arr2),
Flatten[int],
)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result)
})
t.Run("Multiple Concat operations", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
arr3 := []int{3}
arr4 := []int{4}
result := Concat(arr4)(Concat(arr3)(Concat(arr2)(arr1)))
expected := []int{1, 2, 3, 4}
assert.Equal(t, expected, result)
})
}
// TestConcatUseCases demonstrates practical use cases for Concat
func TestConcatUseCases(t *testing.T) {
t.Run("Building array incrementally", func(t *testing.T) {
header := []string{"Name", "Age"}
data := []string{"Alice", "30"}
footer := []string{"Total: 1"}
result := F.Pipe2(
header,
Concat(data),
Concat(footer),
)
expected := []string{"Name", "Age", "Alice", "30", "Total: 1"}
assert.Equal(t, expected, result)
})
t.Run("Merging results from multiple operations", func(t *testing.T) {
evens := Filter(func(n int) bool { return n%2 == 0 })([]int{1, 2, 3, 4, 5, 6})
odds := Filter(func(n int) bool { return n%2 != 0 })([]int{1, 2, 3, 4, 5, 6})
result := Concat(odds)(evens)
expected := []int{2, 4, 6, 1, 3, 5}
assert.Equal(t, expected, result)
})
t.Run("Combining prefix and suffix", func(t *testing.T) {
prefix := []string{"Mr.", "Dr."}
names := []string{"Smith", "Jones"}
addPrefix := func(name string) []string {
return Map(func(p string) string { return p + " " + name })(prefix)
}
result := F.Pipe2(
names,
Chain(addPrefix),
F.Identity[[]string],
)
expected := []string{"Mr. Smith", "Dr. Smith", "Mr. Jones", "Dr. Jones"}
assert.Equal(t, expected, result)
})
t.Run("Queue-like behavior", func(t *testing.T) {
queue := []int{1, 2, 3}
newItems := []int{4, 5}
// Add items to end of queue
updatedQueue := Concat(newItems)(queue)
assert.Equal(t, []int{1, 2, 3, 4, 5}, updatedQueue)
assert.Equal(t, 1, updatedQueue[0]) // Front of queue
assert.Equal(t, 5, updatedQueue[len(updatedQueue)-1]) // Back of queue
})
t.Run("Combining configuration arrays", func(t *testing.T) {
defaultConfig := []string{"--verbose", "--color"}
userConfig := []string{"--output=file.txt", "--format=json"}
finalConfig := Concat(userConfig)(defaultConfig)
expected := []string{"--verbose", "--color", "--output=file.txt", "--format=json"}
assert.Equal(t, expected, finalConfig)
})
}
// TestConcatProperties tests mathematical properties of Concat
func TestConcatProperties(t *testing.T) {
t.Run("Associativity: (a + b) + c == a + (b + c)", func(t *testing.T) {
a := []int{1, 2}
b := []int{3, 4}
c := []int{5, 6}
// (a + b) + c
left := Concat(c)(Concat(b)(a))
// a + (b + c)
right := Concat(Concat(c)(b))(a)
assert.Equal(t, left, right)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, left)
})
t.Run("Identity: a + [] == a and [] + a == a", func(t *testing.T) {
arr := []int{1, 2, 3}
empty := []int{}
// Right identity
rightResult := Concat(empty)(arr)
assert.Equal(t, arr, rightResult)
// Left identity
leftResult := Concat(arr)(empty)
assert.Equal(t, arr, leftResult)
})
t.Run("Length property: len(a + b) == len(a) + len(b)", func(t *testing.T) {
testCases := []struct {
arr1 []int
arr2 []int
}{
{[]int{1, 2, 3}, []int{4, 5}},
{[]int{1}, []int{2, 3, 4, 5}},
{[]int{}, []int{1, 2, 3}},
{[]int{1, 2, 3}, []int{}},
{MakeBy(100, F.Identity[int]), MakeBy(50, F.Identity[int])},
}
for _, tc := range testCases {
result := Concat(tc.arr2)(tc.arr1)
expectedLen := len(tc.arr1) + len(tc.arr2)
assert.Equal(t, expectedLen, len(result))
}
})
t.Run("Order preservation: elements maintain their relative order", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := Concat(arr2)(arr1)
// Check arr1 elements are in order
assert.Equal(t, 1, result[0])
assert.Equal(t, 2, result[1])
assert.Equal(t, 3, result[2])
// Check arr2 elements are in order after arr1
assert.Equal(t, 4, result[3])
assert.Equal(t, 5, result[4])
assert.Equal(t, 6, result[5])
})
t.Run("Immutability: original arrays are not modified", func(t *testing.T) {
original1 := []int{1, 2, 3}
original2 := []int{4, 5, 6}
copy1 := []int{1, 2, 3}
copy2 := []int{4, 5, 6}
_ = Concat(original2)(original1)
assert.Equal(t, copy1, original1)
assert.Equal(t, copy2, original2)
})
}

View File

@@ -375,3 +375,102 @@ func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
func Reverse[GT ~[]T, T any](as GT) GT {
return array.Reverse(as)
}
// Extract returns the first element of an array, or a zero value if empty.
// This is the comonad extract operation for arrays.
//
// Extract is the dual of the monadic return/of operation. While Of wraps a value
// in a context, Extract unwraps a value from its context.
//
// Type Parameters:
// - GA: The array type constraint
// - A: The type of elements in the array
//
// Parameters:
// - as: The input array
//
// Returns:
// - The first element if the array is non-empty, otherwise the zero value of type A
//
// Behavior:
// - Returns as[0] if the array has at least one element
// - Returns the zero value of A if the array is empty
// - Does not modify the input array
//
// Example:
//
// result := Extract([]int{1, 2, 3})
// // result: 1
//
// Example with empty array:
//
// result := Extract([]int{})
// // result: 0 (zero value for int)
//
// Comonad laws:
// - Extract ∘ Of == Identity (extracting from a singleton returns the value)
// - Extract ∘ Extend(f) == f (extract after extend equals applying f)
//
//go:inline
func Extract[GA ~[]A, A any](as GA) A {
if len(as) > 0 {
return as[0]
}
var zero A
return zero
}
// Extend applies a function to every suffix of an array, creating a new array of results.
// This is the comonad extend operation for arrays.
//
// The function f is applied to progressively smaller suffixes of the input array:
// - f(as[0:]) for the first element
// - f(as[1:]) for the second element
// - f(as[2:]) for the third element
// - and so on...
//
// Type Parameters:
// - GA: The input array type constraint
// - GB: The output array type constraint
// - A: The type of elements in the input array
// - B: The type of elements in the output array
//
// Parameters:
// - f: A function that takes an array suffix and returns a value
//
// Returns:
// - A function that transforms an array of A into an array of B
//
// Behavior:
// - Creates a new array with the same length as the input
// - For each position i, applies f to the suffix starting at i
// - Returns an empty array if the input is empty
//
// Example:
//
// // Sum all elements from current position to end
// sumSuffix := Extend[[]int, []int](func(as []int) int {
// return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
// })
// result := sumSuffix([]int{1, 2, 3, 4})
// // result: []int{10, 9, 7, 4}
// // Explanation: [1+2+3+4, 2+3+4, 3+4, 4]
//
// Example with length:
//
// // Get remaining length at each position
// lengths := Extend[[]int, []int](Size[[]int, int])
// result := lengths([]int{10, 20, 30})
// // result: []int{3, 2, 1}
//
// Comonad laws:
// - Left identity: Extend(Extract) == Identity
// - Right identity: Extract ∘ Extend(f) == f
// - Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))
//
//go:inline
func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
return func(as GA) GB {
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
}
}

View File

@@ -0,0 +1,298 @@
// 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 (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestExtract tests the Extract function
func TestExtract(t *testing.T) {
t.Run("Extract from non-empty array", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extract(input)
assert.Equal(t, 1, result)
})
t.Run("Extract from single element array", func(t *testing.T) {
input := []string{"hello"}
result := Extract(input)
assert.Equal(t, "hello", result)
})
t.Run("Extract from empty array returns zero value", func(t *testing.T) {
input := []int{}
result := Extract(input)
assert.Equal(t, 0, result)
})
t.Run("Extract from empty string array returns empty string", func(t *testing.T) {
input := []string{}
result := Extract(input)
assert.Equal(t, "", result)
})
t.Run("Extract does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extract(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extract with floats", func(t *testing.T) {
input := []float64{3.14, 2.71, 1.41}
result := Extract(input)
assert.Equal(t, 3.14, result)
})
t.Run("Extract with custom slice type", func(t *testing.T) {
type IntSlice []int
input := IntSlice{10, 20, 30}
result := Extract(input)
assert.Equal(t, 10, result)
})
}
// TestExtractComonadLaws tests comonad laws for Extract
func TestExtractComonadLaws(t *testing.T) {
t.Run("Extract ∘ Of == Identity", func(t *testing.T) {
value := 42
result := Extract(Of[[]int](value))
assert.Equal(t, value, result)
})
t.Run("Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
extended := Extend[[]int, []int](f)(input)
result := Extract(extended)
expected := f(input)
assert.Equal(t, expected, result)
})
}
// TestExtend tests the Extend function
func TestExtend(t *testing.T) {
t.Run("Extend with sum of suffixes", func(t *testing.T) {
input := []int{1, 2, 3, 4}
sumSuffix := Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := []int{10, 9, 7, 4} // [1+2+3+4, 2+3+4, 3+4, 4]
assert.Equal(t, expected, result)
})
t.Run("Extend with length of suffixes", func(t *testing.T) {
input := []int{10, 20, 30}
lengths := Extend[[]int, []int](Size[[]int, int])
result := lengths(input)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Extend with head extraction", func(t *testing.T) {
input := []int{1, 2, 3}
duplicate := Extend[[]int, []int](Extract[[]int, int])
result := duplicate(input)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
t.Run("Extend with empty array", func(t *testing.T) {
input := []int{}
result := Extend[[]int, []int](Size[[]int, int])(input)
assert.Equal(t, []int{}, result)
})
t.Run("Extend with single element", func(t *testing.T) {
input := []string{"hello"}
result := Extend[[]string, []int](func(as []string) int { return len(as) })(input)
expected := []int{1}
assert.Equal(t, expected, result)
})
t.Run("Extend does not modify original array", func(t *testing.T) {
original := []int{1, 2, 3}
originalCopy := []int{1, 2, 3}
_ = Extend[[]int, []int](Size[[]int, int])(original)
assert.Equal(t, originalCopy, original)
})
t.Run("Extend with string concatenation", func(t *testing.T) {
input := []string{"a", "b", "c"}
concat := Extend[[]string, []string](func(as []string) string {
return MonadReduce(as, func(acc, s string) string { return acc + s }, "")
})
result := concat(input)
expected := []string{"abc", "bc", "c"}
assert.Equal(t, expected, result)
})
t.Run("Extend with custom slice types", func(t *testing.T) {
type IntSlice []int
type ResultSlice []int
input := IntSlice{1, 2, 3}
sumSuffix := Extend[IntSlice, ResultSlice](func(as IntSlice) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := sumSuffix(input)
expected := ResultSlice{6, 5, 3}
assert.Equal(t, expected, result)
})
}
// TestExtendComonadLaws tests comonad laws for Extend
func TestExtendComonadLaws(t *testing.T) {
t.Run("Left identity: Extend(Extract) == Identity", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
result := Extend[[]int, []int](Extract[[]int, int])(input)
assert.Equal(t, input, result)
})
t.Run("Right identity: Extract ∘ Extend(f) == f", func(t *testing.T) {
input := []int{1, 2, 3, 4}
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// Extract(Extend(f)(input)) should equal f(input)
result := F.Pipe2(input, Extend[[]int, []int](f), Extract[[]int, int])
expected := f(input)
assert.Equal(t, expected, result)
})
t.Run("Associativity: Extend(f) ∘ Extend(g) == Extend(f ∘ Extend(g))", func(t *testing.T) {
input := []int{1, 2, 3}
// f: sum of array
f := func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}
// g: length of array
g := func(as []int) int {
return len(as)
}
// Left side: Extend(f) ∘ Extend(g)
left := F.Pipe2(input, Extend[[]int, []int](g), Extend[[]int, []int](f))
// Right side: Extend(f ∘ Extend(g))
right := Extend[[]int, []int](func(as []int) int {
return f(Extend[[]int, []int](g)(as))
})(input)
assert.Equal(t, left, right)
})
}
// TestExtendComposition tests Extend with other array operations
func TestExtendComposition(t *testing.T) {
t.Run("Extend after Map", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Map[[]int, []int](func(x int) int { return x * 2 }),
Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
}),
)
expected := []int{12, 10, 6} // [2+4+6, 4+6, 6]
assert.Equal(t, expected, result)
})
t.Run("Map after Extend", func(t *testing.T) {
input := []int{1, 2, 3}
result := F.Pipe2(
input,
Extend[[]int, []int](Size[[]int, int]),
Map[[]int, []int](func(x int) int { return x * 10 }),
)
expected := []int{30, 20, 10}
assert.Equal(t, expected, result)
})
t.Run("Extend with Filter", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5, 6}
result := F.Pipe2(
input,
Filter[[]int](func(n int) bool { return n%2 == 0 }),
Extend[[]int, []int](Size[[]int, int]),
)
expected := []int{3, 2, 1} // lengths of [2,4,6], [4,6], [6]
assert.Equal(t, expected, result)
})
}
// TestExtendUseCases demonstrates practical use cases for Extend
func TestExtendUseCases(t *testing.T) {
t.Run("Running sum (cumulative sum from each position)", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
runningSum := Extend[[]int, []int](func(as []int) int {
return MonadReduce(as, func(acc, x int) int { return acc + x }, 0)
})
result := runningSum(input)
expected := []int{15, 14, 12, 9, 5}
assert.Equal(t, expected, result)
})
t.Run("Sliding window average", func(t *testing.T) {
input := []float64{1.0, 2.0, 3.0, 4.0, 5.0}
windowAvg := Extend[[]float64, []float64](func(as []float64) float64 {
if len(as) == 0 {
return 0
}
sum := MonadReduce(as, func(acc, x float64) float64 { return acc + x }, 0.0)
return sum / float64(len(as))
})
result := windowAvg(input)
expected := []float64{3.0, 3.5, 4.0, 4.5, 5.0}
assert.Equal(t, expected, result)
})
t.Run("Check if suffix is sorted", func(t *testing.T) {
input := []int{1, 2, 3, 2, 1}
isSorted := Extend[[]int, []bool](func(as []int) bool {
for i := 1; i < len(as); i++ {
if as[i] < as[i-1] {
return false
}
}
return true
})
result := isSorted(input)
expected := []bool{false, false, false, false, true}
assert.Equal(t, expected, result)
})
t.Run("Count remaining elements", func(t *testing.T) {
events := []string{"start", "middle", "end"}
remaining := Extend[[]string, []int](Size[[]string, string])
result := remaining(events)
expected := []int{3, 2, 1}
assert.Equal(t, expected, result)
})
}

View File

@@ -23,12 +23,45 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Of constructs a single element array
// Of constructs a single element NonEmptyArray.
// This is the simplest way to create a NonEmptyArray with exactly one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - first: The single element to include in the array
//
// Returns:
// - NonEmptyArray[A]: A NonEmptyArray containing only the provided element
//
// Example:
//
// arr := Of(42) // NonEmptyArray[int]{42}
// str := Of("hello") // NonEmptyArray[string]{"hello"}
func Of[A any](first A) NonEmptyArray[A] {
return G.Of[NonEmptyArray[A]](first)
}
// From constructs a [NonEmptyArray] from a set of variadic arguments
// From constructs a NonEmptyArray from a set of variadic arguments.
// The first argument is required to ensure the array is non-empty, and additional
// elements can be provided as variadic arguments.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - first: The first element (required to ensure non-emptiness)
// - data: Additional elements (optional)
//
// Returns:
// - NonEmptyArray[A]: A NonEmptyArray containing all provided elements
//
// Example:
//
// arr1 := From(1) // NonEmptyArray[int]{1}
// arr2 := From(1, 2, 3) // NonEmptyArray[int]{1, 2, 3}
// arr3 := From("a", "b", "c") // NonEmptyArray[string]{"a", "b", "c"}
func From[A any](first A, data ...A) NonEmptyArray[A] {
count := len(data)
if count == 0 {
@@ -41,79 +74,358 @@ func From[A any](first A, data ...A) NonEmptyArray[A] {
return buffer
}
// IsEmpty always returns false for NonEmptyArray since it's guaranteed to have at least one element.
// This function exists for API consistency with regular arrays.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - _: The NonEmptyArray (unused, as the result is always false)
//
// Returns:
// - bool: Always false
//
//go:inline
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
return false
}
// IsNonEmpty always returns true for NonEmptyArray since it's guaranteed to have at least one element.
// This function exists for API consistency with regular arrays.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - _: The NonEmptyArray (unused, as the result is always true)
//
// Returns:
// - bool: Always true
//
//go:inline
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
return true
}
// MonadMap applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
// This is the monadic version of Map that takes the array as the first parameter.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - as: The input NonEmptyArray
// - f: The function to apply to each element
//
// Returns:
// - NonEmptyArray[B]: A new NonEmptyArray with the transformed elements
//
// Example:
//
// arr := From(1, 2, 3)
// doubled := MonadMap(arr, func(x int) int { return x * 2 }) // NonEmptyArray[int]{2, 4, 6}
//
//go:inline
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
}
// Map applies a function to each element of a NonEmptyArray, returning a new NonEmptyArray with the results.
// This is the curried version that returns a function.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: The function to apply to each element
//
// Returns:
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
//
// Example:
//
// double := Map(func(x int) int { return x * 2 })
// result := double(From(1, 2, 3)) // NonEmptyArray[int]{2, 4, 6}
//
//go:inline
func Map[A, B any](f func(a A) B) Operator[A, B] {
return G.Map[NonEmptyArray[A], NonEmptyArray[B]](f)
}
// Reduce applies a function to each element of a NonEmptyArray from left to right,
// accumulating a result starting from an initial value.
//
// Type Parameters:
// - A: The element type of the array
// - B: The accumulator type
//
// Parameters:
// - f: The reducer function that takes (accumulator, element) and returns a new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
//
// Example:
//
// sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
// result := sum(From(1, 2, 3, 4)) // 10
//
// concat := Reduce(func(acc string, x string) string { return acc + x }, "")
// result := concat(From("a", "b", "c")) // "abc"
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.Reduce(as, f, initial)
}
}
// ReduceRight applies a function to each element of a NonEmptyArray from right to left,
// accumulating a result starting from an initial value.
//
// Type Parameters:
// - A: The element type of the array
// - B: The accumulator type
//
// Parameters:
// - f: The reducer function that takes (element, accumulator) and returns a new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - func(NonEmptyArray[A]) B: A function that reduces the array to a single value
//
// Example:
//
// concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
// result := concat(From("a", "b", "c")) // "cba"
func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.ReduceRight(as, f, initial)
}
}
// Tail returns all elements of a NonEmptyArray except the first one.
// Returns an empty slice if the array has only one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - []A: A slice containing all elements except the first (may be empty)
//
// Example:
//
// arr := From(1, 2, 3, 4)
// tail := Tail(arr) // []int{2, 3, 4}
//
// single := From(1)
// tail := Tail(single) // []int{}
//
//go:inline
func Tail[A any](as NonEmptyArray[A]) []A {
return as[1:]
}
// Head returns the first element of a NonEmptyArray.
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The first element
//
// Example:
//
// arr := From(1, 2, 3)
// first := Head(arr) // 1
//
//go:inline
func Head[A any](as NonEmptyArray[A]) A {
return as[0]
}
// First returns the first element of a NonEmptyArray.
// This is an alias for Head.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The first element
//
// Example:
//
// arr := From(1, 2, 3)
// first := First(arr) // 1
//
//go:inline
func First[A any](as NonEmptyArray[A]) A {
return as[0]
}
// Last returns the last element of a NonEmptyArray.
// This operation is always safe since NonEmptyArray is guaranteed to have at least one element.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The last element
//
// Example:
//
// arr := From(1, 2, 3)
// last := Last(arr) // 3
//
//go:inline
func Last[A any](as NonEmptyArray[A]) A {
return as[len(as)-1]
}
// Size returns the number of elements in a NonEmptyArray.
// The result is always at least 1.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - int: The number of elements (always >= 1)
//
// Example:
//
// arr := From(1, 2, 3)
// size := Size(arr) // 3
//
//go:inline
func Size[A any](as NonEmptyArray[A]) int {
return G.Size(as)
}
// Flatten flattens a NonEmptyArray of NonEmptyArrays into a single NonEmptyArray.
// This operation concatenates all inner arrays into one.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - mma: A NonEmptyArray of NonEmptyArrays
//
// Returns:
// - NonEmptyArray[A]: A flattened NonEmptyArray containing all elements
//
// Example:
//
// nested := From(From(1, 2), From(3, 4), From(5))
// flat := Flatten(nested) // NonEmptyArray[int]{1, 2, 3, 4, 5}
func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
return G.Flatten(mma)
}
// MonadChain applies a function that returns a NonEmptyArray to each element and flattens the results.
// This is the monadic bind operation (flatMap) that takes the array as the first parameter.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - fa: The input NonEmptyArray
// - f: A function that takes an element and returns a NonEmptyArray
//
// Returns:
// - NonEmptyArray[B]: The flattened result
//
// Example:
//
// arr := From(1, 2, 3)
// result := MonadChain(arr, func(x int) NonEmptyArray[int] {
// return From(x, x*10)
// }) // NonEmptyArray[int]{1, 10, 2, 20, 3, 30}
func MonadChain[A, B any](fa NonEmptyArray[A], f Kleisli[A, B]) NonEmptyArray[B] {
return G.MonadChain(fa, f)
}
// Chain applies a function that returns a NonEmptyArray to each element and flattens the results.
// This is the curried version of MonadChain.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: A function that takes an element and returns a NonEmptyArray
//
// Returns:
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
//
// Example:
//
// duplicate := Chain(func(x int) NonEmptyArray[int] { return From(x, x) })
// result := duplicate(From(1, 2, 3)) // NonEmptyArray[int]{1, 1, 2, 2, 3, 3}
func Chain[A, B any](f func(A) NonEmptyArray[B]) Operator[A, B] {
return G.Chain[NonEmptyArray[A]](f)
}
// MonadAp applies a NonEmptyArray of functions to a NonEmptyArray of values.
// Each function is applied to each value, producing a cartesian product of results.
//
// Type Parameters:
// - B: The output element type
// - A: The input element type
//
// Parameters:
// - fab: A NonEmptyArray of functions
// - fa: A NonEmptyArray of values
//
// Returns:
// - NonEmptyArray[B]: The result of applying all functions to all values
//
// Example:
//
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
// vals := From(1, 2)
// result := MonadAp(fns, vals) // NonEmptyArray[int]{2, 4, 11, 12}
func MonadAp[B, A any](fab NonEmptyArray[func(A) B], fa NonEmptyArray[A]) NonEmptyArray[B] {
return G.MonadAp[NonEmptyArray[B]](fab, fa)
}
// Ap applies a NonEmptyArray of functions to a NonEmptyArray of values.
// This is the curried version of MonadAp.
//
// Type Parameters:
// - B: The output element type
// - A: The input element type
//
// Parameters:
// - fa: A NonEmptyArray of values
//
// Returns:
// - func(NonEmptyArray[func(A) B]) NonEmptyArray[B]: A function that applies functions to the values
//
// Example:
//
// vals := From(1, 2)
// applyTo := Ap[int](vals)
// fns := From(func(x int) int { return x * 2 }, func(x int) int { return x + 10 })
// result := applyTo(fns) // NonEmptyArray[int]{2, 4, 11, 12}
func Ap[B, A any](fa NonEmptyArray[A]) func(NonEmptyArray[func(A) B]) NonEmptyArray[B] {
return G.Ap[NonEmptyArray[B], NonEmptyArray[func(A) B]](fa)
}
@@ -136,7 +448,23 @@ func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
}
}
// Prepend prepends a single value to an array
// Prepend prepends a single value to the beginning of a NonEmptyArray.
// Returns a new NonEmptyArray with the value at the front.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - head: The value to prepend
//
// Returns:
// - EM.Endomorphism[NonEmptyArray[A]]: A function that prepends the value to a NonEmptyArray
//
// Example:
//
// arr := From(2, 3, 4)
// prepend1 := Prepend(1)
// result := prepend1(arr) // NonEmptyArray[int]{1, 2, 3, 4}
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
}
@@ -226,3 +554,59 @@ func ToNonEmptyArray[A any](as []A) Option[NonEmptyArray[A]] {
}
return option.Some(NonEmptyArray[A](as))
}
// Extract returns the first element of a NonEmptyArray.
// This is an alias for Head and is part of the Comonad interface.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: The input NonEmptyArray
//
// Returns:
// - A: The first element
//
// Example:
//
// arr := From(1, 2, 3)
// first := Extract(arr) // 1
//
//go:inline
func Extract[A any](as NonEmptyArray[A]) A {
return Head(as)
}
// Extend applies a function to all suffixes of a NonEmptyArray.
// For each position i, it applies the function to the subarray starting at position i.
// This is part of the Comonad interface.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: A function that takes a NonEmptyArray and returns a value
//
// Returns:
// - Operator[A, B]: A function that transforms NonEmptyArray[A] to NonEmptyArray[B]
//
// Example:
//
// arr := From(1, 2, 3, 4)
// sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
// sum := 0
// for _, x := range xs {
// sum += x
// }
// return sum
// })
// result := sumSuffix(arr) // NonEmptyArray[int]{10, 9, 7, 4}
// // [1,2,3,4] -> 10, [2,3,4] -> 9, [3,4] -> 7, [4] -> 4
//
//go:inline
func Extend[A, B any](f func(NonEmptyArray[A]) B) Operator[A, B] {
return func(as NonEmptyArray[A]) NonEmptyArray[B] {
return G.MakeBy[NonEmptyArray[B]](len(as), func(i int) B { return f(as[i:]) })
}
}

View File

@@ -16,10 +16,13 @@
package nonempty
import (
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
STR "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -368,3 +371,522 @@ func TestToNonEmptyArrayUseCases(t *testing.T) {
assert.Equal(t, "default", result2)
})
}
// TestOf tests the Of function
func TestOf(t *testing.T) {
t.Run("Create single element array with int", func(t *testing.T) {
arr := Of(42)
assert.Equal(t, 1, Size(arr))
assert.Equal(t, 42, Head(arr))
})
t.Run("Create single element array with string", func(t *testing.T) {
arr := Of("hello")
assert.Equal(t, 1, Size(arr))
assert.Equal(t, "hello", Head(arr))
})
t.Run("Create single element array with struct", func(t *testing.T) {
type Person struct {
Name string
Age int
}
person := Person{Name: "Alice", Age: 30}
arr := Of(person)
assert.Equal(t, 1, Size(arr))
assert.Equal(t, "Alice", Head(arr).Name)
})
}
// TestFrom tests the From function
func TestFrom(t *testing.T) {
t.Run("Create array with single element", func(t *testing.T) {
arr := From(1)
assert.Equal(t, 1, Size(arr))
assert.Equal(t, 1, Head(arr))
})
t.Run("Create array with multiple elements", func(t *testing.T) {
arr := From(1, 2, 3, 4, 5)
assert.Equal(t, 5, Size(arr))
assert.Equal(t, 1, Head(arr))
assert.Equal(t, 5, Last(arr))
})
t.Run("Create array with strings", func(t *testing.T) {
arr := From("a", "b", "c")
assert.Equal(t, 3, Size(arr))
assert.Equal(t, "a", Head(arr))
assert.Equal(t, "c", Last(arr))
})
}
// TestIsEmpty tests the IsEmpty function
func TestIsEmpty(t *testing.T) {
t.Run("IsEmpty always returns false", func(t *testing.T) {
arr := From(1, 2, 3)
assert.False(t, IsEmpty(arr))
})
t.Run("IsEmpty returns false for single element", func(t *testing.T) {
arr := Of(1)
assert.False(t, IsEmpty(arr))
})
}
// TestIsNonEmpty tests the IsNonEmpty function
func TestIsNonEmpty(t *testing.T) {
t.Run("IsNonEmpty always returns true", func(t *testing.T) {
arr := From(1, 2, 3)
assert.True(t, IsNonEmpty(arr))
})
t.Run("IsNonEmpty returns true for single element", func(t *testing.T) {
arr := Of(1)
assert.True(t, IsNonEmpty(arr))
})
}
// TestMonadMap tests the MonadMap function
func TestMonadMap(t *testing.T) {
t.Run("Map integers to doubles", func(t *testing.T) {
arr := From(1, 2, 3, 4)
result := MonadMap(arr, func(x int) int { return x * 2 })
assert.Equal(t, 4, Size(result))
assert.Equal(t, 2, Head(result))
assert.Equal(t, 8, Last(result))
})
t.Run("Map strings to lengths", func(t *testing.T) {
arr := From("a", "bb", "ccc")
result := MonadMap(arr, func(s string) int { return len(s) })
assert.Equal(t, 3, Size(result))
assert.Equal(t, 1, Head(result))
assert.Equal(t, 3, Last(result))
})
t.Run("Map single element", func(t *testing.T) {
arr := Of(5)
result := MonadMap(arr, func(x int) int { return x * 10 })
assert.Equal(t, 1, Size(result))
assert.Equal(t, 50, Head(result))
})
}
// TestMap tests the Map function
func TestMap(t *testing.T) {
t.Run("Curried map with integers", func(t *testing.T) {
double := Map(func(x int) int { return x * 2 })
arr := From(1, 2, 3)
result := double(arr)
assert.Equal(t, 3, Size(result))
assert.Equal(t, 2, Head(result))
assert.Equal(t, 6, Last(result))
})
t.Run("Curried map with strings", func(t *testing.T) {
toUpper := Map(func(s string) string { return s + "!" })
arr := From("hello", "world")
result := toUpper(arr)
assert.Equal(t, 2, Size(result))
assert.Equal(t, "hello!", Head(result))
assert.Equal(t, "world!", Last(result))
})
}
// TestReduce tests the Reduce function
func TestReduce(t *testing.T) {
t.Run("Sum integers", func(t *testing.T) {
sum := Reduce(func(acc int, x int) int { return acc + x }, 0)
arr := From(1, 2, 3, 4, 5)
result := sum(arr)
assert.Equal(t, 15, result)
})
t.Run("Concatenate strings", func(t *testing.T) {
concat := Reduce(func(acc string, x string) string { return acc + x }, "")
arr := From("a", "b", "c")
result := concat(arr)
assert.Equal(t, "abc", result)
})
t.Run("Product of numbers", func(t *testing.T) {
product := Reduce(func(acc int, x int) int { return acc * x }, 1)
arr := From(2, 3, 4)
result := product(arr)
assert.Equal(t, 24, result)
})
t.Run("Reduce single element", func(t *testing.T) {
sum := Reduce(func(acc int, x int) int { return acc + x }, 10)
arr := Of(5)
result := sum(arr)
assert.Equal(t, 15, result)
})
}
// TestReduceRight tests the ReduceRight function
func TestReduceRight(t *testing.T) {
t.Run("Concatenate strings right to left", func(t *testing.T) {
concat := ReduceRight(func(x string, acc string) string { return acc + x }, "")
arr := From("a", "b", "c")
result := concat(arr)
assert.Equal(t, "cba", result)
})
t.Run("Build list right to left", func(t *testing.T) {
buildList := ReduceRight(func(x int, acc []int) []int { return append(acc, x) }, []int{})
arr := From(1, 2, 3)
result := buildList(arr)
assert.Equal(t, []int{3, 2, 1}, result)
})
}
// TestTail tests the Tail function
func TestTail(t *testing.T) {
t.Run("Get tail of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3, 4)
tail := Tail(arr)
assert.Equal(t, 3, len(tail))
assert.Equal(t, []int{2, 3, 4}, tail)
})
t.Run("Get tail of single element array", func(t *testing.T) {
arr := Of(1)
tail := Tail(arr)
assert.Equal(t, 0, len(tail))
assert.Equal(t, []int{}, tail)
})
t.Run("Get tail of two element array", func(t *testing.T) {
arr := From(1, 2)
tail := Tail(arr)
assert.Equal(t, 1, len(tail))
assert.Equal(t, []int{2}, tail)
})
}
// TestHead tests the Head function
func TestHead(t *testing.T) {
t.Run("Get head of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3)
head := Head(arr)
assert.Equal(t, 1, head)
})
t.Run("Get head of single element array", func(t *testing.T) {
arr := Of(42)
head := Head(arr)
assert.Equal(t, 42, head)
})
t.Run("Get head of string array", func(t *testing.T) {
arr := From("first", "second", "third")
head := Head(arr)
assert.Equal(t, "first", head)
})
}
// TestFirst tests the First function
func TestFirst(t *testing.T) {
t.Run("First is alias for Head", func(t *testing.T) {
arr := From(1, 2, 3)
assert.Equal(t, Head(arr), First(arr))
})
t.Run("Get first element", func(t *testing.T) {
arr := From("a", "b", "c")
first := First(arr)
assert.Equal(t, "a", first)
})
}
// TestLast tests the Last function
func TestLast(t *testing.T) {
t.Run("Get last of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3, 4, 5)
last := Last(arr)
assert.Equal(t, 5, last)
})
t.Run("Get last of single element array", func(t *testing.T) {
arr := Of(42)
last := Last(arr)
assert.Equal(t, 42, last)
})
t.Run("Get last of string array", func(t *testing.T) {
arr := From("first", "second", "third")
last := Last(arr)
assert.Equal(t, "third", last)
})
}
// TestSize tests the Size function
func TestSize(t *testing.T) {
t.Run("Size of multi-element array", func(t *testing.T) {
arr := From(1, 2, 3, 4, 5)
size := Size(arr)
assert.Equal(t, 5, size)
})
t.Run("Size of single element array", func(t *testing.T) {
arr := Of(1)
size := Size(arr)
assert.Equal(t, 1, size)
})
t.Run("Size of large array", func(t *testing.T) {
elements := make([]int, 1000)
arr := From(1, elements...)
size := Size(arr)
assert.Equal(t, 1001, size)
})
}
// TestFlatten tests the Flatten function
func TestFlatten(t *testing.T) {
t.Run("Flatten nested arrays", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5))
flat := Flatten(nested)
assert.Equal(t, 5, Size(flat))
assert.Equal(t, 1, Head(flat))
assert.Equal(t, 5, Last(flat))
})
t.Run("Flatten single nested array", func(t *testing.T) {
nested := Of(From(1, 2, 3))
flat := Flatten(nested)
assert.Equal(t, 3, Size(flat))
assert.Equal(t, []int{1, 2, 3}, []int(flat))
})
t.Run("Flatten arrays of different sizes", func(t *testing.T) {
nested := From(Of(1), From(2, 3, 4), From(5, 6))
flat := Flatten(nested)
assert.Equal(t, 6, Size(flat))
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, []int(flat))
})
}
// TestMonadChain tests the MonadChain function
func TestMonadChain(t *testing.T) {
t.Run("Chain with duplication", func(t *testing.T) {
arr := From(1, 2, 3)
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
return From(x, x*10)
})
assert.Equal(t, 6, Size(result))
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, []int(result))
})
t.Run("Chain with expansion", func(t *testing.T) {
arr := From(1, 2)
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
return From(x, x+1, x+2)
})
assert.Equal(t, 6, Size(result))
assert.Equal(t, []int{1, 2, 3, 2, 3, 4}, []int(result))
})
t.Run("Chain single element", func(t *testing.T) {
arr := Of(5)
result := MonadChain(arr, func(x int) NonEmptyArray[int] {
return From(x, x*2)
})
assert.Equal(t, 2, Size(result))
assert.Equal(t, []int{5, 10}, []int(result))
})
}
// TestChain tests the Chain function
func TestChain(t *testing.T) {
t.Run("Curried chain with duplication", func(t *testing.T) {
duplicate := Chain(func(x int) NonEmptyArray[int] {
return From(x, x)
})
arr := From(1, 2, 3)
result := duplicate(arr)
assert.Equal(t, 6, Size(result))
assert.Equal(t, []int{1, 1, 2, 2, 3, 3}, []int(result))
})
t.Run("Curried chain with transformation", func(t *testing.T) {
expand := Chain(func(x int) NonEmptyArray[string] {
return Of(fmt.Sprintf("%d", x))
})
arr := From(1, 2, 3)
result := expand(arr)
assert.Equal(t, 3, Size(result))
assert.Equal(t, "1", Head(result))
})
}
// TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) {
t.Run("Apply functions to values", func(t *testing.T) {
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
vals := From(1, 2)
result := MonadAp(fns, vals)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
})
t.Run("Apply single function to multiple values", func(t *testing.T) {
fns := Of(func(x int) int { return x * 3 })
vals := From(1, 2, 3)
result := MonadAp(fns, vals)
assert.Equal(t, 3, Size(result))
assert.Equal(t, []int{3, 6, 9}, []int(result))
})
}
// TestAp tests the Ap function
func TestAp(t *testing.T) {
t.Run("Curried apply", func(t *testing.T) {
vals := From(1, 2)
applyTo := Ap[int](vals)
fns := From(
func(x int) int { return x * 2 },
func(x int) int { return x + 10 },
)
result := applyTo(fns)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{2, 4, 11, 12}, []int(result))
})
}
// TestFoldMap tests the FoldMap function
func TestFoldMap(t *testing.T) {
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := From(1, 2, 3, 4)
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
})
t.Run("FoldMap with string concatenation", func(t *testing.T) {
concatSemigroup := STR.Semigroup
arr := From(1, 2, 3)
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
assert.Equal(t, "123", result)
})
}
// TestFold tests the Fold function
func TestFold(t *testing.T) {
t.Run("Fold with sum semigroup", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := From(1, 2, 3, 4, 5)
result := Fold(sumSemigroup)(arr)
assert.Equal(t, 15, result)
})
t.Run("Fold with string concatenation", func(t *testing.T) {
concatSemigroup := STR.Semigroup
arr := From("a", "b", "c")
result := Fold(concatSemigroup)(arr)
assert.Equal(t, "abc", result)
})
t.Run("Fold single element", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := Of(42)
result := Fold(sumSemigroup)(arr)
assert.Equal(t, 42, result)
})
}
// TestPrepend tests the Prepend function
func TestPrepend(t *testing.T) {
t.Run("Prepend to multi-element array", func(t *testing.T) {
arr := From(2, 3, 4)
prepend1 := Prepend(1)
result := prepend1(arr)
assert.Equal(t, 4, Size(result))
assert.Equal(t, 1, Head(result))
assert.Equal(t, 4, Last(result))
})
t.Run("Prepend to single element array", func(t *testing.T) {
arr := Of(2)
prepend1 := Prepend(1)
result := prepend1(arr)
assert.Equal(t, 2, Size(result))
assert.Equal(t, []int{1, 2}, []int(result))
})
t.Run("Prepend string", func(t *testing.T) {
arr := From("world")
prependHello := Prepend("hello")
result := prependHello(arr)
assert.Equal(t, 2, Size(result))
assert.Equal(t, "hello", Head(result))
})
}
// TestExtract tests the Extract function
func TestExtract(t *testing.T) {
t.Run("Extract from multi-element array", func(t *testing.T) {
arr := From(1, 2, 3)
result := Extract(arr)
assert.Equal(t, 1, result)
})
t.Run("Extract from single element array", func(t *testing.T) {
arr := Of(42)
result := Extract(arr)
assert.Equal(t, 42, result)
})
t.Run("Extract is same as Head", func(t *testing.T) {
arr := From("a", "b", "c")
assert.Equal(t, Head(arr), Extract(arr))
})
}
// TestExtend tests the Extend function
func TestExtend(t *testing.T) {
t.Run("Extend with sum of suffixes", func(t *testing.T) {
arr := From(1, 2, 3, 4)
sumSuffix := Extend(func(xs NonEmptyArray[int]) int {
sum := 0
for _, x := range xs {
sum += x
}
return sum
})
result := sumSuffix(arr)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{10, 9, 7, 4}, []int(result))
})
t.Run("Extend with head of suffixes", func(t *testing.T) {
arr := From(1, 2, 3)
getHeads := Extend(Head[int])
result := getHeads(arr)
assert.Equal(t, 3, Size(result))
assert.Equal(t, []int{1, 2, 3}, []int(result))
})
t.Run("Extend with size of suffixes", func(t *testing.T) {
arr := From("a", "b", "c", "d")
getSizes := Extend(Size[string])
result := getSizes(arr)
assert.Equal(t, 4, Size(result))
assert.Equal(t, []int{4, 3, 2, 1}, []int(result))
})
t.Run("Extend single element", func(t *testing.T) {
arr := Of(5)
double := Extend(func(xs NonEmptyArray[int]) int {
return Head(xs) * 2
})
result := double(arr)
assert.Equal(t, 1, Size(result))
assert.Equal(t, 10, Head(result))
})
}

View File

@@ -519,6 +519,8 @@ func RunAll(testcases map[string]Reader) Reader {
// by providing a function that converts R2 to R1. This allows you to focus a test on a
// specific property or subset of a larger data structure.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This is particularly useful when you have an assertion that operates on a specific field
// or property, and you want to apply it to a complete object. Instead of extracting the
// property and then asserting on it, you can transform the assertion to work directly

View File

@@ -1,7 +1,81 @@
// Copyright (c) 2024 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 builder provides a generic Builder pattern interface for constructing
// complex objects with validation.
//
// The Builder pattern is useful when:
// - Object construction requires multiple steps
// - Construction may fail with validation errors
// - You want to separate construction logic from the object itself
//
// Example usage:
//
// type PersonBuilder struct {
// name string
// age int
// }
//
// func (b PersonBuilder) Build() result.Result[Person] {
// if b.name == "" {
// return result.Error[Person](errors.New("name is required"))
// }
// if b.age < 0 {
// return result.Error[Person](errors.New("age must be non-negative"))
// }
// return result.Of(Person{Name: b.name, Age: b.age})
// }
package builder
type (
// Builder is a generic interface for the Builder pattern that constructs
// objects of type T with validation.
//
// The Build method returns a Result[T] which can be either:
// - Success: containing the constructed object of type T
// - Error: containing an error if validation or construction fails
//
// This allows builders to perform validation and return meaningful errors
// during the construction process, making it explicit that object creation
// may fail.
//
// Type Parameters:
// - T: The type of object being built
//
// Example:
//
// type ConfigBuilder struct {
// host string
// port int
// }
//
// func (b ConfigBuilder) Build() result.Result[Config] {
// if b.host == "" {
// return result.Error[Config](errors.New("host is required"))
// }
// if b.port <= 0 || b.port > 65535 {
// return result.Error[Config](errors.New("invalid port"))
// }
// return result.Of(Config{Host: b.host, Port: b.port})
// }
Builder[T any] interface {
// Build constructs and validates an object of type T.
//
// Returns:
// - Result[T]: A Result containing either the successfully built object
// or an error if validation or construction fails.
Build() Result[T]
}
)

374
v2/builder/builder_test.go Normal file
View File

@@ -0,0 +1,374 @@
// Copyright (c) 2024 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 builder
import (
"errors"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// Test types for demonstration
type Person struct {
Name string
Age int
}
type PersonBuilder struct {
name string
age int
}
func (b PersonBuilder) WithName(name string) PersonBuilder {
b.name = name
return b
}
func (b PersonBuilder) WithAge(age int) PersonBuilder {
b.age = age
return b
}
func (b PersonBuilder) Build() Result[Person] {
if b.name == "" {
return result.Left[Person](errors.New("name is required"))
}
if b.age < 0 {
return result.Left[Person](errors.New("age must be non-negative"))
}
if b.age > 150 {
return result.Left[Person](errors.New("age must be realistic"))
}
return result.Of(Person{Name: b.name, Age: b.age})
}
func NewPersonBuilder(p Person) PersonBuilder {
return PersonBuilder{name: p.Name, age: p.Age}
}
// Config example for additional test coverage
type Config struct {
Host string
Port int
}
type ConfigBuilder struct {
host string
port int
}
func (b ConfigBuilder) WithHost(host string) ConfigBuilder {
b.host = host
return b
}
func (b ConfigBuilder) WithPort(port int) ConfigBuilder {
b.port = port
return b
}
func (b ConfigBuilder) Build() Result[Config] {
if b.host == "" {
return result.Left[Config](errors.New("host is required"))
}
if b.port <= 0 || b.port > 65535 {
return result.Left[Config](errors.New("port must be between 1 and 65535"))
}
return result.Of(Config{Host: b.host, Port: b.port})
}
func NewConfigBuilder(c Config) ConfigBuilder {
return ConfigBuilder{host: c.Host, port: c.Port}
}
// Tests for Builder interface
func TestBuilder_SuccessfulBuild(t *testing.T) {
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
res := builder.Build()
assert.True(t, result.IsRight(res), "Build should succeed")
person := result.ToOption(res)
assert.True(t, O.IsSome(person), "Result should contain a person")
p := O.GetOrElse(func() Person { return Person{} })(person)
assert.Equal(t, "Alice", p.Name)
assert.Equal(t, 30, p.Age)
}
func TestBuilder_ValidationFailure_MissingName(t *testing.T) {
builder := PersonBuilder{}.WithAge(30)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when name is missing")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "name is required", err.Error())
}
func TestBuilder_ValidationFailure_NegativeAge(t *testing.T) {
builder := PersonBuilder{}.
WithName("Bob").
WithAge(-5)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when age is negative")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "age must be non-negative", err.Error())
}
func TestBuilder_ValidationFailure_UnrealisticAge(t *testing.T) {
builder := PersonBuilder{}.
WithName("Charlie").
WithAge(200)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when age is unrealistic")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "age must be realistic", err.Error())
}
func TestBuilder_ConfigSuccessfulBuild(t *testing.T) {
builder := ConfigBuilder{}.
WithHost("localhost").
WithPort(8080)
res := builder.Build()
assert.True(t, result.IsRight(res), "Build should succeed")
config := result.ToOption(res)
assert.True(t, O.IsSome(config), "Result should contain a config")
c := O.GetOrElse(func() Config { return Config{} })(config)
assert.Equal(t, "localhost", c.Host)
assert.Equal(t, 8080, c.Port)
}
func TestBuilder_ConfigValidationFailure_MissingHost(t *testing.T) {
builder := ConfigBuilder{}.WithPort(8080)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when host is missing")
err := result.Fold(
func(e error) error { return e },
func(Config) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "host is required", err.Error())
}
func TestBuilder_ConfigValidationFailure_InvalidPort(t *testing.T) {
tests := []struct {
name string
port int
}{
{"zero port", 0},
{"negative port", -1},
{"port too large", 70000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := ConfigBuilder{}.
WithHost("localhost").
WithPort(tt.port)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail for invalid port")
err := result.Fold(
func(e error) error { return e },
func(Config) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "port must be between 1 and 65535", err.Error())
})
}
}
// Tests for BuilderPrism
func TestBuilderPrism_GetOption_ValidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
personOpt := prism.GetOption(builder)
assert.True(t, O.IsSome(personOpt), "GetOption should return Some for valid builder")
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
}
func TestBuilderPrism_GetOption_InvalidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.WithAge(30) // Missing name
personOpt := prism.GetOption(builder)
assert.True(t, O.IsNone(personOpt), "GetOption should return None for invalid builder")
}
func TestBuilderPrism_ReverseGet(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
person := Person{Name: "Bob", Age: 25}
builder := prism.ReverseGet(person)
assert.Equal(t, "Bob", builder.name)
assert.Equal(t, 25, builder.age)
// Verify the builder can build the same person
res := builder.Build()
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
rebuilt := O.GetOrElse(func() Person { return Person{} })(result.ToOption(res))
assert.Equal(t, person, rebuilt)
}
func TestBuilderPrism_RoundTrip_ValidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
originalBuilder := PersonBuilder{}.
WithName("Charlie").
WithAge(35)
// Extract person from builder
personOpt := prism.GetOption(originalBuilder)
assert.True(t, O.IsSome(personOpt), "Should extract person from valid builder")
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
// Reconstruct builder from person
rebuiltBuilder := prism.ReverseGet(person)
// Verify the rebuilt builder produces the same person
rebuiltRes := rebuiltBuilder.Build()
assert.True(t, result.IsRight(rebuiltRes), "Rebuilt builder should be valid")
rebuiltPerson := O.GetOrElse(func() Person { return Person{} })(result.ToOption(rebuiltRes))
assert.Equal(t, person, rebuiltPerson)
}
func TestBuilderPrism_ConfigPrism(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
builder := ConfigBuilder{}.
WithHost("example.com").
WithPort(443)
configOpt := prism.GetOption(builder)
assert.True(t, O.IsSome(configOpt), "GetOption should return Some for valid config builder")
config := O.GetOrElse(func() Config { return Config{} })(configOpt)
assert.Equal(t, "example.com", config.Host)
assert.Equal(t, 443, config.Port)
}
func TestBuilderPrism_ConfigPrism_InvalidBuilder(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
builder := ConfigBuilder{}.WithPort(8080) // Missing host
configOpt := prism.GetOption(builder)
assert.True(t, O.IsNone(configOpt), "GetOption should return None for invalid config builder")
}
func TestBuilderPrism_ConfigPrism_ReverseGet(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
config := Config{Host: "api.example.com", Port: 9000}
builder := prism.ReverseGet(config)
assert.Equal(t, "api.example.com", builder.host)
assert.Equal(t, 9000, builder.port)
// Verify the builder can build the same config
res := builder.Build()
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
rebuilt := O.GetOrElse(func() Config { return Config{} })(result.ToOption(res))
assert.Equal(t, config, rebuilt)
}
// Benchmark tests
func BenchmarkBuilder_SuccessfulBuild(b *testing.B) {
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.Build()
}
}
func BenchmarkBuilder_FailedBuild(b *testing.B) {
builder := PersonBuilder{}.WithAge(30) // Missing name
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.Build()
}
}
func BenchmarkBuilderPrism_GetOption(b *testing.B) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prism.GetOption(builder)
}
}
func BenchmarkBuilderPrism_ReverseGet(b *testing.B) {
prism := BuilderPrism(NewPersonBuilder)
person := Person{Name: "Bob", Age: 25}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prism.ReverseGet(person)
}
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2024 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 builder
import (
@@ -6,7 +21,61 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// BuilderPrism createa a [Prism] that converts between a builder and its type
// BuilderPrism creates a [Prism] that converts between a builder and its built type.
//
// A Prism is an optic that focuses on a case of a sum type, providing bidirectional
// conversion with the possibility of failure. This function creates a prism that:
// - Extracts: Attempts to build the object from the builder (may fail)
// - Constructs: Creates a builder from a valid object (always succeeds)
//
// The extraction direction (builder -> object) uses the Build method and converts
// the Result to an Option, where errors become None. The construction direction
// (object -> builder) uses the provided creator function.
//
// Type Parameters:
// - T: The type of the object being built
// - B: The builder type that implements Builder[T]
//
// Parameters:
// - creator: A function that creates a builder from a valid object of type T.
// This function should initialize the builder with all fields from the object.
//
// Returns:
// - Prism[B, T]: A prism that can convert between the builder and the built type.
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// type PersonBuilder struct {
// name string
// age int
// }
//
// func (b PersonBuilder) Build() result.Result[Person] {
// if b.name == "" {
// return result.Error[Person](errors.New("name required"))
// }
// return result.Of(Person{Name: b.name, Age: b.age})
// }
//
// func NewPersonBuilder(p Person) PersonBuilder {
// return PersonBuilder{name: p.Name, age: p.Age}
// }
//
// // Create a prism for PersonBuilder
// prism := BuilderPrism(NewPersonBuilder)
//
// // Use the prism to extract a Person from a valid builder
// builder := PersonBuilder{name: "Alice", age: 30}
// person := prism.GetOption(builder) // Some(Person{Name: "Alice", Age: 30})
//
// // Use the prism to create a builder from a Person
// p := Person{Name: "Bob", Age: 25}
// b := prism.ReverseGet(p) // PersonBuilder{name: "Bob", age: 25}
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
}

View File

@@ -4,7 +4,6 @@ import (
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
"github.com/IBM/fp-go/v2/io"
@@ -14,6 +13,7 @@ import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/retry"
)
@@ -241,125 +241,155 @@ func isResetTimeExceeded(ct time.Time) option.Kleisli[openState, openState] {
})
}
// handleSuccessOnClosed handles a successful request when the circuit breaker is in closed state.
// It updates the closed state by recording the success and returns an IO operation that
// modifies the breaker state.
// handleSuccessOnClosed creates a Reader that handles successful requests when the circuit is closed.
// This function is used to update the circuit breaker state after a successful operation completes
// while the circuit is in the closed state.
//
// This function is part of the circuit breaker's state management for the closed state.
// When a request succeeds in closed state:
// 1. The current time is obtained
// 2. The addSuccess function is called with the current time to update the ClosedState
// 3. The updated ClosedState is wrapped in a Right (closed) BreakerState
// 4. The breaker state is modified with the new state
// The function takes a Reader that adds a success record to the ClosedState and lifts it to work
// with BreakerState by mapping over the Right (closed) side of the Either type. This ensures that
// success tracking only affects the closed state and leaves any open state unchanged.
//
// Parameters:
// - currentTime: An IO operation that provides the current time
// - addSuccess: A Reader that takes a time and returns an endomorphism for ClosedState,
// typically resetting failure counters or history
// - addSuccess: A Reader that takes the current time and returns an Endomorphism that updates
// the ClosedState by recording a successful operation. This typically increments a success
// counter or updates a success history.
//
// Returns:
// - An io.Kleisli that takes another io.Kleisli and chains them together.
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
// This allows composing the success handling with other state modifications.
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
// an endomorphism that updates the BreakerState by applying the success update to the closed
// state (if closed) or leaving the state unchanged (if open).
//
// Thread Safety: This function creates IO operations that will atomically modify the
// IORef[BreakerState] when executed. The state modifications are thread-safe.
//
// Type signature:
//
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
// Thread Safety: This is a pure function that creates new state instances. The returned
// endomorphism is safe for concurrent use as it does not mutate its input.
//
// Usage Context:
// - Called when a request succeeds while the circuit is closed
// - Resets failure tracking (counter or history) in the ClosedState
// - Keeps the circuit in closed state
// - Called after a successful request completes while the circuit is closed
// - Updates success metrics/counters in the ClosedState
// - Does not affect the circuit state if it's already open
// - Part of the normal operation flow when the circuit breaker is functioning properly
func handleSuccessOnClosed(
currentTime IO[time.Time],
addSuccess Reader[time.Time, Endomorphism[ClosedState]],
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
) Reader[time.Time, Endomorphism[BreakerState]] {
return F.Flow2(
io.Chain,
identity.Flap[IO[BreakerState]](F.Pipe1(
currentTime,
io.Map(F.Flow2(
addSuccess,
either.Map[openState],
)))),
addSuccess,
either.Map[openState],
)
}
// handleFailureOnClosed handles a failed request when the circuit breaker is in closed state.
// It updates the closed state by recording the failure and checks if the circuit should open.
// handleFailureOnClosed creates a Reader that handles failed requests when the circuit is closed.
// This function manages the critical logic for determining whether a failure should cause the
// circuit breaker to open (transition from closed to open state).
//
// This function is part of the circuit breaker's state management for the closed state.
// When a request fails in closed state:
// 1. The current time is obtained
// 2. The addError function is called to record the failure in the ClosedState
// 3. The checkClosedState function is called to determine if the failure threshold is exceeded
// 4. If the threshold is exceeded (Check returns None):
// - The circuit transitions to open state using openCircuit
// - A new openState is created with resetAt time calculated from the retry policy
// 5. If the threshold is not exceeded (Check returns Some):
// - The circuit remains closed with the updated failure tracking
// The function orchestrates three key operations:
// 1. Records the failure in the ClosedState using addError
// 2. Checks if the failure threshold has been exceeded using checkClosedState
// 3. If threshold exceeded, opens the circuit; otherwise, keeps it closed with updated error count
//
// The decision flow is:
// - Add the error to the closed state's error tracking
// - Check if the updated closed state exceeds the failure threshold
// - If threshold exceeded (checkClosedState returns None):
// - Create a new openState with calculated reset time based on retry policy
// - Transition the circuit to open state (Left side of Either)
// - If threshold not exceeded (checkClosedState returns Some):
// - Keep the circuit closed with the updated error count
// - Continue allowing requests through
//
// Parameters:
// - currentTime: An IO operation that provides the current time
// - addError: A Reader that takes a time and returns an endomorphism for ClosedState,
// recording a failure (incrementing counter or adding to history)
// - checkClosedState: A Reader that takes a time and returns an option.Kleisli that checks
// if the ClosedState should remain closed. Returns Some if circuit stays closed, None if it should open.
// - openCircuit: A Reader that takes a time and returns an openState with calculated resetAt time
// - addError: A Reader that takes the current time and returns an Endomorphism that updates
// the ClosedState by recording a failed operation. This typically increments an error
// counter or adds to an error history.
// - checkClosedState: A Reader that takes the current time and returns an option.Kleisli that
// validates whether the ClosedState is still within acceptable failure thresholds.
// Returns Some(ClosedState) if threshold not exceeded, None if threshold exceeded.
// - openCircuit: A Reader that takes the current time and creates a new openState with
// appropriate reset time calculated from the retry policy. Used when transitioning to open.
//
// Returns:
// - An io.Kleisli that takes another io.Kleisli and chains them together.
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
// This allows composing the failure handling with other state modifications.
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
// an endomorphism that either:
// - Keeps the circuit closed with updated error tracking (if threshold not exceeded)
// - Opens the circuit with calculated reset time (if threshold exceeded)
//
// Thread Safety: This function creates IO operations that will atomically modify the
// IORef[BreakerState] when executed. The state modifications are thread-safe.
//
// Type signature:
//
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
//
// State Transitions:
// - Closed -> Closed: When failure threshold is not exceeded (Some from checkClosedState)
// - Closed -> Open: When failure threshold is exceeded (None from checkClosedState)
// Thread Safety: This is a pure function that creates new state instances. The returned
// endomorphism is safe for concurrent use as it does not mutate its input.
//
// Usage Context:
// - Called when a request fails while the circuit is closed
// - Records the failure in the ClosedState (counter or history)
// - May trigger transition to open state if threshold is exceeded
// - Called after a failed request completes while the circuit is closed
// - Implements the core circuit breaker logic for opening the circuit
// - Determines when to stop allowing requests through to protect the failing service
// - Critical for preventing cascading failures in distributed systems
//
// State Transition:
// - Closed (under threshold) -> Closed (with incremented error count)
// - Closed (at/over threshold) -> Open (with reset time for recovery attempt)
func handleFailureOnClosed(
currentTime IO[time.Time],
addError Reader[time.Time, Endomorphism[ClosedState]],
checkClosedState Reader[time.Time, option.Kleisli[ClosedState, ClosedState]],
openCircuit Reader[time.Time, openState],
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
return F.Flow2(
io.Chain,
identity.Flap[IO[BreakerState]](F.Pipe1(
currentTime,
io.Map(func(ct time.Time) either.Operator[openState, ClosedState, ClosedState] {
return either.Chain(F.Flow3(
addError(ct),
checkClosedState(ct),
option.Fold(
F.Pipe2(
ct,
lazy.Of,
lazy.Map(F.Flow2(
openCircuit,
createOpenCircuit,
)),
),
createClosedCircuit,
),
))
}))),
) Reader[time.Time, Endomorphism[BreakerState]] {
return F.Pipe2(
F.Pipe1(
addError,
reader.ApS(reader.Map[ClosedState], checkClosedState),
),
reader.Chain(F.Flow2(
reader.Map[ClosedState](option.Fold(
F.Pipe2(
openCircuit,
reader.Map[time.Time](createOpenCircuit),
lazy.Of,
),
F.Flow2(
createClosedCircuit,
reader.Of[time.Time],
),
)),
reader.Sequence,
)),
reader.Map[time.Time](either.Chain[openState, ClosedState, ClosedState]),
)
}
func handleErrorOnClosed2[E any](
checkError option.Kleisli[E, E],
onSuccess Reader[time.Time, Endomorphism[BreakerState]],
onFailure Reader[time.Time, Endomorphism[BreakerState]],
) reader.Kleisli[time.Time, E, Endomorphism[BreakerState]] {
return F.Flow3(
checkError,
option.MapTo[E](onFailure),
option.GetOrElse(lazy.Of(onSuccess)),
)
}
func stateModifier(
modify io.Kleisli[Endomorphism[BreakerState], BreakerState],
) reader.Operator[time.Time, Endomorphism[BreakerState], IO[BreakerState]] {
return reader.Map[time.Time](modify)
}
func reportOnClose2(
onClosed ReaderIO[time.Time, Void],
onOpened ReaderIO[time.Time, Void],
) readerio.Operator[time.Time, BreakerState, Void] {
return readerio.Chain(either.Fold(
reader.Of[openState](onOpened),
reader.Of[ClosedState](onClosed),
))
}
func applyAndReportClose2(
currentTime IO[time.Time],
metrics readerio.Operator[time.Time, BreakerState, Void],
) func(io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
return func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
return F.Flow3(
reader.Map[time.Time](modify),
metrics,
readerio.ReadIO[Void](currentTime),
)
}
}
// MakeCircuitBreaker creates a circuit breaker implementation for a higher-kinded type.
@@ -402,6 +432,8 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
chainFirstIOK func(io.Kleisli[T, BreakerState]) func(HKTT) HKTT,
chainFirstLeftIOK func(io.Kleisli[E, BreakerState]) func(HKTT) HKTT,
chainFirstIOK2 func(io.Kleisli[Either[E, T], Void]) func(HKTT) HKTT,
fromIO func(IO[func(HKTT) HKTT]) HKTOP,
flap func(HKTT) func(HKTOP) HKTHKTT,
flatten func(HKTHKTT) HKTT,
@@ -437,47 +469,22 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
reader.Of[HKTT],
)
handleSuccess := handleSuccessOnClosed(currentTime, addSuccess)
handleFailure := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
handleSuccess2 := handleSuccessOnClosed(addSuccess)
handleFailure2 := handleFailureOnClosed(addError, checkClosedState, openCircuit)
handleError2 := handleErrorOnClosed2(checkError, handleSuccess2, handleFailure2)
metricsClose2 := reportOnClose2(metrics.Accept, metrics.Open)
apply2 := applyAndReportClose2(currentTime, metricsClose2)
onClosed := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {
return F.Flow2(
// error case
chainFirstLeftIOK(F.Flow3(
checkError,
option.Fold(
// the error is not applicable, handle as success
F.Pipe2(
modify,
handleSuccess,
lazy.Of,
),
// the error is relevant, record it
F.Pipe2(
modify,
handleFailure,
reader.Of[E],
),
),
// metering
io.ChainFirst(either.Fold(
F.Flow2(
openedAtLens.Get,
metrics.Open,
),
func(c ClosedState) IO[Void] {
return io.Of(function.VOID)
},
)),
)),
// good case
chainFirstIOK(F.Pipe2(
modify,
handleSuccess,
reader.Of[T],
)),
)
return chainFirstIOK2(F.Flow2(
either.Fold(
handleError2,
reader.Of[T](handleSuccess2),
),
apply2(modify),
))
}
onCanary := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {

View File

@@ -5,12 +5,12 @@ import (
"testing"
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioref"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
@@ -452,43 +452,128 @@ func TestIsResetTimeExceeded(t *testing.T) {
// TestHandleSuccessOnClosed tests the handleSuccessOnClosed function
func TestHandleSuccessOnClosed(t *testing.T) {
t.Run("resets failure count on success", func(t *testing.T) {
t.Run("updates closed state with success when circuit is closed", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addSuccess := reader.From1(ClosedState.AddSuccess)
currentTime := vt.Now()
// Create initial state with some failures
now := vt.Now()
// Create a simple addSuccess reader that increments a counter
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
// Create initial closed state
initialClosed := MakeClosedStateCounter(3)
initialClosed = initialClosed.AddError(now)
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
// Apply handleSuccessOnClosed
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
result := endomorphism(initialState)
handler := handleSuccessOnClosed(currentTime, addSuccess)
// Verify the state is still closed
assert.True(t, IsClosed(result), "state should remain closed after success")
// Apply the handler
result := io.Run(handler(modify))
// Verify state is still closed and failures are reset
assert.True(t, IsClosed(result), "circuit should remain closed after success")
// Verify the closed state was updated
closedState := either.Fold(
func(openState) ClosedState { return initialClosed },
F.Identity[ClosedState],
)(result)
// The success should have been recorded (implementation-specific verification)
assert.NotNil(t, closedState, "closed state should be present")
})
t.Run("keeps circuit closed", func(t *testing.T) {
t.Run("does not affect open state", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addSuccess := reader.From1(ClosedState.AddSuccess)
currentTime := vt.Now()
initialState := createClosedCircuit(MakeClosedStateCounter(3))
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
handler := handleSuccessOnClosed(currentTime, addSuccess)
result := io.Run(handler(modify))
// Create initial open state
initialOpen := openState{
openedAt: currentTime.Add(-1 * time.Minute),
resetAt: currentTime.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
initialState := createOpenCircuit(initialOpen)
assert.True(t, IsClosed(result), "circuit should remain closed")
// Apply handleSuccessOnClosed
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
result := endomorphism(initialState)
// Verify the state remains open and unchanged
assert.True(t, IsOpen(result), "state should remain open")
// Extract and verify the open state is unchanged
openResult := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return initialOpen },
)(result)
assert.Equal(t, initialOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
assert.Equal(t, initialOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
assert.Equal(t, initialOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
})
t.Run("preserves time parameter through reader", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
time1 := vt.Now()
vt.Advance(1 * time.Hour)
time2 := vt.Now()
var capturedTime time.Time
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
capturedTime = ct
return F.Identity[ClosedState]
}
initialClosed := MakeClosedStateCounter(3)
initialState := createClosedCircuit(initialClosed)
handler := handleSuccessOnClosed(addSuccess)
// Apply with time1
endomorphism1 := handler(time1)
endomorphism1(initialState)
assert.Equal(t, time1, capturedTime, "should pass time1 to addSuccess")
// Apply with time2
endomorphism2 := handler(time2)
endomorphism2(initialState)
assert.Equal(t, time2, capturedTime, "should pass time2 to addSuccess")
})
t.Run("composes correctly with multiple successes", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
initialClosed := MakeClosedStateCounter(3)
initialState := createClosedCircuit(initialClosed)
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
// Apply multiple times
result1 := endomorphism(initialState)
result2 := endomorphism(result1)
result3 := endomorphism(result2)
// All should remain closed
assert.True(t, IsClosed(result1), "state should remain closed after first success")
assert.True(t, IsClosed(result2), "state should remain closed after second success")
assert.True(t, IsClosed(result3), "state should remain closed after third success")
})
}
@@ -496,9 +581,26 @@ func TestHandleSuccessOnClosed(t *testing.T) {
func TestHandleFailureOnClosed(t *testing.T) {
t.Run("keeps circuit closed when threshold not exceeded", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
// Create a closed state that allows 3 errors
initialClosed := MakeClosedStateCounter(3)
// addError increments error count
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
// checkClosedState returns Some if under threshold
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
// openCircuit creates an open state (shouldn't be called in this test)
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -508,26 +610,39 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
// Create initial state with room for more failures
now := vt.Now()
initialClosed := MakeClosedStateCounter(5) // threshold is 5
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
// First error - should stay closed
result1 := endomorphism(initialState)
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
assert.True(t, IsClosed(result), "circuit should remain closed when threshold not exceeded")
// Second error - should stay closed
result2 := endomorphism(result1)
assert.True(t, IsClosed(result2), "circuit should remain closed after second error")
})
t.Run("opens circuit when threshold exceeded", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
// Create a closed state that allows only 2 errors (opens at 2nd error)
initialClosed := MakeClosedStateCounter(2)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -537,26 +652,85 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
// Create initial state at threshold
now := vt.Now()
initialClosed := MakeClosedStateCounter(2) // threshold is 2
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
// First error - should stay closed (count=1, threshold=2)
result1 := endomorphism(initialState)
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
assert.True(t, IsOpen(result), "circuit should open when threshold exceeded")
// Second error - should open (count=2, threshold=2)
result2 := endomorphism(result1)
assert.True(t, IsOpen(result2), "circuit should open when threshold reached")
})
t.Run("records failure in closed state", func(t *testing.T) {
t.Run("creates open state with correct reset time", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
expectedResetTime := currentTime.Add(5 * time.Minute)
initialClosed := MakeClosedStateCounter(1) // Opens at 1st error
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: expectedResetTime,
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// First error - should open immediately (threshold=1)
result1 := endomorphism(initialState)
assert.True(t, IsOpen(result1), "circuit should open after first error")
// Verify the open state has correct reset time
resultOpen := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(result1)
assert.Equal(t, expectedResetTime, resultOpen.resetAt, "reset time should match expected")
assert.Equal(t, currentTime, resultOpen.openedAt, "opened time should be current time")
})
t.Run("edge case: zero error threshold", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
// Create a closed state that allows 0 errors (opens immediately)
initialClosed := MakeClosedStateCounter(0)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -566,14 +740,212 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
initialState := createClosedCircuit(MakeClosedStateCounter(10))
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Should still be closed but with failure recorded
assert.True(t, IsClosed(result), "circuit should remain closed")
// First error should immediately open the circuit
result := endomorphism(initialState)
assert.True(t, IsOpen(result), "circuit should open immediately with zero threshold")
})
t.Run("edge case: very high error threshold", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
// Create a closed state that allows 1000 errors
initialClosed := MakeClosedStateCounter(1000)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Apply many errors
result := initialState
for i := 0; i < 100; i++ {
result = endomorphism(result)
}
// Should still be closed after 100 errors
assert.True(t, IsClosed(result), "circuit should remain closed with high threshold")
})
t.Run("preserves time parameter through reader chain", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
time1 := vt.Now()
vt.Advance(2 * time.Hour)
time2 := vt.Now()
var capturedAddErrorTime, capturedCheckTime, capturedOpenTime time.Time
initialClosed := MakeClosedStateCounter(2) // Need 2 errors to open
addError := func(ct time.Time) Endomorphism[ClosedState] {
capturedAddErrorTime = ct
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
capturedCheckTime = ct
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
capturedOpenTime = ct
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
// Apply with time1 - first error, stays closed
endomorphism1 := handler(time1)
result1 := endomorphism1(initialState)
assert.Equal(t, time1, capturedAddErrorTime, "addError should receive time1")
assert.Equal(t, time1, capturedCheckTime, "checkClosedState should receive time1")
// Apply with time2 - second error, should trigger open
endomorphism2 := handler(time2)
endomorphism2(result1)
assert.Equal(t, time2, capturedAddErrorTime, "addError should receive time2")
assert.Equal(t, time2, capturedCheckTime, "checkClosedState should receive time2")
assert.Equal(t, time2, capturedOpenTime, "openCircuit should receive time2")
})
t.Run("handles transition from closed to open correctly", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
initialClosed := MakeClosedStateCounter(2) // Opens at 2nd error
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Start with closed state
state := createClosedCircuit(initialClosed)
assert.True(t, IsClosed(state), "initial state should be closed")
// First error - should stay closed (count=1, threshold=2)
state = endomorphism(state)
assert.True(t, IsClosed(state), "should remain closed after first error")
// Second error - should open (count=2, threshold=2)
state = endomorphism(state)
assert.True(t, IsOpen(state), "should open after second error")
// Verify it's truly open with correct properties
resultOpen := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(state)
assert.False(t, resultOpen.canaryRequest, "canaryRequest should be false initially")
assert.Equal(t, currentTime, resultOpen.openedAt, "openedAt should be current time")
})
t.Run("does not affect already open state", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
// Start with an already open state
existingOpen := openState{
openedAt: currentTime.Add(-5 * time.Minute),
resetAt: currentTime.Add(5 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: true,
}
initialState := createOpenCircuit(existingOpen)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Apply to open state - should not change it
result := endomorphism(initialState)
assert.True(t, IsOpen(result), "state should remain open")
// The open state should be unchanged since handleFailureOnClosed
// only operates on the Right (closed) side of the Either
openResult := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(result)
assert.Equal(t, existingOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
assert.Equal(t, existingOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
assert.Equal(t, existingOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
})
}

View File

@@ -28,7 +28,10 @@ import (
//
// Thread Safety: This type is immutable and safe for concurrent use.
type CircuitBreakerError struct {
Name string
// Name: The name identifying this circuit breaker instance
Name string
// ResetAt: The time at which the circuit breaker will transition from open to half-open state
ResetAt time.Time
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
)
type (
@@ -110,6 +111,25 @@ type (
name string
logger *log.Logger
}
// voidMetrics is a no-op implementation of the Metrics interface that does nothing.
// All methods return the same pre-allocated IO[Void] operation that immediately returns
// without performing any action.
//
// This implementation is useful for:
// - Testing scenarios where metrics collection is not needed
// - Production environments where metrics overhead should be eliminated
// - Benchmarking circuit breaker logic without metrics interference
// - Default initialization when no metrics implementation is provided
//
// Thread Safety: This implementation is safe for concurrent use. The noop IO operation
// is immutable and can be safely shared across goroutines.
//
// Performance: This is the most efficient Metrics implementation as it performs no
// operations and has minimal memory overhead (single shared IO[Void] instance).
voidMetrics struct {
noop IO[Void]
}
)
// doLog is a helper method that creates an IO operation for logging a circuit breaker event.
@@ -206,3 +226,79 @@ func (m *loggingMetrics) Canary(ct time.Time) IO[Void] {
func MakeMetricsFromLogger(name string, logger *log.Logger) Metrics {
return &loggingMetrics{name: name, logger: logger}
}
// Open implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Open(_ time.Time) IO[Void] {
return m.noop
}
// Accept implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Accept(_ time.Time) IO[Void] {
return m.noop
}
// Canary implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Canary(_ time.Time) IO[Void] {
return m.noop
}
// Close implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Close(_ time.Time) IO[Void] {
return m.noop
}
// Reject implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Reject(_ time.Time) IO[Void] {
return m.noop
}
// MakeVoidMetrics creates a no-op Metrics implementation that performs no operations.
// All methods return the same pre-allocated IO[Void] operation that does nothing when executed.
//
// This is useful for:
// - Testing scenarios where metrics collection is not needed
// - Production environments where metrics overhead should be eliminated
// - Benchmarking circuit breaker logic without metrics interference
// - Default initialization when no metrics implementation is provided
//
// Returns:
// - Metrics: A thread-safe no-op Metrics implementation
//
// Thread Safety: The returned Metrics implementation is safe for concurrent use.
// All methods return the same immutable IO[Void] operation.
//
// Performance: This is the most efficient Metrics implementation with minimal overhead.
// The IO[Void] operation is pre-allocated once and reused for all method calls.
//
// Example:
//
// metrics := MakeVoidMetrics()
//
// // All operations do nothing
// io.Run(metrics.Open(time.Now())) // No-op
// io.Run(metrics.Accept(time.Now())) // No-op
// io.Run(metrics.Reject(time.Now())) // No-op
//
// // Useful for testing
// breaker := MakeCircuitBreaker(
// // ... other parameters ...
// MakeVoidMetrics(), // No metrics overhead
// )
func MakeVoidMetrics() Metrics {
return &voidMetrics{io.Of(function.VOID)}
}

View File

@@ -504,3 +504,443 @@ func TestMetricsIOOperations(t *testing.T) {
assert.Len(t, lines, 3, "should execute multiple times")
})
}
// TestMakeVoidMetrics tests the MakeVoidMetrics constructor
func TestMakeVoidMetrics(t *testing.T) {
t.Run("creates valid Metrics implementation", func(t *testing.T) {
metrics := MakeVoidMetrics()
assert.NotNil(t, metrics, "MakeVoidMetrics should return non-nil Metrics")
})
t.Run("returns voidMetrics type", func(t *testing.T) {
metrics := MakeVoidMetrics()
_, ok := metrics.(*voidMetrics)
assert.True(t, ok, "should return *voidMetrics type")
})
t.Run("initializes noop IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics().(*voidMetrics)
assert.NotNil(t, metrics.noop, "noop IO operation should be initialized")
})
}
// TestVoidMetricsAccept tests the Accept method of voidMetrics
func TestVoidMetricsAccept(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics().(*voidMetrics)
timestamp := time.Now()
ioOp1 := metrics.Accept(timestamp)
ioOp2 := metrics.Accept(timestamp)
// Both should be non-nil (we can't compare functions directly in Go)
assert.NotNil(t, ioOp1, "should return non-nil IO operation")
assert.NotNil(t, ioOp2, "should return non-nil IO operation")
// Verify they execute without error
io.Run(ioOp1)
io.Run(ioOp2)
})
t.Run("ignores timestamp parameter", func(t *testing.T) {
metrics := MakeVoidMetrics()
time1 := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
time2 := time.Date(2026, 1, 9, 16, 30, 0, 0, time.UTC)
ioOp1 := metrics.Accept(time1)
ioOp2 := metrics.Accept(time2)
// Should return same operation regardless of timestamp
io.Run(ioOp1)
io.Run(ioOp2)
// No assertions needed - just verify it doesn't panic
})
}
// TestVoidMetricsReject tests the Reject method of voidMetrics
func TestVoidMetricsReject(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsOpen tests the Open method of voidMetrics
func TestVoidMetricsOpen(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsClose tests the Close method of voidMetrics
func TestVoidMetricsClose(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsCanary tests the Canary method of voidMetrics
func TestVoidMetricsCanary(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsThreadSafety tests concurrent access to voidMetrics
func TestVoidMetricsThreadSafety(t *testing.T) {
t.Run("handles concurrent metric calls", func(t *testing.T) {
metrics := MakeVoidMetrics()
var wg sync.WaitGroup
numGoroutines := 100
wg.Add(numGoroutines * 5) // 5 methods
timestamp := time.Now()
// Launch multiple goroutines calling all methods concurrently
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
io.Run(metrics.Accept(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Reject(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Open(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Close(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Canary(timestamp))
}()
}
wg.Wait()
// Test passes if no panic occurs
})
t.Run("all methods return valid IO operations concurrently", func(t *testing.T) {
metrics := MakeVoidMetrics()
var wg sync.WaitGroup
numGoroutines := 50
wg.Add(numGoroutines)
timestamp := time.Now()
results := make([]IO[Void], numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(idx int) {
defer wg.Done()
// Each goroutine calls a different method
switch idx % 5 {
case 0:
results[idx] = metrics.Accept(timestamp)
case 1:
results[idx] = metrics.Reject(timestamp)
case 2:
results[idx] = metrics.Open(timestamp)
case 3:
results[idx] = metrics.Close(timestamp)
case 4:
results[idx] = metrics.Canary(timestamp)
}
}(i)
}
wg.Wait()
// All results should be non-nil and executable
for i, result := range results {
assert.NotNil(t, result, "result %d should be non-nil", i)
io.Run(result) // Verify it executes without error
}
})
}
// TestVoidMetricsPerformance tests performance characteristics
func TestVoidMetricsPerformance(t *testing.T) {
t.Run("has minimal overhead", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
// Execute many operations quickly
iterations := 10000
for i := 0; i < iterations; i++ {
io.Run(metrics.Accept(timestamp))
io.Run(metrics.Reject(timestamp))
io.Run(metrics.Open(timestamp))
io.Run(metrics.Close(timestamp))
io.Run(metrics.Canary(timestamp))
}
// Test passes if it completes quickly without issues
})
t.Run("all methods return valid IO operations", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
// All methods should return non-nil IO operations
accept := metrics.Accept(timestamp)
reject := metrics.Reject(timestamp)
open := metrics.Open(timestamp)
close := metrics.Close(timestamp)
canary := metrics.Canary(timestamp)
assert.NotNil(t, accept, "Accept should return non-nil")
assert.NotNil(t, reject, "Reject should return non-nil")
assert.NotNil(t, open, "Open should return non-nil")
assert.NotNil(t, close, "Close should return non-nil")
assert.NotNil(t, canary, "Canary should return non-nil")
// All should execute without error
io.Run(accept)
io.Run(reject)
io.Run(open)
io.Run(close)
io.Run(canary)
})
}
// TestVoidMetricsIntegration tests integration scenarios
func TestVoidMetricsIntegration(t *testing.T) {
t.Run("can be used as drop-in replacement for loggingMetrics", func(t *testing.T) {
// Create both types of metrics
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
loggingMetrics := MakeMetricsFromLogger("TestCircuit", logger)
voidMetrics := MakeVoidMetrics()
timestamp := time.Now()
// Both should implement the same interface
var m1 Metrics = loggingMetrics
var m2 Metrics = voidMetrics
// Both should be callable
io.Run(m1.Accept(timestamp))
io.Run(m2.Accept(timestamp))
// Logging metrics should have output
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
// Void metrics should have no observable side effects
// (we can't directly test this, but the test passes if no panic occurs)
})
t.Run("simulates complete circuit breaker lifecycle without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
baseTime := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
// Simulate circuit breaker lifecycle - all should be no-ops
io.Run(metrics.Accept(baseTime))
io.Run(metrics.Accept(baseTime.Add(1 * time.Second)))
io.Run(metrics.Open(baseTime.Add(2 * time.Second)))
io.Run(metrics.Reject(baseTime.Add(3 * time.Second)))
io.Run(metrics.Canary(baseTime.Add(30 * time.Second)))
io.Run(metrics.Close(baseTime.Add(31 * time.Second)))
// Test passes if no panic occurs and completes quickly
})
}
// TestVoidMetricsEdgeCases tests edge cases
func TestVoidMetricsEdgeCases(t *testing.T) {
t.Run("handles zero time", func(t *testing.T) {
metrics := MakeVoidMetrics()
zeroTime := time.Time{}
io.Run(metrics.Accept(zeroTime))
io.Run(metrics.Reject(zeroTime))
io.Run(metrics.Open(zeroTime))
io.Run(metrics.Close(zeroTime))
io.Run(metrics.Canary(zeroTime))
// Test passes if no panic occurs
})
t.Run("handles far future time", func(t *testing.T) {
metrics := MakeVoidMetrics()
futureTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
io.Run(metrics.Accept(futureTime))
io.Run(metrics.Reject(futureTime))
io.Run(metrics.Open(futureTime))
io.Run(metrics.Close(futureTime))
io.Run(metrics.Canary(futureTime))
// Test passes if no panic occurs
})
t.Run("IO operations are idempotent", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
// Execute same operation multiple times
io.Run(ioOp)
io.Run(ioOp)
io.Run(ioOp)
// Test passes if no panic occurs
})
}
// TestMetricsComparison compares loggingMetrics and voidMetrics
func TestMetricsComparison(t *testing.T) {
t.Run("both implement Metrics interface", func(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
var m1 Metrics = MakeMetricsFromLogger("Test", logger)
var m2 Metrics = MakeVoidMetrics()
assert.NotNil(t, m1)
assert.NotNil(t, m2)
})
t.Run("voidMetrics has no observable side effects unlike loggingMetrics", func(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
loggingMetrics := MakeMetricsFromLogger("Test", logger)
voidMetrics := MakeVoidMetrics()
timestamp := time.Now()
// Logging metrics produces output
io.Run(loggingMetrics.Accept(timestamp))
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
// Void metrics has no observable output
// (we can only verify it doesn't panic)
io.Run(voidMetrics.Accept(timestamp))
})
}

View File

@@ -34,6 +34,7 @@ import (
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/retry"
"github.com/IBM/fp-go/v2/state"
)
@@ -79,10 +80,13 @@ type (
// and produces a value of type A. Used for dependency injection and configuration.
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// openState represents the internal state when the circuit breaker is open.
// In the open state, requests are blocked to give the failing service time to recover.
// The circuit breaker will transition to a half-open state (canary request) after resetAt.
openState struct {
// openedAt is the time when the circuit breaker opened the circuit
openedAt time.Time
// resetAt is the time when the circuit breaker should attempt a canary request

View File

@@ -19,11 +19,13 @@ package consumer
// This is the contravariant map operation for Consumers, analogous to reader.Local
// but operating on the input side rather than the output side.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Given a Consumer[R1] that consumes values of type R1, and a function f that
// converts R2 to R1, Local creates a new Consumer[R2] that:
// 1. Takes a value of type R2
// 2. Applies f to convert it to R1
// 3. Passes the result to the original Consumer[R1]
// 1. Takes a value of type R2
// 2. Applies f to convert it to R1
// 3. Passes the result to the original Consumer[R1]
//
// This is particularly useful for adapting consumers to work with different input types,
// similar to how reader.Local adapts readers to work with different environment types.
@@ -168,7 +170,7 @@ package consumer
// - reader.Local transforms the environment before reading
// - consumer.Local transforms the input before consuming
// - Both are contravariant functors on their input type
func Local[R2, R1 any](f func(R2) R1) Operator[R1, R2] {
func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
return func(c Consumer[R1]) Consumer[R2] {
return func(r2 R2) {
c(f(r2))

View File

@@ -0,0 +1,74 @@
// Copyright (c) 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 readerio
import (
"context"
"github.com/IBM/fp-go/v2/function"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIO.
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Modify the context before passing it to the ReaderIO (via f)
// - Transform the result value after the IO effect completes (via g)
//
// The function f returns both a new context and a CancelFunc that should be called to release resources.
//
// Type Parameters:
// - A: The original result type produced by the ReaderIO
// - B: The new output result type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - g: Function to transform the output value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
)
}
// Contramap changes the context during the execution of a ReaderIO.
// This is the contravariant functor operation that transforms the input context.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is an alias for Local and is useful for adapting a ReaderIO to work with
// a modified context by providing a function that transforms the context.
//
// Type Parameters:
// - A: The result type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderIO[A] and returns a ReaderIO[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return Local[A](f)
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) 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 readerio
import (
"context"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
// ReaderIO that reads a value from context
getValue := func(ctx context.Context) IO[int] {
return func() int {
if v := ctx.Value("key"); v != nil {
return v.(int)
}
return 0
}
}
// Transform context and result
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
assert.Equal(t, "42", result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context transformation", func(t *testing.T) {
getValue := func(ctx context.Context) IO[int] {
return func() int {
if v := ctx.Value("key"); v != nil {
return v.(int)
}
return 0
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
assert.Equal(t, 100, result)
})
}
// TestLocalBasic tests basic Local functionality
func TestLocalBasic(t *testing.T) {
t.Run("adds timeout to context", func(t *testing.T) {
getValue := func(ctx context.Context) IO[bool] {
return func() bool {
_, hasDeadline := ctx.Deadline()
return hasDeadline
}
}
addTimeout := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, time.Second)
}
adapted := Local[bool](addTimeout)(getValue)
result := adapted(context.Background())()
assert.True(t, result)
})
}

View File

@@ -560,6 +560,63 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
return RIO.Read[A](r)
}
// ReadIO executes a ReaderIO computation by providing a context wrapped in an IO effect.
// This is useful when the context itself needs to be computed or retrieved through side effects.
//
// The function takes an IO[context.Context] (an effectful computation that produces a context) and returns
// a function that can execute a ReaderIO[A] to produce an IO[A].
//
// This is particularly useful in scenarios where:
// - The context needs to be created with side effects (e.g., loading configuration)
// - The context requires initialization or setup
// - You want to compose context creation with the computation that uses it
//
// The execution flow is:
// 1. Execute the IO[context.Context] to get the context
// 2. Pass the context to the ReaderIO[A] to get an IO[A]
// 3. Execute the resulting IO[A] to get the final result A
//
// Type Parameters:
// - A: The result type of the ReaderIO computation
//
// Parameters:
// - r: An IO effect that produces a context.Context
//
// Returns:
// - A function that takes a ReaderIO[A] and returns an IO[A]
//
// Example:
//
// import (
// "context"
// G "github.com/IBM/fp-go/v2/io"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Create context with side effects (e.g., loading config)
// createContext := G.Of(context.WithValue(context.Background(), "key", "value"))
//
// // A computation that uses the context
// getValue := readerio.FromReader(func(ctx context.Context) string {
// if val := ctx.Value("key"); val != nil {
// return val.(string)
// }
// return "default"
// })
//
// // Compose them together
// result := readerio.ReadIO[string](createContext)(getValue)
// value := result() // Executes both effects and returns "value"
//
// Comparison with Read:
// - [Read]: Takes a pure context.Context value and executes the ReaderIO immediately
// - [ReadIO]: Takes an IO[context.Context] and chains the effects together
//
//go:inline
func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
return RIO.ReadIO[A](r)
}
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
//
// This is the Reader's local operation, which allows you to modify the environment

View File

@@ -500,3 +500,188 @@ func TestTapWithLogging(t *testing.T) {
assert.Equal(t, 84, value)
assert.Equal(t, []int{42, 84}, logged)
}
func TestReadIO(t *testing.T) {
// Test basic ReadIO functionality
contextIO := G.Of(context.WithValue(context.Background(), "testKey", "testValue"))
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("testKey"); val != nil {
return val.(string)
}
return "default"
})
ioAction := ReadIO[string](contextIO)(rio)
result := ioAction()
assert.Equal(t, "testValue", result)
}
func TestReadIOWithBackground(t *testing.T) {
// Test ReadIO with plain background context
contextIO := G.Of(context.Background())
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
result := ioAction()
assert.Equal(t, 42, result)
}
func TestReadIOWithChain(t *testing.T) {
// Test ReadIO with chained operations
contextIO := G.Of(context.WithValue(context.Background(), "multiplier", 3))
result := F.Pipe1(
FromReader(func(ctx context.Context) int {
if val := ctx.Value("multiplier"); val != nil {
return val.(int)
}
return 1
}),
Chain(func(n int) ReaderIO[int] {
return Of(n * 10)
}),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 30, value) // 3 * 10
}
func TestReadIOWithMap(t *testing.T) {
// Test ReadIO with Map operations
contextIO := G.Of(context.Background())
result := F.Pipe2(
Of(5),
Map(N.Mul(2)),
Map(N.Add(10)),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 20, value) // (5 * 2) + 10
}
func TestReadIOWithSideEffects(t *testing.T) {
// Test ReadIO with side effects in context creation
counter := 0
contextIO := func() context.Context {
counter++
return context.WithValue(context.Background(), "counter", counter)
}
rio := FromReader(func(ctx context.Context) int {
if val := ctx.Value("counter"); val != nil {
return val.(int)
}
return 0
})
ioAction := ReadIO[int](contextIO)(rio)
result := ioAction()
assert.Equal(t, 1, result)
assert.Equal(t, 1, counter)
}
func TestReadIOMultipleExecutions(t *testing.T) {
// Test that ReadIO creates fresh effects on each execution
counter := 0
contextIO := func() context.Context {
counter++
return context.Background()
}
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
result1 := ioAction()
result2 := ioAction()
assert.Equal(t, 42, result1)
assert.Equal(t, 42, result2)
assert.Equal(t, 2, counter) // Context IO executed twice
}
func TestReadIOComparisonWithRead(t *testing.T) {
// Compare ReadIO with Read to show the difference
ctx := context.WithValue(context.Background(), "key", "value")
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("key"); val != nil {
return val.(string)
}
return "default"
})
// Using Read (direct context)
ioAction1 := Read[string](ctx)(rio)
result1 := ioAction1()
// Using ReadIO (context wrapped in IO)
contextIO := G.Of(ctx)
ioAction2 := ReadIO[string](contextIO)(rio)
result2 := ioAction2()
assert.Equal(t, result1, result2)
assert.Equal(t, "value", result1)
assert.Equal(t, "value", result2)
}
func TestReadIOWithComplexContext(t *testing.T) {
// Test ReadIO with complex context manipulation
type contextKey string
const (
userKey contextKey = "user"
tokenKey contextKey = "token"
)
contextIO := G.Of(
context.WithValue(
context.WithValue(context.Background(), userKey, "Alice"),
tokenKey,
"secret123",
),
)
rio := FromReader(func(ctx context.Context) map[string]string {
result := make(map[string]string)
if user := ctx.Value(userKey); user != nil {
result["user"] = user.(string)
}
if token := ctx.Value(tokenKey); token != nil {
result["token"] = token.(string)
}
return result
})
ioAction := ReadIO[map[string]string](contextIO)(rio)
result := ioAction()
assert.Equal(t, "Alice", result["user"])
assert.Equal(t, "secret123", result["token"])
}
func TestReadIOWithAsk(t *testing.T) {
// Test ReadIO combined with Ask
contextIO := G.Of(context.WithValue(context.Background(), "data", 100))
result := F.Pipe1(
Ask(),
Map(func(ctx context.Context) int {
if val := ctx.Value("data"); val != nil {
return val.(int)
}
return 0
}),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 100, value)
}

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/circuitbreaker"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/retry"
)
@@ -27,6 +28,9 @@ func MakeCircuitBreaker[T any](
Left,
ChainFirstIOK,
ChainFirstLeftIOK,
readerio.ChainFirstIOK,
FromIO,
Flap,
Flatten,

View File

@@ -608,7 +608,7 @@ func TestCircuitBreaker_ErrorMessageFormat(t *testing.T) {
protectedOp := pair.Tail(resultEnv)
outcome := protectedOp(ctx)()
assert.True(t, result.IsLeft[string](outcome))
assert.True(t, result.IsLeft(outcome))
// Error message should indicate circuit breaker is open
_, err := result.Unwrap(outcome)

View File

@@ -0,0 +1,75 @@
// Copyright (c) 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 readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/function"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Modify the context before passing it to the ReaderIOResult (via f)
// - Transform the success value after the IO effect completes (via g)
//
// The function f returns both a new context and a CancelFunc that should be called to release resources.
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - A: The original success type produced by the ReaderIOResult
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
)
}
// Contramap changes the context during the execution of a ReaderIOResult.
// This is the contravariant functor operation that transforms the input context.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is an alias for Local and is useful for adapting a ReaderIOResult to work with
// a modified context by providing a function that transforms the context.
//
// Type Parameters:
// - A: The success type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return Local[A](f)
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) 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 readerioresult
import (
"context"
"strconv"
"testing"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
assert.Equal(t, R.Of("42"), result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context transformation", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalBasic tests basic Local functionality
func TestLocalBasic(t *testing.T) {
t.Run("adds value to context", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())()
assert.Equal(t, R.Of("Alice"), result)
})
}

View File

@@ -914,6 +914,21 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}
//go:inline
func ReadIO[A any](r IO[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIO[A](r)
}
//go:inline
func ReadIOEither[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIOEither[A](r)
}
//go:inline
func ReadIOResult[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIOResult[A](r)
}
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type. If the input is a Right value, it passes through unchanged.

View File

@@ -0,0 +1,106 @@
// Copyright (c) 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 readerresult
import (
"context"
"github.com/IBM/fp-go/v2/function"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderResult.
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Modify the context before passing it to the ReaderResult (via f)
// - Transform the success value after the computation completes (via g)
//
// The function f returns both a new context and a CancelFunc that should be called to release resources.
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - A: The original success type produced by the ReaderResult
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input context (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
)
}
// Contramap changes the context during the execution of a ReaderResult.
// This is the contravariant functor operation that transforms the input context.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is an alias for Local and is useful for adapting a ReaderResult to work with
// a modified context by providing a function that transforms the context.
//
// Type Parameters:
// - A: The success type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return Local[A](f)
}
// Local changes the context during the execution of a ReaderResult.
// This allows you to modify the context before passing it to a ReaderResult computation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Local is particularly useful for:
// - Adding values to the context
// - Setting timeouts or deadlines
// - Modifying context metadata
//
// The function f returns both a new context and a CancelFunc. The CancelFunc is automatically
// called (via defer) after the ReaderResult computation completes to ensure proper cleanup.
//
// Type Parameters:
// - A: The result type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and CancelFunc
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[A]
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) Result[A] {
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)
}
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 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 readerresult
import (
"context"
"strconv"
"testing"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
getValue := func(ctx context.Context) Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())
assert.Equal(t, R.Of("42"), result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context transformation", func(t *testing.T) {
getValue := func(ctx context.Context) Result[int] {
if v := ctx.Value("key"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalBasic tests basic Local functionality
func TestLocalBasic(t *testing.T) {
t.Run("adds value to context", func(t *testing.T) {
getValue := func(ctx context.Context) Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())
assert.Equal(t, R.Of("Alice"), result)
})
}

View File

@@ -148,6 +148,16 @@ func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
return readereither.Read[error, A](r)
}
//go:inline
func ReadEither[A any](r Result[context.Context]) func(ReaderResult[A]) Result[A] {
return readereither.ReadEither[error, A](r)
}
//go:inline
func ReadResult[A any](r Result[context.Context]) func(ReaderResult[A]) Result[A] {
return readereither.ReadEither[error, A](r)
}
// MonadMapTo executes a ReaderResult computation, discards its success value, and returns a constant value.
// This is the monadic version that takes both the ReaderResult and the constant value as parameters.
//

View File

@@ -0,0 +1,650 @@
// Copyright (c) 2024 - 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 either
import (
"errors"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
// TestApplicativeOf tests the Of operation of the Applicative type class
func TestApplicativeOf(t *testing.T) {
app := Applicative[error, int, string]()
t.Run("wraps a value in Right context", func(t *testing.T) {
result := app.Of(42)
assert.True(t, IsRight(result))
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
t.Run("wraps string value", func(t *testing.T) {
app := Applicative[error, string, int]()
result := app.Of("hello")
assert.True(t, IsRight(result))
assert.Equal(t, "hello", GetOrElse(func(error) string { return "" })(result))
})
t.Run("wraps zero value", func(t *testing.T) {
result := app.Of(0)
assert.True(t, IsRight(result))
assert.Equal(t, 0, GetOrElse(func(error) int { return -1 })(result))
})
t.Run("wraps nil pointer", func(t *testing.T) {
app := Applicative[error, *int, *string]()
var ptr *int = nil
result := app.Of(ptr)
assert.True(t, IsRight(result))
})
}
// TestApplicativeMap tests the Map operation of the Applicative type class
func TestApplicativeMap(t *testing.T) {
app := Applicative[error, int, int]()
t.Run("maps a function over Right value", func(t *testing.T) {
double := func(x int) int { return x * 2 }
eitherValue := app.Of(21)
result := app.Map(double)(eitherValue)
assert.True(t, IsRight(result))
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
t.Run("maps type conversion", func(t *testing.T) {
app := Applicative[error, int, string]()
eitherValue := app.Of(42)
result := app.Map(strconv.Itoa)(eitherValue)
assert.True(t, IsRight(result))
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("maps identity function", func(t *testing.T) {
identity := func(x int) int { return x }
eitherValue := app.Of(42)
result := app.Map(identity)(eitherValue)
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
t.Run("preserves Left on map", func(t *testing.T) {
double := func(x int) int { return x * 2 }
eitherValue := Left[int](errors.New("error"))
result := app.Map(double)(eitherValue)
assert.True(t, IsLeft(result))
})
t.Run("maps with utils.Double", func(t *testing.T) {
result := F.Pipe1(
app.Of(21),
app.Map(utils.Double),
)
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
}
// TestApplicativeAp tests the Ap operation of the standard Applicative (fail-fast)
func TestApplicativeAp(t *testing.T) {
app := Applicative[error, int, int]()
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
eitherFunc := Right[error](add(10))
eitherValue := Right[error](32)
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsRight(result))
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
t.Run("fails fast when function is Left", func(t *testing.T) {
err1 := errors.New("function error")
eitherFunc := Left[func(int) int](err1)
eitherValue := Right[error](42)
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
assert.Equal(t, err1, ToError(result))
})
t.Run("fails fast when value is Left", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
err2 := errors.New("value error")
eitherFunc := Right[error](add(10))
eitherValue := Left[int](err2)
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
assert.Equal(t, err2, ToError(result))
})
t.Run("fails fast when both are Left - returns first error", func(t *testing.T) {
err1 := errors.New("function error")
err2 := errors.New("value error")
eitherFunc := Left[func(int) int](err1)
eitherValue := Left[int](err2)
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
// Should return the first error (function error)
assert.Equal(t, err1, ToError(result))
})
t.Run("applies with type conversion", func(t *testing.T) {
toStringAndAppend := func(suffix string) func(int) string {
return func(n int) string {
return strconv.Itoa(n) + suffix
}
}
eitherFunc := Right[error](toStringAndAppend("!"))
eitherValue := Right[error](42)
result := Ap[string](eitherValue)(eitherFunc)
assert.Equal(t, "42!", GetOrElse(func(error) string { return "" })(result))
})
}
// TestApplicativeVOf tests the Of operation of ApplicativeV
func TestApplicativeVOf(t *testing.T) {
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
app := ApplicativeV[string, int, string](sg)
t.Run("wraps a value in Right context", func(t *testing.T) {
result := app.Of(42)
assert.True(t, IsRight(result))
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
})
}
// TestApplicativeVMap tests the Map operation of ApplicativeV
func TestApplicativeVMap(t *testing.T) {
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
app := ApplicativeV[string, int, int](sg)
t.Run("maps a function over Right value", func(t *testing.T) {
double := func(x int) int { return x * 2 }
eitherValue := app.Of(21)
result := app.Map(double)(eitherValue)
assert.True(t, IsRight(result))
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
})
t.Run("preserves Left on map", func(t *testing.T) {
double := func(x int) int { return x * 2 }
eitherValue := Left[int]("error")
result := app.Map(double)(eitherValue)
assert.True(t, IsLeft(result))
})
}
// TestApplicativeVAp tests the Ap operation of ApplicativeV (validation with error accumulation)
func TestApplicativeVAp(t *testing.T) {
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
app := ApplicativeV[string, int, int](sg)
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
eitherFunc := Right[string](add(10))
eitherValue := Right[string](32)
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsRight(result))
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result))
})
t.Run("returns Left when function is Left", func(t *testing.T) {
eitherFunc := Left[func(int) int]("function error")
eitherValue := Right[string](42)
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
leftValue := Fold(F.Identity[string], F.Constant1[int](""))(result)
assert.Equal(t, "function error", leftValue)
})
t.Run("returns Left when value is Left", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
eitherFunc := Right[string](add(10))
eitherValue := Left[int]("value error")
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
leftValue := Fold(F.Identity[string], F.Constant1[int](""))(result)
assert.Equal(t, "value error", leftValue)
})
t.Run("accumulates errors when both are Left", func(t *testing.T) {
eitherFunc := Left[func(int) int]("function error")
eitherValue := Left[int]("value error")
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
// Should combine both errors using the semigroup
combined := Fold(F.Identity[string], F.Constant1[int](""))(result)
assert.Equal(t, "function error; value error", combined)
})
t.Run("accumulates multiple validation errors", func(t *testing.T) {
type ValidationErrors []string
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
return append(append(ValidationErrors{}, a...), b...)
})
app := ApplicativeV[ValidationErrors, int, int](sg)
eitherFunc := Left[func(int) int](ValidationErrors{"error1", "error2"})
eitherValue := Left[int](ValidationErrors{"error3", "error4"})
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
errors := Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(result)
assert.Equal(t, ValidationErrors{"error1", "error2", "error3", "error4"}, errors)
})
}
// TestApplicativeLaws tests the applicative functor laws for standard Applicative
func TestApplicativeLaws(t *testing.T) {
app := Applicative[error, int, int]()
t.Run("identity law: Ap(Of(id))(v) = v", func(t *testing.T) {
identity := func(x int) int { return x }
v := app.Of(42)
left := app.Ap(v)(Of[error](identity))
right := v
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
GetOrElse(func(error) int { return 0 })(left))
})
t.Run("homomorphism law: Ap(Of(x))(Of(f)) = Of(f(x))", func(t *testing.T) {
f := func(x int) int { return x * 2 }
x := 21
left := app.Ap(app.Of(x))(Of[error](f))
right := app.Of(f(x))
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
GetOrElse(func(error) int { return 0 })(left))
})
t.Run("interchange law: Ap(Of(y))(u) = Ap(u)(Of(f => f(y)))", func(t *testing.T) {
double := func(x int) int { return x * 2 }
u := Of[error](double)
y := 21
left := app.Ap(app.Of(y))(u)
// For interchange, we need to apply the value to the function
// This test verifies the law holds for the applicative
right := Map[error](func(f func(int) int) int { return f(y) })(u)
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
GetOrElse(func(error) int { return 0 })(left))
})
t.Run("composition law", func(t *testing.T) {
// For Either, we test a simpler version of composition
f := func(x int) int { return x * 2 }
g := func(x int) int { return x + 10 }
x := 16
// Apply g then f
left := F.Pipe2(
app.Of(x),
app.Map(g),
app.Map(f),
)
// Compose f and g, then apply
composed := func(x int) int { return f(g(x)) }
right := app.Map(composed)(app.Of(x))
assert.Equal(t, GetOrElse(func(error) int { return 0 })(right),
GetOrElse(func(error) int { return 0 })(left))
})
}
// TestApplicativeVLaws tests the applicative functor laws for ApplicativeV
func TestApplicativeVLaws(t *testing.T) {
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
app := ApplicativeV[string, int, int](sg)
t.Run("identity law: Ap(Of(id))(v) = v", func(t *testing.T) {
identity := func(x int) int { return x }
v := app.Of(42)
left := app.Ap(v)(Of[string](identity))
right := v
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
GetOrElse(func(string) int { return 0 })(left))
})
t.Run("homomorphism law: Ap(Of(x))(Of(f)) = Of(f(x))", func(t *testing.T) {
f := func(x int) int { return x * 2 }
x := 21
left := app.Ap(app.Of(x))(Of[string](f))
right := app.Of(f(x))
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
GetOrElse(func(string) int { return 0 })(left))
})
t.Run("interchange law: Ap(Of(y))(u) = Ap(u)(Of(f => f(y)))", func(t *testing.T) {
double := func(x int) int { return x * 2 }
u := Of[string](double)
y := 21
left := app.Ap(app.Of(y))(u)
// For interchange, we need to apply the value to the function
right := Map[string](func(f func(int) int) int { return f(y) })(u)
assert.Equal(t, GetOrElse(func(string) int { return 0 })(right),
GetOrElse(func(string) int { return 0 })(left))
})
}
// TestApplicativeComposition tests composition of applicative operations
func TestApplicativeComposition(t *testing.T) {
app := Applicative[error, int, int]()
t.Run("composes Map and Of", func(t *testing.T) {
double := func(x int) int { return x * 2 }
result := F.Pipe1(
app.Of(21),
app.Map(double),
)
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
t.Run("composes multiple Map operations", func(t *testing.T) {
app := Applicative[error, int, string]()
double := func(x int) int { return x * 2 }
toString := func(x int) string { return strconv.Itoa(x) }
result := F.Pipe2(
app.Of(21),
Map[error](double),
app.Map(toString),
)
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("composes Map and Ap", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
eitherFunc := F.Pipe1(
app.Of(5),
Map[error](add),
)
eitherValue := app.Of(16)
result := app.Ap(eitherValue)(eitherFunc)
assert.Equal(t, 21, GetOrElse(func(error) int { return 0 })(result))
})
}
// TestApplicativeMultipleArguments tests applying functions with multiple arguments
func TestApplicativeMultipleArguments(t *testing.T) {
app := Applicative[error, int, int]()
t.Run("applies curried two-argument function", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
eitherFunc := F.Pipe1(
app.Of(10),
Map[error](add),
)
result := app.Ap(app.Of(32))(eitherFunc)
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
t.Run("applies curried three-argument function", func(t *testing.T) {
add3 := func(a int) func(int) func(int) int {
return func(b int) func(int) int {
return func(c int) int {
return a + b + c
}
}
}
eitherFunc1 := F.Pipe1(
app.Of(10),
Map[error](add3),
)
eitherFunc2 := Ap[func(int) int](app.Of(20))(eitherFunc1)
result := Ap[int](app.Of(12))(eitherFunc2)
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result))
})
}
// TestApplicativeInstance tests that Applicative returns a valid instance
func TestApplicativeInstance(t *testing.T) {
t.Run("returns non-nil instance", func(t *testing.T) {
app := Applicative[error, int, string]()
assert.NotNil(t, app)
})
t.Run("multiple calls return independent instances", func(t *testing.T) {
app1 := Applicative[error, int, string]()
app2 := Applicative[error, int, string]()
result1 := app1.Of(42)
result2 := app2.Of(43)
assert.Equal(t, 42, GetOrElse(func(error) int { return 0 })(result1))
assert.Equal(t, 43, GetOrElse(func(error) int { return 0 })(result2))
})
}
// TestApplicativeVInstance tests that ApplicativeV returns a valid instance
func TestApplicativeVInstance(t *testing.T) {
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
t.Run("returns non-nil instance", func(t *testing.T) {
app := ApplicativeV[string, int, string](sg)
assert.NotNil(t, app)
})
t.Run("multiple calls return independent instances", func(t *testing.T) {
app1 := ApplicativeV[string, int, string](sg)
app2 := ApplicativeV[string, int, string](sg)
result1 := app1.Of(42)
result2 := app2.Of(43)
assert.Equal(t, 42, GetOrElse(func(string) int { return 0 })(result1))
assert.Equal(t, 43, GetOrElse(func(string) int { return 0 })(result2))
})
}
// TestApplicativeWithDifferentTypes tests applicative with various type combinations
func TestApplicativeWithDifferentTypes(t *testing.T) {
t.Run("int to string", func(t *testing.T) {
app := Applicative[error, int, string]()
result := app.Map(strconv.Itoa)(app.Of(42))
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("string to int", func(t *testing.T) {
app := Applicative[error, string, int]()
toLength := func(s string) int { return len(s) }
result := app.Map(toLength)(app.Of("hello"))
assert.Equal(t, 5, GetOrElse(func(error) int { return 0 })(result))
})
t.Run("bool to string", func(t *testing.T) {
app := Applicative[error, bool, string]()
toString := func(b bool) string {
if b {
return "true"
}
return "false"
}
result := app.Map(toString)(app.Of(true))
assert.Equal(t, "true", GetOrElse(func(error) string { return "" })(result))
})
}
// TestApplicativeVFormValidationExample demonstrates a realistic form validation scenario
func TestApplicativeVFormValidationExample(t *testing.T) {
type ValidationErrors []string
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
return append(append(ValidationErrors{}, a...), b...)
})
validateName := func(name string) Either[ValidationErrors, string] {
if len(name) < 3 {
return Left[string](ValidationErrors{"Name must be at least 3 characters"})
}
return Right[ValidationErrors](name)
}
validateAge := func(age int) Either[ValidationErrors, int] {
if age < 18 {
return Left[int](ValidationErrors{"Must be 18 or older"})
}
return Right[ValidationErrors](age)
}
validateEmail := func(email string) Either[ValidationErrors, string] {
if len(email) == 0 {
return Left[string](ValidationErrors{"Email is required"})
}
return Right[ValidationErrors](email)
}
t.Run("all validations pass", func(t *testing.T) {
name := validateName("Alice")
age := validateAge(25)
email := validateEmail("alice@example.com")
// Verify all individual validations passed
assert.True(t, IsRight(name))
assert.True(t, IsRight(age))
assert.True(t, IsRight(email))
// Combine validations - all pass
result := F.Pipe2(
name,
Map[ValidationErrors](func(n string) string { return n }),
Map[ValidationErrors](func(n string) string { return n + " validated" }),
)
assert.True(t, IsRight(result))
value := GetOrElse(func(ValidationErrors) string { return "" })(result)
assert.Equal(t, "Alice validated", value)
})
t.Run("all validations fail - accumulates all errors", func(t *testing.T) {
name := validateName("ab")
age := validateAge(16)
email := validateEmail("")
// Manually combine errors using the semigroup
var allErrors ValidationErrors
if IsLeft(name) {
allErrors = Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(name)
}
if IsLeft(age) {
ageErrors := Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(age)
allErrors = sg.Concat(allErrors, ageErrors)
}
if IsLeft(email) {
emailErrors := Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(email)
allErrors = sg.Concat(allErrors, emailErrors)
}
assert.Len(t, allErrors, 3)
assert.Contains(t, allErrors, "Name must be at least 3 characters")
assert.Contains(t, allErrors, "Must be 18 or older")
assert.Contains(t, allErrors, "Email is required")
})
t.Run("partial validation failure", func(t *testing.T) {
name := validateName("Alice")
age := validateAge(16)
email := validateEmail("")
// Verify name passes
assert.True(t, IsRight(name))
// Manually combine errors using the semigroup
var allErrors ValidationErrors
if IsLeft(age) {
allErrors = Fold(F.Identity[ValidationErrors], F.Constant1[int](ValidationErrors{}))(age)
}
if IsLeft(email) {
emailErrors := Fold(F.Identity[ValidationErrors], F.Constant1[string](ValidationErrors{}))(email)
if len(allErrors) > 0 {
allErrors = sg.Concat(allErrors, emailErrors)
} else {
allErrors = emailErrors
}
}
assert.Len(t, allErrors, 2)
assert.Contains(t, allErrors, "Must be 18 or older")
assert.Contains(t, allErrors, "Email is required")
})
}
// TestApplicativeVsApplicativeV demonstrates the difference between fail-fast and validation
func TestApplicativeVsApplicativeV(t *testing.T) {
t.Run("Applicative fails fast", func(t *testing.T) {
app := Applicative[error, int, int]()
err1 := errors.New("error1")
err2 := errors.New("error2")
eitherFunc := Left[func(int) int](err1)
eitherValue := Left[int](err2)
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
// Only the first error is returned
assert.Equal(t, err1, ToError(result))
})
t.Run("ApplicativeV accumulates errors", func(t *testing.T) {
sg := S.MakeSemigroup(func(a, b string) string { return a + "; " + b })
app := ApplicativeV[string, int, int](sg)
eitherFunc := Left[func(int) int]("error1")
eitherValue := Left[int]("error2")
result := app.Ap(eitherValue)(eitherFunc)
assert.True(t, IsLeft(result))
// Both errors are accumulated
combined := Fold(F.Identity[string], F.Constant1[int](""))(result)
assert.Equal(t, "error1; error2", combined)
})
}

View File

@@ -570,3 +570,41 @@ func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
func MonadAlt[E, A any](fa Either[E, A], that Lazy[Either[E, A]]) Either[E, A] {
return MonadFold(fa, F.Ignore1of1[E](that), Of[E, A])
}
// Zero returns the zero value of an [Either], which is a Right containing the zero value of type A.
// This function is useful as an identity element in monoid operations or for creating an empty Either
// in a Right state.
//
// The returned Either is always a Right value containing the zero value of type A. For reference types
// (pointers, slices, maps, channels, functions, interfaces), the zero value is nil. For value types
// (numbers, booleans, structs), it's the type's zero value.
//
// Important: Zero() returns the same value as the default initialization of Either[E, A].
// When you declare `var e Either[E, A]` without initialization, it has the same value as Zero[E, A]().
//
// Note: This differs from creating a Left value, which would represent an error or failure state.
// Zero always produces a successful (Right) state with a zero value.
//
// Example:
//
// // Zero Either with int value
// e1 := either.Zero[error, int]() // Right(0)
//
// // Zero Either with string value
// e2 := either.Zero[error, string]() // Right("")
//
// // Zero Either with pointer type
// e3 := either.Zero[error, *int]() // Right(nil)
//
// // Zero equals default initialization
// var defaultInit Either[error, int]
// zero := either.Zero[error, int]()
// assert.Equal(t, defaultInit, zero) // true
//
// // Verify it's a Right value
// e := either.Zero[error, int]()
// assert.True(t, either.IsRight(e)) // true
// assert.False(t, either.IsLeft(e)) // false
func Zero[E, A any]() Either[E, A] {
return Either[E, A]{isLeft: false}
}

View File

@@ -119,3 +119,227 @@ func TestStringer(t *testing.T) {
var s fmt.Stringer = &e
assert.Equal(t, exp, s.String())
}
// TestZeroWithIntegers tests Zero function with integer types
func TestZeroWithIntegers(t *testing.T) {
e := Zero[error, int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, 0, value, "Right value should be zero for int")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithStrings tests Zero function with string types
func TestZeroWithStrings(t *testing.T) {
e := Zero[error, string]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, "", value, "Right value should be empty string")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithBooleans tests Zero function with boolean types
func TestZeroWithBooleans(t *testing.T) {
e := Zero[error, bool]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Equal(t, false, value, "Right value should be false for bool")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithFloats tests Zero function with float types
func TestZeroWithFloats(t *testing.T) {
e := Zero[error, float64]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Equal(t, 0.0, value, "Right value should be 0.0 for float64")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithPointers tests Zero function with pointer types
func TestZeroWithPointers(t *testing.T) {
e := Zero[error, *int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for pointer type")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithSlices tests Zero function with slice types
func TestZeroWithSlices(t *testing.T) {
e := Zero[error, []int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for slice type")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithMaps tests Zero function with map types
func TestZeroWithMaps(t *testing.T) {
e := Zero[error, map[string]int]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for map type")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithStructs tests Zero function with struct types
func TestZeroWithStructs(t *testing.T) {
type TestStruct struct {
Field1 int
Field2 string
}
e := Zero[error, TestStruct]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
expected := TestStruct{Field1: 0, Field2: ""}
assert.Equal(t, expected, value, "Right value should be zero value for struct")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithInterfaces tests Zero function with interface types
func TestZeroWithInterfaces(t *testing.T) {
e := Zero[error, interface{}]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.Nil(t, value, "Right value should be nil for interface type")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithCustomErrorType tests Zero function with custom error types
func TestZeroWithCustomErrorType(t *testing.T) {
type CustomError struct {
Code int
Message string
}
e := Zero[CustomError, string]()
assert.True(t, IsRight(e), "Zero should create a Right value")
assert.False(t, IsLeft(e), "Zero should not create a Left value")
value, err := Unwrap(e)
assert.Equal(t, "", value, "Right value should be empty string")
assert.Equal(t, CustomError{Code: 0, Message: ""}, err, "Error should be zero value for CustomError")
}
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Eithers work with other either functions
func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
e := Zero[error, int]()
// Test with Map
mapped := MonadMap(e, func(n int) string {
return fmt.Sprintf("%d", n)
})
assert.True(t, IsRight(mapped), "Mapped Zero should still be Right")
value, _ := Unwrap(mapped)
assert.Equal(t, "0", value, "Mapped value should be '0'")
// Test with Chain
chained := MonadChain(e, func(n int) Either[error, string] {
return Right[error](fmt.Sprintf("value: %d", n))
})
assert.True(t, IsRight(chained), "Chained Zero should still be Right")
chainedValue, _ := Unwrap(chained)
assert.Equal(t, "value: 0", chainedValue, "Chained value should be 'value: 0'")
// Test with Fold
folded := MonadFold(e,
func(err error) string { return "error" },
func(n int) string { return fmt.Sprintf("success: %d", n) },
)
assert.Equal(t, "success: 0", folded, "Folded value should be 'success: 0'")
}
// TestZeroEquality tests that multiple Zero calls produce equal Eithers
func TestZeroEquality(t *testing.T) {
e1 := Zero[error, int]()
e2 := Zero[error, int]()
assert.Equal(t, IsRight(e1), IsRight(e2), "Both should be Right")
assert.Equal(t, IsLeft(e1), IsLeft(e2), "Both should not be Left")
v1, err1 := Unwrap(e1)
v2, err2 := Unwrap(e2)
assert.Equal(t, v1, v2, "Values should be equal")
assert.Equal(t, err1, err2, "Errors should be equal")
}
// TestZeroWithComplexTypes tests Zero with more complex nested types
func TestZeroWithComplexTypes(t *testing.T) {
type ComplexType struct {
Nested map[string][]int
Ptr *string
}
e := Zero[error, ComplexType]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
expected := ComplexType{Nested: nil, Ptr: nil}
assert.Equal(t, expected, value, "Right value should be zero value for complex struct")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroWithOption tests Zero with Option type
func TestZeroWithOption(t *testing.T) {
e := Zero[error, O.Option[int]]()
assert.True(t, IsRight(e), "Zero should create a Right value")
value, err := Unwrap(e)
assert.True(t, O.IsNone(value), "Right value should be None for Option type")
assert.Nil(t, err, "Error should be nil for Right value")
}
// TestZeroIsNotLeft tests that Zero never creates a Left value
func TestZeroIsNotLeft(t *testing.T) {
// Test with various type combinations
e1 := Zero[string, int]()
e2 := Zero[error, string]()
e3 := Zero[int, bool]()
assert.False(t, IsLeft(e1), "Zero should never create a Left value")
assert.False(t, IsLeft(e2), "Zero should never create a Left value")
assert.False(t, IsLeft(e3), "Zero should never create a Left value")
assert.True(t, IsRight(e1), "Zero should always create a Right value")
assert.True(t, IsRight(e2), "Zero should always create a Right value")
assert.True(t, IsRight(e3), "Zero should always create a Right value")
}
// TestZeroEqualsDefaultInitialization tests that Zero returns the same value as default initialization
func TestZeroEqualsDefaultInitialization(t *testing.T) {
// Default initialization of Either
var defaultInit Either[error, int]
// Zero function
zero := Zero[error, int]()
// They should be equal
assert.Equal(t, defaultInit, zero, "Zero should equal default initialization")
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
}

View File

@@ -25,7 +25,7 @@ import (
// TestFirstMonoid tests the FirstMonoid implementation
func TestFirstMonoid(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[error, int](zero)
m := FirstMonoid(zero)
t.Run("both Right values - returns first", func(t *testing.T) {
result := m.Concat(Right[error](2), Right[error](3))
@@ -94,7 +94,7 @@ func TestFirstMonoid(t *testing.T) {
t.Run("with strings", func(t *testing.T) {
zeroStr := func() Either[error, string] { return Left[string](errors.New("empty")) }
strMonoid := FirstMonoid[error, string](zeroStr)
strMonoid := FirstMonoid(zeroStr)
result := strMonoid.Concat(Right[error]("first"), Right[error]("second"))
assert.Equal(t, Right[error]("first"), result)
@@ -107,7 +107,7 @@ func TestFirstMonoid(t *testing.T) {
// TestLastMonoid tests the LastMonoid implementation
func TestLastMonoid(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := LastMonoid[error, int](zero)
m := LastMonoid(zero)
t.Run("both Right values - returns last", func(t *testing.T) {
result := m.Concat(Right[error](2), Right[error](3))
@@ -176,7 +176,7 @@ func TestLastMonoid(t *testing.T) {
t.Run("with strings", func(t *testing.T) {
zeroStr := func() Either[error, string] { return Left[string](errors.New("empty")) }
strMonoid := LastMonoid[error, string](zeroStr)
strMonoid := LastMonoid(zeroStr)
result := strMonoid.Concat(Right[error]("first"), Right[error]("second"))
assert.Equal(t, Right[error]("second"), result)
@@ -189,8 +189,8 @@ func TestLastMonoid(t *testing.T) {
// TestFirstMonoidVsAltMonoid verifies FirstMonoid and AltMonoid have the same behavior
func TestFirstMonoidVsAltMonoid(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
firstMonoid := FirstMonoid[error, int](zero)
altMonoid := AltMonoid[error, int](zero)
firstMonoid := FirstMonoid(zero)
altMonoid := AltMonoid(zero)
testCases := []struct {
name string
@@ -223,8 +223,8 @@ func TestFirstMonoidVsAltMonoid(t *testing.T) {
// TestFirstMonoidVsLastMonoid verifies the difference between FirstMonoid and LastMonoid
func TestFirstMonoidVsLastMonoid(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
firstMonoid := FirstMonoid[error, int](zero)
lastMonoid := LastMonoid[error, int](zero)
firstMonoid := FirstMonoid(zero)
lastMonoid := LastMonoid(zero)
t.Run("both Right - different results", func(t *testing.T) {
firstResult := firstMonoid.Concat(Right[error](1), Right[error](2))
@@ -279,7 +279,7 @@ func TestFirstMonoidVsLastMonoid(t *testing.T) {
func TestMonoidLaws(t *testing.T) {
t.Run("FirstMonoid laws", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[error, int](zero)
m := FirstMonoid(zero)
a := Right[error](1)
b := Right[error](2)
@@ -301,7 +301,7 @@ func TestMonoidLaws(t *testing.T) {
t.Run("LastMonoid laws", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := LastMonoid[error, int](zero)
m := LastMonoid(zero)
a := Right[error](1)
b := Right[error](2)
@@ -323,7 +323,7 @@ func TestMonoidLaws(t *testing.T) {
t.Run("FirstMonoid laws with Left values", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[error, int](zero)
m := FirstMonoid(zero)
a := Left[int](errors.New("err1"))
b := Left[int](errors.New("err2"))
@@ -337,7 +337,7 @@ func TestMonoidLaws(t *testing.T) {
t.Run("LastMonoid laws with Left values", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := LastMonoid[error, int](zero)
m := LastMonoid(zero)
a := Left[int](errors.New("err1"))
b := Left[int](errors.New("err2"))
@@ -354,7 +354,7 @@ func TestMonoidLaws(t *testing.T) {
func TestMonoidEdgeCases(t *testing.T) {
t.Run("FirstMonoid with empty concatenations", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[error, int](zero)
m := FirstMonoid(zero)
// Empty with empty
result := m.Concat(m.Empty(), m.Empty())
@@ -363,7 +363,7 @@ func TestMonoidEdgeCases(t *testing.T) {
t.Run("LastMonoid with empty concatenations", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := LastMonoid[error, int](zero)
m := LastMonoid(zero)
// Empty with empty
result := m.Concat(m.Empty(), m.Empty())
@@ -372,7 +372,7 @@ func TestMonoidEdgeCases(t *testing.T) {
t.Run("FirstMonoid chain of operations", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[error, int](zero)
m := FirstMonoid(zero)
// Chain multiple operations
result := m.Concat(
@@ -387,7 +387,7 @@ func TestMonoidEdgeCases(t *testing.T) {
t.Run("LastMonoid chain of operations", func(t *testing.T) {
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
m := LastMonoid[error, int](zero)
m := LastMonoid(zero)
// Chain multiple operations
result := m.Concat(

91
v2/either/profunctor.go Normal file
View File

@@ -0,0 +1,91 @@
// 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 either
import F "github.com/IBM/fp-go/v2/function"
// MonadExtend applies a function to an Either value, where the function receives the entire Either as input.
// This is the Extend (or Comonad) operation that allows computations to depend on the context.
//
// If the Either is Left, it returns Left unchanged without applying the function.
// If the Either is Right, it applies the function to the entire Either and wraps the result in a Right.
//
// This operation is useful when you need to perform computations that depend on whether
// a value is present (Right) or absent (Left), not just on the value itself.
//
// Type Parameters:
// - E: The error type (Left channel)
// - A: The input value type (Right channel)
// - B: The output value type
//
// Parameters:
// - fa: The Either value to extend
// - f: Function that takes the entire Either[E, A] and produces a value of type B
//
// Returns:
// - Either[E, B]: Left if input was Left, otherwise Right containing the result of f(fa)
//
// Example:
//
// // Count how many times we've seen a Right value
// counter := func(e either.Either[error, int]) int {
// return either.Fold(
// func(err error) int { return 0 },
// func(n int) int { return 1 },
// )(e)
// }
// result := either.MonadExtend(either.Right[error](42), counter) // Right(1)
// result := either.MonadExtend(either.Left[int](errors.New("err")), counter) // Left(error)
//
//go:inline
func MonadExtend[E, A, B any](fa Either[E, A], f func(Either[E, A]) B) Either[E, B] {
if fa.isLeft {
return Left[B](fa.l)
}
return Of[E](f(fa))
}
// Extend is the curried version of [MonadExtend].
// It returns a function that applies the given function to an Either value.
//
// This is useful for creating reusable transformations that depend on the Either context.
//
// Type Parameters:
// - E: The error type (Left channel)
// - A: The input value type (Right channel)
// - B: The output value type
//
// Parameters:
// - f: Function that takes the entire Either[E, A] and produces a value of type B
//
// Returns:
// - Operator[E, A, B]: A function that transforms Either[E, A] to Either[E, B]
//
// Example:
//
// // Create a reusable extender that extracts metadata
// getMetadata := either.Extend(func(e either.Either[error, string]) string {
// return either.Fold(
// func(err error) string { return "error: " + err.Error() },
// func(s string) string { return "value: " + s },
// )(e)
// })
// result := getMetadata(either.Right[error]("hello")) // Right("value: hello")
//
//go:inline
func Extend[E, A, B any](f func(Either[E, A]) B) Operator[E, A, B] {
return F.Bind2nd(MonadExtend[E, A, B], f)
}

View File

@@ -0,0 +1,375 @@
// 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 either
import (
"errors"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestMonadExtendWithRight tests MonadExtend with Right values
func TestMonadExtendWithRight(t *testing.T) {
t.Run("applies function to Right value", func(t *testing.T) {
input := Right[error](42)
// Function that extracts and doubles the value if Right
f := func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, 84, GetOrElse(F.Constant1[error](0))(result))
})
t.Run("function receives entire Either context", func(t *testing.T) {
input := Right[error]("hello")
// Function that creates metadata about the Either
f := func(e Either[error, string]) string {
return Fold(
func(err error) string { return "error: " + err.Error() },
S.Prepend("value: "),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, "value: hello", GetOrElse(func(error) string { return "" })(result))
})
t.Run("can count Right occurrences", func(t *testing.T) {
input := Right[error](100)
counter := func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
F.Constant1[int](1),
)(e)
}
result := MonadExtend(input, counter)
assert.True(t, IsRight(result))
assert.Equal(t, 1, GetOrElse(func(error) int { return -1 })(result))
})
}
// TestMonadExtendWithLeft tests MonadExtend with Left values
func TestMonadExtendWithLeft(t *testing.T) {
t.Run("returns Left without applying function", func(t *testing.T) {
testErr := errors.New("test error")
input := Left[int](testErr)
// Function should not be called
called := false
f := func(e Either[error, int]) int {
called = true
return 42
}
result := MonadExtend(input, f)
assert.False(t, called, "function should not be called for Left")
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
t.Run("preserves Left error type", func(t *testing.T) {
input := Left[string](errors.New("original error"))
f := func(e Either[error, string]) string {
return "should not be called"
}
result := MonadExtend(input, f)
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, "original error", leftVal.Error())
})
}
// TestMonadExtendEdgeCases tests edge cases for MonadExtend
func TestMonadExtendEdgeCases(t *testing.T) {
t.Run("function returns zero value", func(t *testing.T) {
input := Right[error](42)
f := func(e Either[error, int]) int {
return 0
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, 0, GetOrElse(func(error) int { return -1 })(result))
})
t.Run("function changes type", func(t *testing.T) {
input := Right[error](42)
f := func(e Either[error, int]) string {
return Fold(
F.Constant1[error]("error"),
S.Format[int]("number: %d"),
)(e)
}
result := MonadExtend(input, f)
assert.True(t, IsRight(result))
assert.Equal(t, "number: 42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("nested Either handling", func(t *testing.T) {
inner := Right[error](10)
outer := Right[error](inner)
// Extract the inner value
f := func(e Either[error, Either[error, int]]) int {
return Fold(
F.Constant1[error](-1),
func(innerEither Either[error, int]) int {
return GetOrElse(F.Constant1[error](-2))(innerEither)
},
)(e)
}
result := MonadExtend(outer, f)
assert.True(t, IsRight(result))
assert.Equal(t, 10, GetOrElse(F.Constant1[error](-3))(result))
})
}
// TestExtendWithRight tests Extend (curried version) with Right values
func TestExtendWithRight(t *testing.T) {
t.Run("creates reusable extender", func(t *testing.T) {
// Create a reusable extender
doubler := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
})
result1 := doubler(Right[error](21))
result2 := doubler(Right[error](50))
assert.True(t, IsRight(result1))
assert.Equal(t, 42, GetOrElse(F.Constant1[error](0))(result1))
assert.True(t, IsRight(result2))
assert.Equal(t, 100, GetOrElse(F.Constant1[error](0))(result2))
})
t.Run("metadata extractor", func(t *testing.T) {
getMetadata := Extend(func(e Either[error, string]) string {
return Fold(
func(err error) string { return "error: " + err.Error() },
S.Prepend("value: "),
)(e)
})
result := getMetadata(Right[error]("test"))
assert.True(t, IsRight(result))
assert.Equal(t, "value: test", GetOrElse(func(error) string { return "" })(result))
})
t.Run("composition with other operations", func(t *testing.T) {
// Create an extender that counts characters
charCounter := Extend(func(e Either[error, string]) int {
return Fold(
F.Constant1[error](0),
S.Size,
)(e)
})
// Apply to a Right value
input := Right[error]("hello")
result := charCounter(input)
assert.True(t, IsRight(result))
assert.Equal(t, 5, GetOrElse(func(error) int { return -1 })(result))
})
}
// TestExtendWithLeft tests Extend with Left values
func TestExtendWithLeft(t *testing.T) {
t.Run("returns Left without calling function", func(t *testing.T) {
testErr := errors.New("test error")
called := false
extender := Extend(func(e Either[error, int]) int {
called = true
return 42
})
result := extender(Left[int](testErr))
assert.False(t, called, "function should not be called for Left")
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
t.Run("preserves error through multiple applications", func(t *testing.T) {
originalErr := errors.New("original")
extender := Extend(func(e Either[error, string]) string {
return "transformed"
})
result := extender(Left[string](originalErr))
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, originalErr, leftVal)
})
}
// TestExtendChaining tests chaining multiple Extend operations
func TestExtendChaining(t *testing.T) {
t.Run("chain multiple extenders", func(t *testing.T) {
// First extender: double the value
doubler := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Mul(2),
)(e)
})
// Second extender: add 10
adder := Extend(func(e Either[error, int]) int {
return Fold(
F.Constant1[error](0),
N.Add(10),
)(e)
})
input := Right[error](5)
result := adder(doubler(input))
assert.True(t, IsRight(result))
assert.Equal(t, 20, GetOrElse(F.Constant1[error](0))(result))
})
t.Run("short-circuits on Left", func(t *testing.T) {
testErr := errors.New("error")
extender1 := Extend(func(e Either[error, int]) int { return 1 })
extender2 := Extend(func(e Either[error, int]) int { return 2 })
input := Left[int](testErr)
result := extender2(extender1(input))
assert.True(t, IsLeft(result))
_, leftVal := Unwrap(result)
assert.Equal(t, testErr, leftVal)
})
}
// TestExtendTypeTransformations tests type transformations with Extend
func TestExtendTypeTransformations(t *testing.T) {
t.Run("int to string transformation", func(t *testing.T) {
toString := Extend(func(e Either[error, int]) string {
return Fold(
F.Constant1[error]("error"),
strconv.Itoa,
)(e)
})
result := toString(Right[error](42))
assert.True(t, IsRight(result))
assert.Equal(t, "42", GetOrElse(func(error) string { return "" })(result))
})
t.Run("string to bool transformation", func(t *testing.T) {
isEmpty := Extend(func(e Either[error, string]) bool {
return Fold(
F.Constant1[error](true),
S.IsEmpty,
)(e)
})
result1 := isEmpty(Right[error](""))
result2 := isEmpty(Right[error]("hello"))
assert.True(t, IsRight(result1))
assert.True(t, GetOrElse(F.Constant1[error](false))(result1))
assert.True(t, IsRight(result2))
assert.False(t, GetOrElse(F.Constant1[error](true))(result2))
})
}
// TestExtendWithComplexTypes tests Extend with complex types
func TestExtendWithComplexTypes(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("extract field from struct", func(t *testing.T) {
getName := Extend(func(e Either[error, User]) string {
return Fold(
func(err error) string { return "unknown" },
func(u User) string { return u.Name },
)(e)
})
user := User{Name: "Alice", Age: 30}
result := getName(Right[error](user))
assert.True(t, IsRight(result))
assert.Equal(t, "Alice", GetOrElse(func(error) string { return "" })(result))
})
t.Run("compute derived value", func(t *testing.T) {
isAdult := Extend(func(e Either[error, User]) bool {
return Fold(
func(err error) bool { return false },
func(u User) bool { return u.Age >= 18 },
)(e)
})
user1 := User{Name: "Bob", Age: 25}
user2 := User{Name: "Charlie", Age: 15}
result1 := isAdult(Right[error](user1))
result2 := isAdult(Right[error](user2))
assert.True(t, IsRight(result1))
assert.True(t, GetOrElse(F.Constant1[error](false))(result1))
assert.True(t, IsRight(result2))
assert.False(t, GetOrElse(F.Constant1[error](true))(result2))
})
}

View File

@@ -19,6 +19,64 @@ import (
"github.com/IBM/fp-go/v2/tailrec"
)
// TailRec converts a tail-recursive Kleisli arrow into a stack-safe iterative computation.
//
// This function enables writing recursive algorithms in a functional style while avoiding
// stack overflow errors. It takes a Kleisli arrow that returns a Trampoline wrapped in Either,
// and converts it into a regular Kleisli arrow that executes the recursion iteratively.
//
// The function handles both success and failure cases:
// - If any step returns Left[E], the recursion stops and returns that error
// - If a step returns Right with Landed=true, the final result is returned
// - If a step returns Right with Landed=false, recursion continues with the bounced value
//
// Type Parameters:
// - E: The error type (Left case)
// - A: The input type for each recursive step
// - B: The final result type (Right case)
//
// Parameters:
// - f: A Kleisli arrow that takes an input of type A and returns Either[E, Trampoline[A, B]]
// The Trampoline indicates whether to continue (Bounce) or terminate (Land)
//
// Returns:
// - A Kleisli arrow that executes the tail recursion iteratively and returns Either[E, B]
//
// Example - Factorial with error handling:
//
// type State struct { n, acc int }
//
// factorialStep := func(state State) either.Either[string, tailrec.Trampoline[State, int]] {
// if state.n < 0 {
// return either.Left[tailrec.Trampoline[State, int]]("negative input")
// }
// if state.n <= 1 {
// return either.Right[string](tailrec.Land[State](state.acc))
// }
// return either.Right[string](tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
// }
//
// factorial := either.TailRec(factorialStep)
// result := factorial(State{5, 1}) // Right(120)
// error := factorial(State{-1, 1}) // Left("negative input")
//
// Example - Countdown with validation:
//
// countdown := either.TailRec(func(n int) either.Either[string, tailrec.Trampoline[int, int]] {
// if n < 0 {
// return either.Left[tailrec.Trampoline[int, int]]("already negative")
// }
// if n == 0 {
// return either.Right[string](tailrec.Land[int](0))
// }
// return either.Right[string](tailrec.Bounce[int](n - 1))
// })
//
// result := countdown(5) // Right(0)
//
// The function is stack-safe and can handle arbitrarily deep recursion without
// causing stack overflow, as it uses iteration internally rather than actual recursion.
//
//go:inline
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
return func(a A) Either[E, B] {

495
v2/either/rec_test.go Normal file
View File

@@ -0,0 +1,495 @@
// 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 either
import (
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
TR "github.com/IBM/fp-go/v2/tailrec"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests factorial computation with error handling
func TestTailRecFactorial(t *testing.T) {
type State struct {
n int
acc int
}
factorialStep := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.n < 0 {
return Left[TR.Trampoline[State, int]]("negative input not allowed")
}
if state.n <= 1 {
return Right[string](TR.Land[State](state.acc))
}
return Right[string](TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
}
factorial := TailRec(factorialStep)
// Test successful computation
result := factorial(State{5, 1})
assert.Equal(t, Of[string](120), result)
// Test base case
result = factorial(State{0, 1})
assert.Equal(t, Of[string](1), result)
// Test error case
result = factorial(State{-1, 1})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Equal(t, "negative input not allowed", err)
}
// TestTailRecFibonacci tests Fibonacci computation with validation
func TestTailRecFibonacci(t *testing.T) {
type State struct {
n int
prev int
curr int
}
fibStep := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.n < 0 {
return Left[TR.Trampoline[State, int]]("negative index")
}
if state.curr > 1000 {
return Left[TR.Trampoline[State, int]](fmt.Sprintf("value too large: %d", state.curr))
}
if state.n <= 0 {
return Right[string](TR.Land[State](state.curr))
}
return Right[string](TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
}
fib := TailRec(fibStep)
// Test successful computation
result := fib(State{10, 0, 1})
assert.Equal(t, Of[string](89), result) // 10th Fibonacci number
// Test base case
result = fib(State{0, 0, 1})
assert.Equal(t, Of[string](1), result)
// Test error case - negative
result = fib(State{-1, 0, 1})
assert.True(t, IsLeft(result))
// Test error case - value too large
result = fib(State{20, 0, 1})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Contains(t, err, "value too large")
}
// TestTailRecCountdown tests countdown with validation
func TestTailRecCountdown(t *testing.T) {
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
if n < 0 {
return Left[TR.Trampoline[int, int]]("already negative")
}
if n == 0 {
return Right[string](TR.Land[int](0))
}
return Right[string](TR.Bounce[int](n - 1))
}
countdown := TailRec(countdownStep)
// Test successful countdown
result := countdown(10)
assert.Equal(t, Of[string](0), result)
// Test immediate termination
result = countdown(0)
assert.Equal(t, Of[string](0), result)
// Test error case
result = countdown(-5)
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Equal(t, "already negative", err)
}
// TestTailRecSumList tests summing a list with error handling
func TestTailRecSumList(t *testing.T) {
type State struct {
list []int
sum int
}
sumStep := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.sum > 100 {
return Left[TR.Trampoline[State, int]](fmt.Sprintf("sum exceeds limit: %d", state.sum))
}
if A.IsEmpty(state.list) {
return Right[string](TR.Land[State](state.sum))
}
return Right[string](TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
}
sumList := TailRec(sumStep)
// Test successful sum
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})
assert.Equal(t, Of[string](15), result)
// Test empty list
result = sumList(State{[]int{}, 0})
assert.Equal(t, Of[string](0), result)
// Test error case - sum too large
result = sumList(State{[]int{50, 60}, 0})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Contains(t, err, "sum exceeds limit")
}
// TestTailRecImmediateTermination tests immediate termination (Land on first call)
func TestTailRecImmediateTermination(t *testing.T) {
immediateStep := func(n int) Either[string, TR.Trampoline[int, int]] {
return Right[string](TR.Land[int](n * 2))
}
immediate := TailRec(immediateStep)
result := immediate(21)
assert.Equal(t, Of[string](42), result)
}
// TestTailRecImmediateError tests immediate error (Left on first call)
func TestTailRecImmediateError(t *testing.T) {
immediateErrorStep := func(n int) Either[string, TR.Trampoline[int, int]] {
return Left[TR.Trampoline[int, int]]("immediate error")
}
immediateError := TailRec(immediateErrorStep)
result := immediateError(42)
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Equal(t, "immediate error", err)
}
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
func TestTailRecStackSafety(t *testing.T) {
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
if n <= 0 {
return Right[string](TR.Land[int](n))
}
return Right[string](TR.Bounce[int](n - 1))
}
countdown := TailRec(countdownStep)
result := countdown(10000)
assert.Equal(t, Of[string](0), result)
}
// TestTailRecFindInRange tests finding a value in a range
func TestTailRecFindInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.current > 1000 {
return Left[TR.Trampoline[State, int]]("search exceeded maximum iterations")
}
if state.current >= state.max {
return Right[string](TR.Land[State](-1)) // Not found
}
if state.current == state.target {
return Right[string](TR.Land[State](state.current)) // Found
}
return Right[string](TR.Bounce[int](State{state.current + 1, state.max, state.target}))
}
find := TailRec(findStep)
// Test found
result := find(State{0, 100, 42})
assert.Equal(t, Of[string](42), result)
// Test not found
result = find(State{0, 100, 200})
assert.Equal(t, Of[string](-1), result)
// Test error - exceeded iterations
result = find(State{0, 2000, 1500})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Contains(t, err, "exceeded maximum")
}
// TestTailRecCollatzConjecture tests the Collatz conjecture
func TestTailRecCollatzConjecture(t *testing.T) {
collatzStep := func(n int) Either[string, TR.Trampoline[int, int]] {
if n <= 0 {
return Left[TR.Trampoline[int, int]]("invalid input: must be positive")
}
if n == 1 {
return Right[string](TR.Land[int](1))
}
if n%2 == 0 {
return Right[string](TR.Bounce[int](n / 2))
}
return Right[string](TR.Bounce[int](3*n + 1))
}
collatz := TailRec(collatzStep)
// Test various starting points
result := collatz(10)
assert.Equal(t, Of[string](1), result)
result = collatz(27)
assert.Equal(t, Of[string](1), result)
// Test error case
result = collatz(0)
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Contains(t, err, "invalid input")
}
// TestTailRecGCD tests greatest common divisor computation
func TestTailRecGCD(t *testing.T) {
type State struct {
a int
b int
}
gcdStep := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.a < 0 || state.b < 0 {
return Left[TR.Trampoline[State, int]]("negative values not allowed")
}
if state.b == 0 {
return Right[string](TR.Land[State](state.a))
}
return Right[string](TR.Bounce[int](State{state.b, state.a % state.b}))
}
gcd := TailRec(gcdStep)
// Test successful GCD
result := gcd(State{48, 18})
assert.Equal(t, Of[string](6), result)
result = gcd(State{100, 35})
assert.Equal(t, Of[string](5), result)
// Test error case
result = gcd(State{-10, 5})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Contains(t, err, "negative values")
}
// TestTailRecPowerOfTwo tests computing powers of 2
func TestTailRecPowerOfTwo(t *testing.T) {
type State struct {
exponent int
result int
target int
}
powerStep := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.target < 0 {
return Left[TR.Trampoline[State, int]]("negative exponent not supported")
}
if state.exponent >= state.target {
return Right[string](TR.Land[State](state.result))
}
return Right[string](TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}))
}
power := TailRec(powerStep)
// Test 2^10
result := power(State{0, 1, 10})
assert.Equal(t, Of[string](1024), result)
// Test 2^0
result = power(State{0, 1, 0})
assert.Equal(t, Of[string](1), result)
// Test error case
result = power(State{0, 1, -1})
assert.True(t, IsLeft(result))
}
// TestTailRecErrorInMiddle tests error occurring in the middle of recursion
func TestTailRecErrorInMiddle(t *testing.T) {
countdownStep := func(n int) Either[string, TR.Trampoline[int, int]] {
if n == 5 {
return Left[TR.Trampoline[int, int]]("error at 5")
}
if n <= 0 {
return Right[string](TR.Land[int](n))
}
return Right[string](TR.Bounce[int](n - 1))
}
countdown := TailRec(countdownStep)
result := countdown(10)
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Equal(t, "error at 5", err)
}
// TestTailRecMultipleErrorConditions tests multiple error conditions
func TestTailRecMultipleErrorConditions(t *testing.T) {
type State struct {
value int
steps int
}
step := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.steps > 100 {
return Left[TR.Trampoline[State, int]]("too many steps")
}
if state.value < 0 {
return Left[TR.Trampoline[State, int]]("negative value encountered")
}
if state.value == 0 {
return Right[string](TR.Land[State](state.steps))
}
return Right[string](TR.Bounce[int](State{state.value - 1, state.steps + 1}))
}
counter := TailRec(step)
// Test successful case
result := counter(State{10, 0})
assert.Equal(t, Of[string](10), result)
// Test too many steps error
result = counter(State{200, 0})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Contains(t, err, "too many steps")
}
// TestTailRecWithComplexState tests recursion with complex state
func TestTailRecWithComplexState(t *testing.T) {
type State struct {
numbers []int
sum int
product int
}
processStep := func(state State) Either[string, TR.Trampoline[State, State]] {
if state.product > 10000 {
return Left[TR.Trampoline[State, State]]("product overflow")
}
if A.IsEmpty(state.numbers) {
return Right[string](TR.Land[State](state))
}
head := state.numbers[0]
tail := state.numbers[1:]
return Right[string](TR.Bounce[State](State{
numbers: tail,
sum: state.sum + head,
product: state.product * head,
}))
}
process := TailRec(processStep)
// Test successful processing
result := process(State{[]int{2, 3, 4}, 0, 1})
assert.True(t, IsRight(result))
finalState, _ := Unwrap(result)
assert.Equal(t, 9, finalState.sum)
assert.Equal(t, 24, finalState.product)
// Test overflow error
result = process(State{[]int{100, 200, 300}, 0, 1})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Contains(t, err, "product overflow")
}
// TestTailRecDivisionByZeroProtection tests protection against division by zero
func TestTailRecDivisionByZeroProtection(t *testing.T) {
type State struct {
numerator int
denominator int
result int
}
divideStep := func(state State) Either[string, TR.Trampoline[State, int]] {
if state.denominator == 0 {
return Left[TR.Trampoline[State, int]]("division by zero")
}
if state.numerator < state.denominator {
return Right[string](TR.Land[State](state.result))
}
return Right[string](TR.Bounce[int](State{
numerator: state.numerator - state.denominator,
denominator: state.denominator,
result: state.result + 1,
}))
}
divide := TailRec(divideStep)
// Test successful division
result := divide(State{10, 3, 0})
assert.Equal(t, Of[string](3), result) // 10 / 3 = 3 (integer division)
// Test division by zero
result = divide(State{10, 0, 0})
assert.True(t, IsLeft(result))
_, err := Unwrap(result)
assert.Equal(t, "division by zero", err)
}
// TestTailRecStringProcessing tests recursion with string processing
func TestTailRecStringProcessing(t *testing.T) {
type State struct {
remaining string
count int
}
countVowels := func(state State) Either[string, TR.Trampoline[State, int]] {
if len(state.remaining) == 0 {
return Right[string](TR.Land[State](state.count))
}
char := state.remaining[0]
isVowel := char == 'a' || char == 'e' || char == 'i' || char == 'o' || char == 'u' ||
char == 'A' || char == 'E' || char == 'I' || char == 'O' || char == 'U'
newCount := state.count
if isVowel {
newCount++
}
return Right[string](TR.Bounce[int](State{state.remaining[1:], newCount}))
}
counter := TailRec(countVowels)
result := counter(State{"hello world", 0})
assert.Equal(t, Of[string](3), result) // e, o, o
}

View File

@@ -20,6 +20,8 @@ package eq
// by mapping the input type. It's particularly useful for comparing complex types by
// extracting comparable fields.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// The name "contramap" comes from category theory, where it represents a contravariant
// functor. Unlike regular map (covariant), which transforms the output, contramap
// transforms the input in the opposite direction.

89
v2/file/doc.go Normal file
View File

@@ -0,0 +1,89 @@
// 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 file provides functional programming utilities for working with file paths
// and I/O interfaces in Go.
//
// # Overview
//
// This package offers a collection of utility functions designed to work seamlessly
// with functional programming patterns, particularly with the fp-go library's pipe
// and composition utilities.
//
// # Path Manipulation
//
// The Join function provides a curried approach to path joining, making it easy to
// create reusable path builders:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// "github.com/IBM/fp-go/v2/file"
// )
//
// // Create a reusable path builder
// addConfig := file.Join("config.json")
// configPath := addConfig("/etc/myapp")
// // Result: "/etc/myapp/config.json"
//
// // Use in a functional pipeline
// logPath := F.Pipe1("/var/log", file.Join("app.log"))
// // Result: "/var/log/app.log"
//
// // Chain multiple joins
// deepPath := F.Pipe2(
// "/root",
// file.Join("subdir"),
// file.Join("file.txt"),
// )
// // Result: "/root/subdir/file.txt"
//
// # I/O Interface Conversions
//
// The package provides generic type conversion functions for common I/O interfaces.
// These are useful for type erasure when you need to work with interface types
// rather than concrete implementations:
//
// import (
// "bytes"
// "io"
// "github.com/IBM/fp-go/v2/file"
// )
//
// // Convert concrete types to interfaces
// buf := bytes.NewBuffer([]byte("hello"))
// var reader io.Reader = file.ToReader(buf)
//
// writer := &bytes.Buffer{}
// var w io.Writer = file.ToWriter(writer)
//
// f, _ := os.Open("file.txt")
// var closer io.Closer = file.ToCloser(f)
// defer closer.Close()
//
// # Design Philosophy
//
// The functions in this package follow functional programming principles:
//
// - Currying: Functions like Join return functions, enabling partial application
// - Type Safety: Generic functions maintain type safety while providing flexibility
// - Composability: All functions work well with fp-go's pipe and composition utilities
// - Immutability: Functions don't modify their inputs
//
// # Performance
//
// The type conversion functions (ToReader, ToWriter, ToCloser) have zero overhead
// as they simply return their input cast to the interface type. The Join function
// uses Go's standard filepath.Join internally, ensuring cross-platform compatibility.
package file

View File

@@ -13,6 +13,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides utility functions for working with file paths and I/O interfaces.
// It offers functional programming utilities for path manipulation and type conversions
// for common I/O interfaces.
package file
import (
@@ -20,24 +23,93 @@ import (
"path/filepath"
)
// Join appends a filename to a root path
func Join(name string) func(root string) string {
// Join appends a filename to a root path using the operating system's path separator.
// Returns a curried function that takes a root path and joins it with the provided name.
//
// This function follows the "data last" principle, where the data (root path) is provided
// last, making it ideal for use in functional pipelines and partial application. The name
// parameter is fixed first, creating a reusable path builder function.
//
// This is useful for creating reusable path builders in functional pipelines.
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Data last: fix the filename first, apply root path later
// addConfig := file.Join("config.json")
// path := addConfig("/etc/myapp")
// // path is "/etc/myapp/config.json" on Unix
// // path is "\etc\myapp\config.json" on Windows
//
// // Using with Pipe (data flows through the pipeline)
// result := F.Pipe1("/var/log", file.Join("app.log"))
// // result is "/var/log/app.log" on Unix
//
// // Chain multiple joins
// result := F.Pipe2(
// "/root",
// file.Join("subdir"),
// file.Join("file.txt"),
// )
// // result is "/root/subdir/file.txt"
func Join(name string) Endomorphism[string] {
return func(root string) string {
return filepath.Join(root, name)
}
}
// ToReader converts a [io.Reader]
// ToReader converts any type that implements io.Reader to the io.Reader interface.
// This is useful for type erasure when you need to work with the interface type
// rather than a concrete implementation.
//
// Example:
//
// import (
// "bytes"
// "io"
// )
//
// buf := bytes.NewBuffer([]byte("hello"))
// var reader io.Reader = file.ToReader(buf)
// // reader is now of type io.Reader
func ToReader[R io.Reader](r R) io.Reader {
return r
}
// ToWriter converts a [io.Writer]
// ToWriter converts any type that implements io.Writer to the io.Writer interface.
// This is useful for type erasure when you need to work with the interface type
// rather than a concrete implementation.
//
// Example:
//
// import (
// "bytes"
// "io"
// )
//
// buf := &bytes.Buffer{}
// var writer io.Writer = file.ToWriter(buf)
// // writer is now of type io.Writer
func ToWriter[W io.Writer](w W) io.Writer {
return w
}
// ToCloser converts a [io.Closer]
// ToCloser converts any type that implements io.Closer to the io.Closer interface.
// This is useful for type erasure when you need to work with the interface type
// rather than a concrete implementation.
//
// Example:
//
// import (
// "os"
// "io"
// )
//
// f, _ := os.Open("file.txt")
// var closer io.Closer = file.ToCloser(f)
// defer closer.Close()
// // closer is now of type io.Closer
func ToCloser[C io.Closer](c C) io.Closer {
return c
}

367
v2/file/getters_test.go Normal file
View File

@@ -0,0 +1,367 @@
// 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 file
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestJoin(t *testing.T) {
t.Run("joins simple paths", func(t *testing.T) {
result := Join("config.json")("/etc/myapp")
expected := filepath.Join("/etc/myapp", "config.json")
assert.Equal(t, expected, result)
})
t.Run("joins with subdirectories", func(t *testing.T) {
result := Join("logs/app.log")("/var")
expected := filepath.Join("/var", "logs/app.log")
assert.Equal(t, expected, result)
})
t.Run("handles empty root", func(t *testing.T) {
result := Join("file.txt")("")
assert.Equal(t, "file.txt", result)
})
t.Run("handles empty name", func(t *testing.T) {
result := Join("")("/root")
expected := filepath.Join("/root", "")
assert.Equal(t, expected, result)
})
t.Run("handles relative paths", func(t *testing.T) {
result := Join("config.json")("./app")
expected := filepath.Join("./app", "config.json")
assert.Equal(t, expected, result)
})
t.Run("normalizes path separators", func(t *testing.T) {
result := Join("file.txt")("/root/path")
// Should use OS-specific separator
assert.Contains(t, result, "file.txt")
assert.Contains(t, result, "root")
assert.Contains(t, result, "path")
})
t.Run("works with Pipe", func(t *testing.T) {
result := F.Pipe1("/var/log", Join("app.log"))
expected := filepath.Join("/var/log", "app.log")
assert.Equal(t, expected, result)
})
t.Run("chains multiple joins", func(t *testing.T) {
result := F.Pipe2(
"/root",
Join("subdir"),
Join("file.txt"),
)
expected := filepath.Join("/root", "subdir", "file.txt")
assert.Equal(t, expected, result)
})
t.Run("handles special characters", func(t *testing.T) {
result := Join("my file.txt")("/path with spaces")
expected := filepath.Join("/path with spaces", "my file.txt")
assert.Equal(t, expected, result)
})
t.Run("handles dots in path", func(t *testing.T) {
result := Join("../config.json")("/app/current")
expected := filepath.Join("/app/current", "../config.json")
assert.Equal(t, expected, result)
})
}
func TestToReader(t *testing.T) {
t.Run("converts bytes.Buffer to io.Reader", func(t *testing.T) {
buf := bytes.NewBuffer([]byte("hello world"))
reader := ToReader(buf)
// Verify it's an io.Reader
var _ io.Reader = reader
// Verify it works
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "hello world", string(data))
})
t.Run("converts bytes.Reader to io.Reader", func(t *testing.T) {
bytesReader := bytes.NewReader([]byte("test data"))
reader := ToReader(bytesReader)
var _ io.Reader = reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "test data", string(data))
})
t.Run("converts strings.Reader to io.Reader", func(t *testing.T) {
strReader := strings.NewReader("string content")
reader := ToReader(strReader)
var _ io.Reader = reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "string content", string(data))
})
t.Run("preserves reader functionality", func(t *testing.T) {
original := bytes.NewBuffer([]byte("test"))
reader := ToReader(original)
// Read once
buf1 := make([]byte, 2)
n, err := reader.Read(buf1)
assert.NoError(t, err)
assert.Equal(t, 2, n)
assert.Equal(t, "te", string(buf1))
// Read again
buf2 := make([]byte, 2)
n, err = reader.Read(buf2)
assert.NoError(t, err)
assert.Equal(t, 2, n)
assert.Equal(t, "st", string(buf2))
})
t.Run("handles empty reader", func(t *testing.T) {
buf := bytes.NewBuffer([]byte{})
reader := ToReader(buf)
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "", string(data))
})
}
func TestToWriter(t *testing.T) {
t.Run("converts bytes.Buffer to io.Writer", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
// Verify it's an io.Writer
var _ io.Writer = writer
// Verify it works
n, err := writer.Write([]byte("hello"))
assert.NoError(t, err)
assert.Equal(t, 5, n)
assert.Equal(t, "hello", buf.String())
})
t.Run("preserves writer functionality", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
// Write multiple times
writer.Write([]byte("hello "))
writer.Write([]byte("world"))
assert.Equal(t, "hello world", buf.String())
})
t.Run("handles empty writes", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
n, err := writer.Write([]byte{})
assert.NoError(t, err)
assert.Equal(t, 0, n)
assert.Equal(t, "", buf.String())
})
t.Run("handles large writes", func(t *testing.T) {
buf := &bytes.Buffer{}
writer := ToWriter(buf)
data := make([]byte, 10000)
for i := range data {
data[i] = byte('A' + (i % 26))
}
n, err := writer.Write(data)
assert.NoError(t, err)
assert.Equal(t, 10000, n)
assert.Equal(t, 10000, buf.Len())
})
}
func TestToCloser(t *testing.T) {
t.Run("converts file to io.Closer", func(t *testing.T) {
// Create a temporary file
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
closer := ToCloser(tmpfile)
// Verify it's an io.Closer
var _ io.Closer = closer
// Verify it works
err = closer.Close()
assert.NoError(t, err)
})
t.Run("converts nopCloser to io.Closer", func(t *testing.T) {
// Use io.NopCloser which is a standard implementation
reader := strings.NewReader("test")
nopCloser := io.NopCloser(reader)
closer := ToCloser(nopCloser)
var _ io.Closer = closer
err := closer.Close()
assert.NoError(t, err)
})
t.Run("preserves close functionality", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
closer := ToCloser(tmpfile)
// Close should work
err = closer.Close()
assert.NoError(t, err)
// Subsequent operations should fail
_, err = tmpfile.Write([]byte("test"))
assert.Error(t, err)
})
}
// Test type conversions work together
func TestIntegration(t *testing.T) {
t.Run("reader and closer together", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// Write some data
tmpfile.Write([]byte("test content"))
tmpfile.Seek(0, 0)
// Convert to interfaces
reader := ToReader(tmpfile)
closer := ToCloser(tmpfile)
// Use as reader
data, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Equal(t, "test content", string(data))
// Close
err = closer.Close()
assert.NoError(t, err)
})
t.Run("writer and closer together", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// Convert to interfaces
writer := ToWriter(tmpfile)
closer := ToCloser(tmpfile)
// Use as writer
n, err := writer.Write([]byte("test data"))
assert.NoError(t, err)
assert.Equal(t, 9, n)
// Close
err = closer.Close()
assert.NoError(t, err)
// Verify data was written
data, err := os.ReadFile(tmpfile.Name())
assert.NoError(t, err)
assert.Equal(t, "test data", string(data))
})
t.Run("all conversions with file", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "test")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
// File implements Reader, Writer, and Closer
var reader io.Reader = ToReader(tmpfile)
var writer io.Writer = ToWriter(tmpfile)
var closer io.Closer = ToCloser(tmpfile)
// All should be non-nil
assert.NotNil(t, reader)
assert.NotNil(t, writer)
assert.NotNil(t, closer)
// Write, read, close
writer.Write([]byte("hello"))
tmpfile.Seek(0, 0)
data, _ := io.ReadAll(reader)
assert.Equal(t, "hello", string(data))
closer.Close()
})
}
// Benchmark tests
func BenchmarkJoin(b *testing.B) {
joiner := Join("config.json")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = joiner("/etc/myapp")
}
}
func BenchmarkToReader(b *testing.B) {
buf := bytes.NewBuffer([]byte("test data"))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToReader(buf)
}
}
func BenchmarkToWriter(b *testing.B) {
buf := &bytes.Buffer{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToWriter(buf)
}
}
func BenchmarkToCloser(b *testing.B) {
tmpfile, _ := os.CreateTemp("", "bench")
defer os.Remove(tmpfile.Name())
defer tmpfile.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ToCloser(tmpfile)
}
}

45
v2/file/types.go Normal file
View File

@@ -0,0 +1,45 @@
// 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 file
import "github.com/IBM/fp-go/v2/endomorphism"
type (
// Endomorphism represents a function from a type to itself: A -> A.
// This is a type alias for endomorphism.Endomorphism[A].
//
// In the context of the file package, this is used for functions that
// transform strings (paths) into strings (paths), such as the Join function.
//
// An endomorphism has useful algebraic properties:
// - Identity: There exists an identity endomorphism (the identity function)
// - Composition: Endomorphisms can be composed to form new endomorphisms
// - Associativity: Composition is associative
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Join returns an Endomorphism[string]
// addConfig := file.Join("config.json") // Endomorphism[string]
// addLogs := file.Join("logs") // Endomorphism[string]
//
// // Compose endomorphisms
// addConfigLogs := F.Flow2(addLogs, addConfig)
// result := addConfigLogs("/var")
// // result is "/var/logs/config.json"
Endomorphism[A any] = endomorphism.Endomorphism[A]
)

492
v2/function/bind_test.go Normal file
View File

@@ -0,0 +1,492 @@
// 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 function
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// TestBind1st tests the Bind1st function with various scenarios
func TestBind1st(t *testing.T) {
t.Run("binds first parameter of multiplication", func(t *testing.T) {
multiply := func(a, b int) int { return a * b }
double := Bind1st(multiply, 2)
triple := Bind1st(multiply, 3)
assert.Equal(t, 10, double(5))
assert.Equal(t, 20, double(10))
assert.Equal(t, 15, triple(5))
assert.Equal(t, 30, triple(10))
})
t.Run("binds first parameter of division", func(t *testing.T) {
divide := func(a, b float64) float64 { return a / b }
divideBy10 := Bind1st(divide, 10.0)
divideBy5 := Bind1st(divide, 5.0)
assert.Equal(t, 5.0, divideBy10(2.0))
assert.Equal(t, 2.0, divideBy10(5.0))
assert.Equal(t, 1.0, divideBy5(5.0))
})
t.Run("binds first parameter of subtraction", func(t *testing.T) {
subtract := func(a, b int) int { return a - b }
subtract10From := Bind1st(subtract, 10)
assert.Equal(t, 7, subtract10From(3)) // 10 - 3
assert.Equal(t, 0, subtract10From(10)) // 10 - 10
assert.Equal(t, -5, subtract10From(15)) // 10 - 15
})
t.Run("binds first parameter of string concatenation", func(t *testing.T) {
concat := func(a, b string) string { return a + b }
addHello := Bind1st(concat, "Hello ")
addPrefix := Bind1st(concat, "Prefix: ")
assert.Equal(t, "Hello World", addHello("World"))
assert.Equal(t, "Hello Go", addHello("Go"))
assert.Equal(t, "Prefix: Test", addPrefix("Test"))
})
t.Run("binds first parameter with different types", func(t *testing.T) {
repeat := func(s string, n int) string {
return strings.Repeat(s, n)
}
repeatX := Bind1st(repeat, "x")
repeatAB := Bind1st(repeat, "ab")
assert.Equal(t, "xxx", repeatX(3))
assert.Equal(t, "xxxxx", repeatX(5))
assert.Equal(t, "abab", repeatAB(2))
})
t.Run("binds first parameter with complex types", func(t *testing.T) {
type Person struct {
Name string
Age int
}
format := func(p Person, suffix string) string {
return fmt.Sprintf("%s (%d) %s", p.Name, p.Age, suffix)
}
alice := Person{Name: "Alice", Age: 30}
formatAlice := Bind1st(format, alice)
assert.Equal(t, "Alice (30) is here", formatAlice("is here"))
assert.Equal(t, "Alice (30) says hello", formatAlice("says hello"))
})
t.Run("binds first parameter with slice operations", func(t *testing.T) {
appendSlice := func(slice []int, elem int) []int {
return append(slice, elem)
}
nums := []int{1, 2, 3}
appendToNums := Bind1st(appendSlice, nums)
result1 := appendToNums(4)
assert.Equal(t, []int{1, 2, 3, 4}, result1)
result2 := appendToNums(5)
assert.Equal(t, []int{1, 2, 3, 5}, result2)
})
t.Run("binds first parameter with map operations", func(t *testing.T) {
getFromMap := func(m map[string]int, key string) int {
return m[key]
}
data := map[string]int{"a": 1, "b": 2, "c": 3}
getFromData := Bind1st(getFromMap, data)
assert.Equal(t, 1, getFromData("a"))
assert.Equal(t, 2, getFromData("b"))
assert.Equal(t, 3, getFromData("c"))
})
t.Run("creates specialized comparison functions", func(t *testing.T) {
greaterThan := func(a, b int) bool { return a > b }
greaterThan10 := Bind1st(greaterThan, 10)
greaterThan5 := Bind1st(greaterThan, 5)
assert.True(t, greaterThan10(3)) // 10 > 3
assert.False(t, greaterThan10(15)) // 10 > 15
assert.True(t, greaterThan5(3)) // 5 > 3
assert.False(t, greaterThan5(10)) // 5 > 10
})
}
// TestBind2nd tests the Bind2nd function with various scenarios
func TestBind2nd(t *testing.T) {
t.Run("binds second parameter of multiplication", func(t *testing.T) {
multiply := func(a, b int) int { return a * b }
double := Bind2nd(multiply, 2)
triple := Bind2nd(multiply, 3)
assert.Equal(t, 10, double(5))
assert.Equal(t, 20, double(10))
assert.Equal(t, 15, triple(5))
assert.Equal(t, 30, triple(10))
})
t.Run("binds second parameter of division", func(t *testing.T) {
divide := func(a, b float64) float64 { return a / b }
halve := Bind2nd(divide, 2.0)
third := Bind2nd(divide, 3.0)
assert.Equal(t, 5.0, halve(10.0))
assert.Equal(t, 2.5, halve(5.0))
assert.InDelta(t, 3.333, third(10.0), 0.001)
})
t.Run("binds second parameter of subtraction", func(t *testing.T) {
subtract := func(a, b int) int { return a - b }
decrementBy5 := Bind2nd(subtract, 5)
decrementBy10 := Bind2nd(subtract, 10)
assert.Equal(t, 5, decrementBy5(10)) // 10 - 5
assert.Equal(t, 0, decrementBy5(5)) // 5 - 5
assert.Equal(t, 0, decrementBy10(10)) // 10 - 10
assert.Equal(t, -5, decrementBy10(5)) // 5 - 10
})
t.Run("binds second parameter of string concatenation", func(t *testing.T) {
concat := func(a, b string) string { return a + b }
addWorld := Bind2nd(concat, " World")
addSuffix := Bind2nd(concat, "!")
assert.Equal(t, "Hello World", addWorld("Hello"))
assert.Equal(t, "Goodbye World", addWorld("Goodbye"))
assert.Equal(t, "Hello!", addSuffix("Hello"))
})
t.Run("binds second parameter with different types", func(t *testing.T) {
repeat := func(s string, n int) string {
return strings.Repeat(s, n)
}
repeatThrice := Bind2nd(repeat, 3)
repeatTwice := Bind2nd(repeat, 2)
assert.Equal(t, "xxx", repeatThrice("x"))
assert.Equal(t, "ababab", repeatThrice("ab"))
assert.Equal(t, "aa", repeatTwice("a"))
})
t.Run("binds second parameter with complex types", func(t *testing.T) {
type Config struct {
Debug bool
Port int
}
format := func(name string, cfg Config) string {
return fmt.Sprintf("%s: debug=%v, port=%d", name, cfg.Debug, cfg.Port)
}
prodConfig := Config{Debug: false, Port: 8080}
formatWithProd := Bind2nd(format, prodConfig)
assert.Equal(t, "API: debug=false, port=8080", formatWithProd("API"))
assert.Equal(t, "Web: debug=false, port=8080", formatWithProd("Web"))
})
t.Run("binds second parameter with slice operations", func(t *testing.T) {
appendElem := func(slice []int, elem int) []int {
return append(slice, elem)
}
append5 := Bind2nd(appendElem, 5)
result1 := append5([]int{1, 2, 3})
assert.Equal(t, []int{1, 2, 3, 5}, result1)
result2 := append5([]int{10, 20})
assert.Equal(t, []int{10, 20, 5}, result2)
})
t.Run("creates specialized comparison functions", func(t *testing.T) {
greaterThan := func(a, b int) bool { return a > b }
greaterThan10 := Bind2nd(greaterThan, 10)
greaterThan5 := Bind2nd(greaterThan, 5)
assert.False(t, greaterThan10(3)) // 3 > 10
assert.True(t, greaterThan10(15)) // 15 > 10
assert.False(t, greaterThan5(3)) // 3 > 5
assert.True(t, greaterThan5(10)) // 10 > 5
})
t.Run("binds second parameter for power function", func(t *testing.T) {
power := func(base, exp float64) float64 {
result := 1.0
for i := 0; i < int(exp); i++ {
result *= base
}
return result
}
square := Bind2nd(power, 2.0)
cube := Bind2nd(power, 3.0)
assert.Equal(t, 25.0, square(5.0))
assert.Equal(t, 100.0, square(10.0))
assert.Equal(t, 125.0, cube(5.0))
assert.Equal(t, 8.0, cube(2.0))
})
}
// TestBind1stVsBind2nd tests the difference between Bind1st and Bind2nd
func TestBind1stVsBind2nd(t *testing.T) {
t.Run("demonstrates difference with non-commutative operations", func(t *testing.T) {
subtract := func(a, b int) int { return a - b }
// Bind1st: fixes first parameter (a)
subtract10From := Bind1st(subtract, 10) // 10 - b
assert.Equal(t, 7, subtract10From(3)) // 10 - 3 = 7
// Bind2nd: fixes second parameter (b)
decrementBy10 := Bind2nd(subtract, 10) // a - 10
assert.Equal(t, -7, decrementBy10(3)) // 3 - 10 = -7
})
t.Run("demonstrates difference with division", func(t *testing.T) {
divide := func(a, b float64) float64 { return a / b }
// Bind1st: fixes numerator
divide10By := Bind1st(divide, 10.0) // 10 / b
assert.Equal(t, 5.0, divide10By(2.0)) // 10 / 2 = 5
// Bind2nd: fixes denominator
divideBy10 := Bind2nd(divide, 10.0) // a / 10
assert.Equal(t, 0.2, divideBy10(2.0)) // 2 / 10 = 0.2
})
t.Run("demonstrates equivalence with commutative operations", func(t *testing.T) {
add := func(a, b int) int { return a + b }
// For commutative operations, both should give same result
add5First := Bind1st(add, 5) // 5 + b
add5Second := Bind2nd(add, 5) // a + 5
assert.Equal(t, 8, add5First(3))
assert.Equal(t, 8, add5Second(3))
assert.Equal(t, add5First(10), add5Second(10))
})
}
// TestSK tests the SK combinator function
func TestSK(t *testing.T) {
t.Run("returns second argument ignoring first", func(t *testing.T) {
assert.Equal(t, "hello", SK(42, "hello"))
assert.Equal(t, 100, SK(true, 100))
assert.Equal(t, 3.14, SK("test", 3.14))
assert.Equal(t, false, SK(123, false))
})
t.Run("works with nil values", func(t *testing.T) {
var nilPtr *int
assert.Nil(t, SK("ignored", nilPtr))
assert.Equal(t, 42, SK(nilPtr, 42))
})
t.Run("works with complex types", func(t *testing.T) {
type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 30}
bob := Person{Name: "Bob", Age: 25}
result := SK(alice, bob)
assert.Equal(t, "Bob", result.Name)
assert.Equal(t, 25, result.Age)
})
t.Run("works with slices", func(t *testing.T) {
slice1 := []int{1, 2, 3}
slice2 := []string{"a", "b", "c"}
result := SK(slice1, slice2)
assert.Equal(t, []string{"a", "b", "c"}, result)
})
t.Run("works with maps", func(t *testing.T) {
map1 := map[string]int{"a": 1}
map2 := map[int]string{1: "one"}
result := SK(map1, map2)
assert.Equal(t, map[int]string{1: "one"}, result)
})
t.Run("behaves identically to Second", func(t *testing.T) {
// SK should be identical to Second function
testCases := []struct {
first any
second any
}{
{42, "hello"},
{true, 100},
{"test", 3.14},
{[]int{1, 2}, []string{"a", "b"}},
}
for _, tc := range testCases {
assert.Equal(t,
Second(tc.first, tc.second),
SK(tc.first, tc.second),
"SK should behave like Second")
}
})
t.Run("demonstrates K combinator property", func(t *testing.T) {
// SK is the K combinator applied to the second argument
// K x y = x, so SK x y = K y x = y
// This means SK always returns its second argument
// Test with various types
assert.Equal(t, 42, SK("anything", 42))
assert.Equal(t, "result", SK(999, "result"))
assert.True(t, SK(false, true))
})
}
// TestBindComposition tests composition of bind operations
func TestBindComposition(t *testing.T) {
t.Run("composes multiple Bind1st operations", func(t *testing.T) {
add := func(a, b int) int { return a + b }
multiply := func(a, b int) int { return a * b }
add5 := Bind1st(add, 5)
double := Bind1st(multiply, 2)
// Compose: first add 5, then double
result := double(add5(3)) // (3 + 5) * 2 = 16
assert.Equal(t, 16, result)
})
t.Run("composes Bind1st and Bind2nd", func(t *testing.T) {
subtract := func(a, b int) int { return a - b }
subtract10From := Bind1st(subtract, 10) // 10 - b
decrementBy5 := Bind2nd(subtract, 5) // a - 5
// Apply both transformations
result1 := decrementBy5(subtract10From(3)) // (10 - 3) - 5 = 2
assert.Equal(t, 2, result1)
result2 := subtract10From(decrementBy5(8)) // 10 - (8 - 5) = 7
assert.Equal(t, 7, result2)
})
t.Run("creates pipeline with bound functions", func(t *testing.T) {
multiply := func(a, b int) int { return a * b }
add := func(a, b int) int { return a + b }
double := Bind2nd(multiply, 2)
add10 := Bind2nd(add, 10)
// Pipeline: input -> double -> add10
pipeline := func(n int) int {
return add10(double(n))
}
assert.Equal(t, 20, pipeline(5)) // (5 * 2) + 10 = 20
assert.Equal(t, 30, pipeline(10)) // (10 * 2) + 10 = 30
})
}
// TestBindWithHigherOrderFunctions tests bind with higher-order functions
func TestBindWithHigherOrderFunctions(t *testing.T) {
t.Run("binds function parameter", func(t *testing.T) {
applyTwice := func(f func(int) int, n int) int {
return f(f(n))
}
increment := func(n int) int { return n + 1 }
applyIncrementTwice := Bind1st(applyTwice, increment)
assert.Equal(t, 7, applyIncrementTwice(5)) // increment(increment(5)) = 7
})
t.Run("binds value for higher-order function", func(t *testing.T) {
applyFunc := func(f func(int) int, n int) int {
return f(n)
}
applyTo10 := Bind2nd(applyFunc, 10)
double := func(n int) int { return n * 2 }
square := func(n int) int { return n * n }
assert.Equal(t, 20, applyTo10(double)) // double(10) = 20
assert.Equal(t, 100, applyTo10(square)) // square(10) = 100
})
}
// BenchmarkBind1st benchmarks the Bind1st function
func BenchmarkBind1st(b *testing.B) {
multiply := func(a, b int) int { return a * b }
double := Bind1st(multiply, 2)
b.Run("direct call", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = multiply(2, i)
}
})
b.Run("bound function", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = double(i)
}
})
}
// BenchmarkBind2nd benchmarks the Bind2nd function
func BenchmarkBind2nd(b *testing.B) {
multiply := func(a, b int) int { return a * b }
double := Bind2nd(multiply, 2)
b.Run("direct call", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = multiply(i, 2)
}
})
b.Run("bound function", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = double(i)
}
})
}
// BenchmarkSK benchmarks the SK combinator
func BenchmarkSK(b *testing.B) {
b.Run("SK with ints", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = SK(i, i+1)
}
})
b.Run("Second with ints", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Second(i, i+1)
}
})
}

View File

@@ -19,23 +19,265 @@ import (
G "github.com/IBM/fp-go/v2/function/generic"
)
// Memoize converts a unary function into a unary function that caches the value depending on the parameter
// Memoize converts a unary function into a memoized version that caches computed values.
//
// Behavior:
// - On first call with a given input, the function executes and the result is cached
// - Subsequent calls with the same input return the cached result without re-execution
// - The cache uses the input parameter directly as the key (must be comparable)
// - The cache is thread-safe using mutex locks
// - The cache has no size limit and grows unbounded
// - Each unique input creates a new cache entry that persists for the lifetime of the memoized function
//
// Implementation Details:
// - Uses an internal map[K]func()T to store lazy values
// - The cached value is wrapped in a lazy function to defer computation until needed
// - Lock is held only to access the cache map, not during value computation
// - This allows concurrent computations for different keys
//
// Type Parameters:
// - K: The type of the function parameter, must be comparable (used as cache key)
// - T: The return type of the function
//
// Parameters:
// - f: The function to memoize
//
// Returns:
// - A memoized version of the function that caches results by parameter value
//
// Example:
//
// // Expensive computation
// expensiveCalc := func(n int) int {
// time.Sleep(100 * time.Millisecond)
// return n * n
// }
//
// // Memoize to avoid redundant calculations
// memoized := Memoize(expensiveCalc)
// result1 := memoized(5) // Takes 100ms, computes and caches 25
// result2 := memoized(5) // Instant, returns cached 25
// result3 := memoized(10) // Takes 100ms, computes and caches 100
//
// Note: The cache grows unbounded. For bounded caches, use CacheCallback with a custom cache implementation.
func Memoize[K comparable, T any](f func(K) T) func(K) T {
return G.Memoize(f)
}
// ContramapMemoize converts a unary function into a unary function that caches the value depending on the parameter
// ContramapMemoize creates a higher-order function that memoizes functions using a custom key extraction strategy.
//
// Behavior:
// - Allows caching based on a derived key rather than the full input parameter
// - The key extraction function (kf) determines what constitutes a cache hit
// - Two inputs that produce the same key will share the same cached result
// - This enables caching for non-comparable types by extracting comparable keys
// - The cache is thread-safe and unbounded
//
// Use Cases:
// - Cache by a subset of struct fields (e.g., User.ID instead of entire User)
// - Cache by a computed property (e.g., string length, hash value)
// - Normalize inputs before caching (e.g., lowercase strings, rounded numbers)
//
// Implementation Details:
// - Internally uses the same caching mechanism as Memoize
// - The key function is applied to each input before cache lookup
// - Returns a function transformer that can be applied to any function with matching signature
//
// Type Parameters:
// - T: The return type of the function to be memoized
// - A: The input type of the function to be memoized
// - K: The type of the cache key, must be comparable
//
// Parameters:
// - kf: A function that extracts a cache key from the input parameter
//
// Returns:
// - A function that takes a function (A) -> T and returns its memoized version
//
// Example:
//
// type User struct {
// ID int
// Name string
// Email string
// }
//
// // Cache by user ID only, ignoring other fields
// cacheByID := ContramapMemoize[string, User, int](func(u User) int {
// return u.ID
// })
//
// getUserData := func(u User) string {
// // Expensive database lookup
// return fmt.Sprintf("Data for user %d", u.ID)
// }
//
// memoized := cacheByID(getUserData)
// result1 := memoized(User{ID: 1, Name: "Alice", Email: "a@example.com"}) // Computed
// result2 := memoized(User{ID: 1, Name: "Bob", Email: "b@example.com"}) // Cached (same ID)
// result3 := memoized(User{ID: 2, Name: "Alice", Email: "a@example.com"}) // Computed (different ID)
func ContramapMemoize[T, A any, K comparable](kf func(A) K) func(func(A) T) func(A) T {
return G.ContramapMemoize[func(A) T](kf)
}
// CacheCallback converts a unary function into a unary function that caches the value depending on the parameter
// CacheCallback creates a higher-order function that memoizes functions using a custom cache implementation.
//
// Behavior:
// - Provides complete control over caching strategy through the getOrCreate callback
// - Separates cache key extraction (kf) from cache storage (getOrCreate)
// - The getOrCreate function receives a key and a lazy value generator
// - The cache implementation decides when to store, evict, or retrieve values
// - Enables advanced caching strategies: LRU, LFU, TTL, bounded size, etc.
//
// How It Works:
// 1. When the memoized function is called with input A:
// 2. The key function (kf) extracts a cache key K from A
// 3. A lazy value generator is created that will compute f(A) when called
// 4. The getOrCreate callback is invoked with the key and lazy generator
// 5. The cache implementation returns a lazy value (either cached or newly created)
// 6. The lazy value is evaluated to produce the final result T
//
// Cache Implementation Contract:
// - getOrCreate receives: (key K, generator func() func() T)
// - getOrCreate returns: func() T (a lazy value)
// - The generator creates a new lazy value when called
// - The cache should store and return lazy values, not final results
// - This allows deferred computation and proper lazy evaluation
//
// Type Parameters:
// - T: The return type of the function to be memoized
// - A: The input type of the function to be memoized
// - K: The type of the cache key, must be comparable
//
// Parameters:
// - kf: A function that extracts a cache key from the input parameter
// - getOrCreate: A cache implementation that stores and retrieves lazy values
//
// Returns:
// - A function that takes a function (A) -> T and returns its memoized version
//
// Example:
//
// // Create a bounded LRU cache (max 100 items)
// lruCache := func() func(int, func() func() string) func() string {
// cache := make(map[int]func() string)
// keys := []int{}
// var mu sync.Mutex
// maxSize := 100
//
// return func(k int, gen func() func() string) func() string {
// mu.Lock()
// defer mu.Unlock()
//
// if existing, ok := cache[k]; ok {
// return existing // Cache hit
// }
//
// // Evict oldest if at capacity
// if len(keys) >= maxSize {
// delete(cache, keys[0])
// keys = keys[1:]
// }
//
// // Create and store new lazy value
// value := gen()
// cache[k] = value
// keys = append(keys, k)
// return value
// }
// }
//
// // Use custom cache with memoization
// memoizer := CacheCallback[string, int, int](
// Identity[int], // Use input as key
// lruCache(),
// )
//
// expensiveFunc := func(n int) string {
// time.Sleep(100 * time.Millisecond)
// return fmt.Sprintf("Result: %d", n)
// }
//
// memoized := memoizer(expensiveFunc)
// result := memoized(42) // Computed and cached
// result = memoized(42) // Retrieved from cache
//
// See also: SingleElementCache for a simple bounded cache implementation.
func CacheCallback[
T, A any, K comparable](kf func(A) K, getOrCreate func(K, func() func() T) func() T) func(func(A) T) func(A) T {
return G.CacheCallback[func(func(A) T) func(A) T](kf, getOrCreate)
}
// SingleElementCache creates a cache function for use with the [CacheCallback] method that has a maximum capacity of one single item
// SingleElementCache creates a thread-safe cache implementation that stores at most one element.
//
// Behavior:
// - Stores only the most recently accessed key-value pair
// - When a new key is accessed, it replaces the previous cached entry
// - If the same key is accessed again, the cached value is returned
// - Thread-safe: uses mutex to protect concurrent access
// - Memory-efficient: constant O(1) space regardless of usage
//
// How It Works:
// 1. Initially, the cache is empty (hasKey = false)
// 2. On first access with key K1:
// - Calls the generator to create a lazy value
// - Stores K1 and the lazy value
// - Returns the lazy value
// 3. On subsequent access with same key K1:
// - Returns the stored lazy value without calling generator
// 4. On access with different key K2:
// - Calls the generator to create a new lazy value
// - Replaces K1 with K2 and updates the stored lazy value
// - Returns the new lazy value
// 5. If K1 is accessed again, it's treated as a new key (cache miss)
//
// Use Cases:
// - Sequential processing where the same key is accessed multiple times in a row
// - Memory-constrained environments where unbounded caches are not feasible
// - Scenarios where only the most recent computation needs caching
// - Testing or debugging with controlled cache behavior
//
// Important Notes:
// - The cache stores the lazy value (func() T), not the computed result
// - Each time the returned lazy value is called, it may recompute (depends on lazy implementation)
// - For true result caching, combine with lazy memoization (as done in CacheCallback)
// - Alternating between two keys will cause constant cache misses
//
// Type Parameters:
// - K: The type of the cache key, must be comparable
// - T: The type of the cached value
//
// Returns:
// - A cache function suitable for use with CacheCallback
//
// Example:
//
// // Create a single-element cache
// cache := SingleElementCache[int, string]()
//
// // Use with CacheCallback
// memoizer := CacheCallback[string, int, int](
// Identity[int], // Use input as key
// cache,
// )
//
// expensiveFunc := func(n int) string {
// time.Sleep(100 * time.Millisecond)
// return fmt.Sprintf("Result: %d", n)
// }
//
// memoized := memoizer(expensiveFunc)
// result1 := memoized(42) // Computed (100ms) and cached
// result2 := memoized(42) // Instant - returns cached value
// result3 := memoized(99) // Computed (100ms) - replaces cache entry for 42
// result4 := memoized(99) // Instant - returns cached value
// result5 := memoized(42) // Computed (100ms) - cache was replaced, must recompute
//
// Performance Characteristics:
// - Space: O(1) - stores exactly one key-value pair
// - Time: O(1) - cache lookup and update are constant time
// - Best case: Same key accessed repeatedly (100% hit rate)
// - Worst case: Alternating keys (0% hit rate)
func SingleElementCache[K comparable, T any]() func(K, func() func() T) func() T {
return G.SingleElementCache[func() func() T, K]()
}

View File

@@ -17,54 +17,601 @@ package function
import (
"fmt"
"math/rand"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCache(t *testing.T) {
var count int
// TestMemoize tests the Memoize function
func TestMemoize(t *testing.T) {
t.Run("caches computed values", func(t *testing.T) {
callCount := 0
expensive := func(n int) int {
callCount++
time.Sleep(10 * time.Millisecond)
return n * 2
}
withSideEffect := func(n int) int {
count++
return n
}
memoized := Memoize(expensive)
cached := Memoize(withSideEffect)
// First call should compute
result1 := memoized(5)
assert.Equal(t, 10, result1)
assert.Equal(t, 1, callCount)
assert.Equal(t, 0, count)
// Second call with same input should use cache
result2 := memoized(5)
assert.Equal(t, 10, result2)
assert.Equal(t, 1, callCount, "should not recompute for cached value")
assert.Equal(t, 10, cached(10))
assert.Equal(t, 1, count)
// Different input should compute again
result3 := memoized(10)
assert.Equal(t, 20, result3)
assert.Equal(t, 2, callCount)
assert.Equal(t, 10, cached(10))
assert.Equal(t, 1, count)
// Original input should still be cached
result4 := memoized(5)
assert.Equal(t, 10, result4)
assert.Equal(t, 2, callCount, "should still use cached value")
})
assert.Equal(t, 20, cached(20))
assert.Equal(t, 2, count)
t.Run("works with string keys", func(t *testing.T) {
callCount := 0
toUpper := func(s string) string {
callCount++
return fmt.Sprintf("UPPER_%s", s)
}
assert.Equal(t, 20, cached(20))
assert.Equal(t, 2, count)
memoized := Memoize(toUpper)
assert.Equal(t, 10, cached(10))
assert.Equal(t, 2, count)
result1 := memoized("hello")
assert.Equal(t, "UPPER_hello", result1)
assert.Equal(t, 1, callCount)
result2 := memoized("hello")
assert.Equal(t, "UPPER_hello", result2)
assert.Equal(t, 1, callCount)
result3 := memoized("world")
assert.Equal(t, "UPPER_world", result3)
assert.Equal(t, 2, callCount)
})
t.Run("is thread-safe", func(t *testing.T) {
var callCount int32
expensive := func(n int) int {
atomic.AddInt32(&callCount, 1)
time.Sleep(5 * time.Millisecond)
return n * n
}
memoized := Memoize(expensive)
// Run concurrent calls with same input
var wg sync.WaitGroup
results := make([]int, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
results[idx] = memoized(7)
}(i)
}
wg.Wait()
// All results should be the same
for _, result := range results {
assert.Equal(t, 49, result)
}
// Function should be called at least once, but possibly more due to race
// (the cache is eventually consistent)
assert.Greater(t, atomic.LoadInt32(&callCount), int32(0))
})
t.Run("handles zero values correctly", func(t *testing.T) {
callCount := 0
identity := func(n int) int {
callCount++
return n
}
memoized := Memoize(identity)
result1 := memoized(0)
assert.Equal(t, 0, result1)
assert.Equal(t, 1, callCount)
result2 := memoized(0)
assert.Equal(t, 0, result2)
assert.Equal(t, 1, callCount, "should cache zero value")
})
t.Run("caches multiple different values", func(t *testing.T) {
callCount := 0
square := func(n int) int {
callCount++
return n * n
}
memoized := Memoize(square)
// Cache multiple values
assert.Equal(t, 4, memoized(2))
assert.Equal(t, 9, memoized(3))
assert.Equal(t, 16, memoized(4))
assert.Equal(t, 3, callCount)
// All should be cached
assert.Equal(t, 4, memoized(2))
assert.Equal(t, 9, memoized(3))
assert.Equal(t, 16, memoized(4))
assert.Equal(t, 3, callCount, "all values should be cached")
})
}
// TestContramapMemoize tests the ContramapMemoize function
func TestContramapMemoize(t *testing.T) {
type User struct {
ID int
Name string
Age int
}
t.Run("caches by extracted key", func(t *testing.T) {
callCount := 0
getUserData := func(u User) string {
callCount++
return fmt.Sprintf("Data for user %d: %s", u.ID, u.Name)
}
// Cache by ID only
cacheByID := ContramapMemoize[string, User, int](func(u User) int {
return u.ID
})
memoized := cacheByID(getUserData)
user1 := User{ID: 1, Name: "Alice", Age: 30}
result1 := memoized(user1)
assert.Equal(t, "Data for user 1: Alice", result1)
assert.Equal(t, 1, callCount)
// Same ID, different name - should use cache
user2 := User{ID: 1, Name: "Bob", Age: 25}
result2 := memoized(user2)
assert.Equal(t, "Data for user 1: Alice", result2, "should return cached result")
assert.Equal(t, 1, callCount, "should not recompute")
// Different ID - should compute
user3 := User{ID: 2, Name: "Charlie", Age: 35}
result3 := memoized(user3)
assert.Equal(t, "Data for user 2: Charlie", result3)
assert.Equal(t, 2, callCount)
})
t.Run("works with string key extraction", func(t *testing.T) {
type Product struct {
SKU string
Name string
Price float64
}
callCount := 0
getPrice := func(p Product) float64 {
callCount++
return p.Price * 1.1 // Add 10% markup
}
cacheBySKU := ContramapMemoize[float64, Product, string](func(p Product) string {
return p.SKU
})
memoized := cacheBySKU(getPrice)
prod1 := Product{SKU: "ABC123", Name: "Widget", Price: 100.0}
result1 := memoized(prod1)
assert.InDelta(t, 110.0, result1, 0.01)
assert.Equal(t, 1, callCount)
// Same SKU, different price - should use cached result
prod2 := Product{SKU: "ABC123", Name: "Widget", Price: 200.0}
result2 := memoized(prod2)
assert.InDelta(t, 110.0, result2, 0.01, "should use cached value")
assert.Equal(t, 1, callCount)
})
t.Run("can use complex key extraction", func(t *testing.T) {
type Request struct {
Method string
Path string
Body string
}
callCount := 0
processRequest := func(r Request) string {
callCount++
return fmt.Sprintf("Processed: %s %s", r.Method, r.Path)
}
// Cache by method and path, ignore body
cacheByMethodPath := ContramapMemoize[string, Request, string](func(r Request) string {
return r.Method + ":" + r.Path
})
memoized := cacheByMethodPath(processRequest)
req1 := Request{Method: "GET", Path: "/api/users", Body: "body1"}
result1 := memoized(req1)
assert.Equal(t, "Processed: GET /api/users", result1)
assert.Equal(t, 1, callCount)
// Same method and path, different body - should use cache
req2 := Request{Method: "GET", Path: "/api/users", Body: "body2"}
result2 := memoized(req2)
assert.Equal(t, "Processed: GET /api/users", result2)
assert.Equal(t, 1, callCount)
// Different path - should compute
req3 := Request{Method: "GET", Path: "/api/posts", Body: "body1"}
result3 := memoized(req3)
assert.Equal(t, "Processed: GET /api/posts", result3)
assert.Equal(t, 2, callCount)
})
}
// TestCacheCallback tests the CacheCallback function
func TestCacheCallback(t *testing.T) {
t.Run("works with custom cache implementation", func(t *testing.T) {
// Create a simple bounded cache (max 2 items)
boundedCache := func() func(int, func() func() string) func() string {
cache := make(map[int]func() string)
keys := []int{}
var mu sync.Mutex
return func(k int, gen func() func() string) func() string {
mu.Lock()
defer mu.Unlock()
if existing, ok := cache[k]; ok {
return existing
}
// Evict oldest if at capacity
if len(keys) >= 2 {
oldestKey := keys[0]
delete(cache, oldestKey)
keys = keys[1:]
}
value := gen()
cache[k] = value
keys = append(keys, k)
return value
}
}
callCount := 0
expensive := func(n int) string {
callCount++
return fmt.Sprintf("Result: %d", n)
}
memoizer := CacheCallback[string, int, int](
Identity[int],
boundedCache(),
)
memoized := memoizer(expensive)
// Cache first two values
result1 := memoized(1)
assert.Equal(t, "Result: 1", result1)
assert.Equal(t, 1, callCount)
result2 := memoized(2)
assert.Equal(t, "Result: 2", result2)
assert.Equal(t, 2, callCount)
// Both should be cached
memoized(1)
memoized(2)
assert.Equal(t, 2, callCount)
// Third value should evict first
result3 := memoized(3)
assert.Equal(t, "Result: 3", result3)
assert.Equal(t, 3, callCount)
// First value should be recomputed (evicted)
// Note: The cache stores lazy generators, so calling memoized(1) again
// will create a new cache entry with a new lazy generator
memoized(1)
// The call count increases because a new lazy value is created and evaluated
assert.GreaterOrEqual(t, callCount, 3, "first value should have been evicted")
// Verify cache still works for remaining values
prevCount := callCount
memoized(2)
memoized(3)
// These might or might not increase count depending on eviction
assert.GreaterOrEqual(t, callCount, prevCount)
})
t.Run("integrates with key extraction", func(t *testing.T) {
type Item struct {
ID int
Value string
}
// Simple cache
simpleCache := func() func(int, func() func() string) func() string {
cache := make(map[int]func() string)
var mu sync.Mutex
return func(k int, gen func() func() string) func() string {
mu.Lock()
defer mu.Unlock()
if existing, ok := cache[k]; ok {
return existing
}
value := gen()
cache[k] = value
return value
}
}
callCount := 0
process := func(item Item) string {
callCount++
return fmt.Sprintf("Processed: %s", item.Value)
}
memoizer := CacheCallback[string, Item, int](
func(item Item) int { return item.ID },
simpleCache(),
)
memoized := memoizer(process)
item1 := Item{ID: 1, Value: "first"}
result1 := memoized(item1)
assert.Equal(t, "Processed: first", result1)
assert.Equal(t, 1, callCount)
// Same ID, different value - should use cache
item2 := Item{ID: 1, Value: "second"}
result2 := memoized(item2)
assert.Equal(t, "Processed: first", result2)
assert.Equal(t, 1, callCount)
})
}
// TestSingleElementCache tests the SingleElementCache function
func TestSingleElementCache(t *testing.T) {
f := func(key string) string {
return fmt.Sprintf("%s: %d", key, rand.Int())
}
cb := CacheCallback(func(s string) string { return s }, SingleElementCache[string, string]())
cf := cb(f)
t.Run("caches single element", func(t *testing.T) {
cache := SingleElementCache[int, string]()
v1 := cf("1")
v2 := cf("1")
v3 := cf("2")
v4 := cf("1")
callCount := 0
gen := func(n int) func() func() string {
// This returns a generator that creates a lazy value
return func() func() string {
// This is the lazy value that gets cached
return func() string {
// This gets called when the lazy value is evaluated
callCount++
return fmt.Sprintf("Value: %d", n)
}
}
}
assert.Equal(t, v1, v2)
assert.NotEqual(t, v2, v3)
assert.NotEqual(t, v3, v4)
assert.NotEqual(t, v1, v4)
// First call - creates and caches lazy value for key 1
lazy1 := cache(1, gen(1))
result1 := lazy1()
assert.Equal(t, "Value: 1", result1)
assert.Equal(t, 1, callCount)
// Same key - returns the same cached lazy value
lazy1Again := cache(1, gen(1))
result2 := lazy1Again()
assert.Equal(t, "Value: 1", result2)
// The lazy value is called again, so count increases
assert.Equal(t, 2, callCount, "cached lazy value is called again")
// Different key - replaces cache with new lazy value
lazy2 := cache(2, gen(2))
result3 := lazy2()
assert.Equal(t, "Value: 2", result3)
assert.Equal(t, 3, callCount)
// Original key - cache was replaced, creates new lazy value
lazy1New := cache(1, gen(1))
result4 := lazy1New()
assert.Equal(t, "Value: 1", result4)
assert.Equal(t, 4, callCount, "new lazy value created after cache replacement")
})
t.Run("works with CacheCallback", func(t *testing.T) {
cache := SingleElementCache[int, string]()
callCount := 0
expensive := func(n int) string {
callCount++
return fmt.Sprintf("Result: %d", n*n)
}
memoizer := CacheCallback[string, int, int](
Identity[int],
cache,
)
memoized := memoizer(expensive)
// First computation
result1 := memoized(5)
assert.Equal(t, "Result: 25", result1)
assert.Equal(t, 1, callCount)
// Same input - cached
result2 := memoized(5)
assert.Equal(t, "Result: 25", result2)
assert.Equal(t, 1, callCount)
// Different input - replaces cache
result3 := memoized(10)
assert.Equal(t, "Result: 100", result3)
assert.Equal(t, 2, callCount)
// Back to first input - recomputed
result4 := memoized(5)
assert.Equal(t, "Result: 25", result4)
assert.Equal(t, 3, callCount)
})
t.Run("is thread-safe", func(t *testing.T) {
cache := SingleElementCache[int, string]()
var callCount int32
gen := func(n int) func() func() string {
return func() func() string {
return func() string {
atomic.AddInt32(&callCount, 1)
time.Sleep(5 * time.Millisecond)
return fmt.Sprintf("Value: %d", n)
}
}
}
var wg sync.WaitGroup
results := make([]string, 20)
// Concurrent access with same key
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
results[idx] = cache(1, gen(1))()
}(i)
}
// Concurrent access with different key
for i := 10; i < 20; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
results[idx] = cache(2, gen(2))()
}(i)
}
wg.Wait()
// All results should be valid (either "Value: 1" or "Value: 2")
for _, result := range results {
assert.True(t, result == "Value: 1" || result == "Value: 2")
}
// Function should have been called, but exact count depends on race conditions
assert.Greater(t, atomic.LoadInt32(&callCount), int32(0))
})
t.Run("handles rapid key changes", func(t *testing.T) {
cache := SingleElementCache[int, string]()
callCount := 0
gen := func(n int) func() func() string {
return func() func() string {
return func() string {
callCount++
return fmt.Sprintf("Value: %d", n)
}
}
}
// Rapidly alternate between keys
for i := 0; i < 10; i++ {
cache(1, gen(1))()
cache(2, gen(2))()
}
// Each key change should trigger a computation
// (20 calls total: 10 for key 1, 10 for key 2)
assert.Equal(t, 20, callCount)
})
}
// TestMemoizeIntegration tests integration scenarios
func TestMemoizeIntegration(t *testing.T) {
t.Run("fibonacci with memoization", func(t *testing.T) {
callCount := 0
expensive := func(n int) int {
callCount++
time.Sleep(10 * time.Millisecond)
return n * n
}
memoized := Memoize(expensive)
// First call computes
result1 := memoized(10)
assert.Equal(t, 100, result1)
assert.Equal(t, 1, callCount)
// Second call with same input uses cache
result2 := memoized(10)
assert.Equal(t, 100, result2)
assert.Equal(t, 1, callCount, "should use cached value")
// Different input computes again
result3 := memoized(5)
assert.Equal(t, 25, result3)
assert.Equal(t, 2, callCount)
// Both values should remain cached
assert.Equal(t, 100, memoized(10))
assert.Equal(t, 25, memoized(5))
assert.Equal(t, 2, callCount, "both values should be cached")
})
t.Run("chaining memoization strategies", func(t *testing.T) {
type Request struct {
UserID int
Action string
}
callCount := 0
processRequest := func(r Request) string {
callCount++
return fmt.Sprintf("User %d: %s", r.UserID, r.Action)
}
// First level: cache by UserID
cacheByUser := ContramapMemoize[string, Request, int](func(r Request) int {
return r.UserID
})
memoized := cacheByUser(processRequest)
req1 := Request{UserID: 1, Action: "login"}
result1 := memoized(req1)
assert.Equal(t, "User 1: login", result1)
assert.Equal(t, 1, callCount)
// Same user, different action - uses cache
req2 := Request{UserID: 1, Action: "logout"}
result2 := memoized(req2)
assert.Equal(t, "User 1: login", result2)
assert.Equal(t, 1, callCount)
// Different user - computes
req3 := Request{UserID: 2, Action: "login"}
result3 := memoized(req3)
assert.Equal(t, "User 2: login", result3)
assert.Equal(t, 2, callCount)
})
}

View File

@@ -194,79 +194,6 @@ func TestSecond(t *testing.T) {
})
}
// TestBind1st tests the Bind1st function
func TestBind1st(t *testing.T) {
t.Run("binds first parameter of multiplication", func(t *testing.T) {
multiply := func(a, b int) int { return a * b }
double := Bind1st(multiply, 2)
triple := Bind1st(multiply, 3)
assert.Equal(t, 10, double(5))
assert.Equal(t, 20, double(10))
assert.Equal(t, 15, triple(5))
})
t.Run("binds first parameter of division", func(t *testing.T) {
divide := func(a, b float64) float64 { return a / b }
divideBy10 := Bind1st(divide, 10.0)
assert.Equal(t, 5.0, divideBy10(2.0))
assert.Equal(t, 2.0, divideBy10(5.0))
})
t.Run("binds first parameter of string concatenation", func(t *testing.T) {
concat := func(a, b string) string { return a + b }
addHello := Bind1st(concat, "Hello ")
assert.Equal(t, "Hello World", addHello("World"))
assert.Equal(t, "Hello Go", addHello("Go"))
})
}
// TestBind2nd tests the Bind2nd function
func TestBind2nd(t *testing.T) {
t.Run("binds second parameter of multiplication", func(t *testing.T) {
multiply := func(a, b int) int { return a * b }
double := Bind2nd(multiply, 2)
triple := Bind2nd(multiply, 3)
assert.Equal(t, 10, double(5))
assert.Equal(t, 20, double(10))
assert.Equal(t, 15, triple(5))
})
t.Run("binds second parameter of division", func(t *testing.T) {
divide := func(a, b float64) float64 { return a / b }
halve := Bind2nd(divide, 2.0)
assert.Equal(t, 5.0, halve(10.0))
assert.Equal(t, 2.5, halve(5.0))
})
t.Run("binds second parameter of subtraction", func(t *testing.T) {
subtract := func(a, b int) int { return a - b }
decrementBy5 := Bind2nd(subtract, 5)
assert.Equal(t, 5, decrementBy5(10))
assert.Equal(t, 0, decrementBy5(5))
})
}
// TestSK tests the SK function
func TestSK(t *testing.T) {
t.Run("returns second argument ignoring first", func(t *testing.T) {
assert.Equal(t, "hello", SK(42, "hello"))
assert.Equal(t, 100, SK(true, 100))
assert.Equal(t, 3.14, SK("test", 3.14))
})
t.Run("behaves like Second", func(t *testing.T) {
// SK should be identical to Second
assert.Equal(t, Second(42, "hello"), SK(42, "hello"))
assert.Equal(t, Second(true, 100), SK(true, 100))
})
}
// TestTernary tests the Ternary function
func TestTernary(t *testing.T) {
t.Run("applies onTrue when predicate is true", func(t *testing.T) {

View File

@@ -13,11 +13,45 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package content provides constants for common HTTP Content-Type header values.
//
// These constants can be used when setting or checking Content-Type headers in HTTP
// requests and responses, ensuring consistency and avoiding typos in content type strings.
//
// Example usage:
//
// req.Header.Set("Content-Type", content.JSON)
// if contentType == content.TextPlain {
// // handle plain text
// }
package content
const (
TextPlain = "text/plain"
JSON = "application/json"
Json = JSON // Deprecated: use [JSON] instead
// TextPlain represents the "text/plain" content type for plain text data.
// This is commonly used for simple text responses or requests without any
// specific formatting or structure.
//
// Defined in RFC 2046, Section 4.1.3: https://www.rfc-editor.org/rfc/rfc2046.html#section-4.1.3
TextPlain = "text/plain"
// JSON represents the "application/json" content type for JSON-encoded data.
// This is the standard content type for JSON payloads in HTTP requests and responses.
//
// Defined in RFC 8259: https://www.rfc-editor.org/rfc/rfc8259.html
JSON = "application/json"
// Json is deprecated. Use [JSON] instead.
//
// Deprecated: Use JSON for consistency with Go naming conventions.
Json = JSON
// FormEncoded represents the "application/x-www-form-urlencoded" content type.
// This is used for HTML form submissions where form data is encoded as key-value
// pairs in the request body, with keys and values URL-encoded.
//
// Defined in HTML 4.01 Specification, Section 17.13.4:
// https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
// Also referenced in WHATWG HTML Living Standard:
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#application/x-www-form-urlencoded-encoding-algorithm
FormEncoded = "application/x-www-form-urlencoded"
)

View File

@@ -13,6 +13,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package form provides functional utilities for working with HTTP form data (url.Values).
//
// This package offers a functional approach to building and manipulating HTTP form data
// using lenses, endomorphisms, and monoids. It enables immutable transformations of
// url.Values through composable operations.
//
// # Core Concepts
//
// The package is built around several key abstractions:
// - Endomorphism: A function that transforms url.Values immutably
// - Lenses: Optics for focusing on specific form fields
// - Monoids: For combining form transformations and values
//
// # Basic Usage
//
// Create form data by composing endomorphisms:
//
// form := F.Pipe3(
// form.Default,
// form.WithValue("username")("john"),
// form.WithValue("email")("john@example.com"),
// form.WithValue("age")("30"),
// )
//
// Remove fields from forms:
//
// updated := F.Pipe1(
// form,
// form.WithoutValue("age"),
// )
//
// # Lenses
//
// The package provides two main lenses:
// - AtValues: Focuses on all values of a form field ([]string)
// - AtValue: Focuses on the first value of a form field (Option[string])
//
// Use lenses to read and update form fields:
//
// lens := form.AtValue("username")
// value := lens.Get(form) // Returns Option[string]
// updated := lens.Set(O.Some("jane"))(form)
//
// # Monoids
//
// Combine multiple form transformations:
//
// transform := form.Monoid.Concat(
// form.WithValue("field1")("value1"),
// form.WithValue("field2")("value2"),
// )
// result := transform(form.Default)
//
// Merge form values:
//
// merged := form.ValuesMonoid.Concat(form1, form2)
package form
import (
@@ -29,23 +85,61 @@ import (
)
type (
// Endomorphism returns an [ENDO.Endomorphism] that transforms a form
// Endomorphism is a function that transforms url.Values immutably.
// It represents a transformation from url.Values to url.Values,
// enabling functional composition of form modifications.
//
// Example:
// transform := form.WithValue("key")("value")
// result := transform(form.Default)
Endomorphism = ENDO.Endomorphism[url.Values]
)
var (
// Default is the default form field
// Default is an empty url.Values that serves as the starting point
// for building form data. Use this with Pipe operations to construct
// forms functionally.
//
// Example:
// form := F.Pipe2(
// form.Default,
// form.WithValue("key1")("value1"),
// form.WithValue("key2")("value2"),
// )
Default = make(url.Values)
noField = O.None[string]()
// Monoid is the [M.Monoid] for the [Endomorphism]
// Monoid is a Monoid for Endomorphism that allows combining multiple
// form transformations into a single transformation. The identity element
// is the identity function, and concatenation composes transformations.
//
// Example:
// transform := form.Monoid.Concat(
// form.WithValue("field1")("value1"),
// form.WithValue("field2")("value2"),
// )
// result := transform(form.Default)
Monoid = ENDO.Monoid[url.Values]()
// ValuesMonoid is a [M.Monoid] to concatenate [url.Values] maps
// ValuesMonoid is a Monoid for url.Values that concatenates form data.
// When two forms are combined, arrays of values for the same key are
// concatenated using the array Semigroup.
//
// Example:
// form1 := url.Values{"key": []string{"value1"}}
// form2 := url.Values{"key": []string{"value2"}}
// merged := form.ValuesMonoid.Concat(form1, form2)
// // Result: url.Values{"key": []string{"value1", "value2"}}
ValuesMonoid = RG.UnionMonoid[url.Values](A.Semigroup[string]())
// AtValues is a [L.Lens] that focusses on the values of a form field
// AtValues is a Lens that focuses on all values of a form field as a slice.
// It provides access to the complete []string array for a given field name.
//
// Example:
// lens := form.AtValues("tags")
// values := lens.Get(form) // Returns Option[[]string]
// updated := lens.Set(O.Some([]string{"tag1", "tag2"}))(form)
AtValues = LRG.AtRecord[url.Values, []string]
composeHead = F.Pipe1(
@@ -53,14 +147,39 @@ var (
LO.Compose[url.Values, string](A.Empty[string]()),
)
// AtValue is a [L.Lens] that focusses on first value in form fields
// AtValue is a Lens that focuses on the first value of a form field.
// It returns an Option[string] representing the first value if present,
// or None if the field doesn't exist or has no values.
//
// Example:
// lens := form.AtValue("username")
// value := lens.Get(form) // Returns Option[string]
// updated := lens.Set(O.Some("newuser"))(form)
AtValue = F.Flow2(
AtValues,
composeHead,
)
)
// WithValue creates a [FormBuilder] for a certain field
// WithValue creates an Endomorphism that sets a form field to a specific value.
// It returns a curried function that takes the field name first, then the value,
// and finally returns a transformation function.
//
// The transformation is immutable - it creates a new url.Values rather than
// modifying the input.
//
// Example:
//
// // Set a single field
// form := form.WithValue("username")("john")(form.Default)
//
// // Compose multiple fields
// form := F.Pipe3(
// form.Default,
// form.WithValue("username")("john"),
// form.WithValue("email")("john@example.com"),
// form.WithValue("age")("30"),
// )
func WithValue(name string) func(value string) Endomorphism {
return F.Flow2(
O.Of[string],
@@ -68,7 +187,21 @@ func WithValue(name string) func(value string) Endomorphism {
)
}
// WithoutValue creates a [FormBuilder] that removes a field
// WithoutValue creates an Endomorphism that removes a form field.
// The transformation is immutable - it creates a new url.Values rather than
// modifying the input.
//
// Example:
//
// // Remove a field
// updated := form.WithoutValue("age")(form)
//
// // Compose with other operations
// form := F.Pipe2(
// existingForm,
// form.WithValue("username")("john"),
// form.WithoutValue("password"),
// )
func WithoutValue(name string) Endomorphism {
return AtValue(name).Set(noField)
}

View File

@@ -16,6 +16,7 @@
package form
import (
"fmt"
"net/url"
"testing"
@@ -91,3 +92,448 @@ func TestFormField(t *testing.T) {
assert.Equal(t, O.Of("v1"), l1.Get(v2))
assert.Equal(t, O.Of("v2"), l2.Get(v2))
}
// TestWithValue tests the WithValue function
func TestWithValue(t *testing.T) {
t.Run("sets a single value", func(t *testing.T) {
form := WithValue("key")("value")(Default)
assert.Equal(t, "value", form.Get("key"))
})
t.Run("creates immutable transformation", func(t *testing.T) {
original := Default
modified := WithValue("key")("value")(original)
assert.False(t, valuesEq.Equals(original, modified))
assert.Equal(t, "", original.Get("key"))
assert.Equal(t, "value", modified.Get("key"))
})
t.Run("overwrites existing value", func(t *testing.T) {
form := WithValue("key")("value1")(Default)
updated := WithValue("key")("value2")(form)
assert.Equal(t, "value2", updated.Get("key"))
assert.Equal(t, "value1", form.Get("key"))
})
t.Run("composes multiple values", func(t *testing.T) {
form := F.Pipe3(
Default,
WithValue("key1")("value1"),
WithValue("key2")("value2"),
WithValue("key3")("value3"),
)
assert.Equal(t, "value1", form.Get("key1"))
assert.Equal(t, "value2", form.Get("key2"))
assert.Equal(t, "value3", form.Get("key3"))
})
t.Run("handles empty string values", func(t *testing.T) {
form := WithValue("key")("")(Default)
assert.Equal(t, "", form.Get("key"))
assert.True(t, form.Has("key"))
})
t.Run("handles special characters in keys", func(t *testing.T) {
form := F.Pipe2(
Default,
WithValue("key-with-dash")("value1"),
WithValue("key_with_underscore")("value2"),
)
assert.Equal(t, "value1", form.Get("key-with-dash"))
assert.Equal(t, "value2", form.Get("key_with_underscore"))
})
}
// TestWithoutValue tests the WithoutValue function
func TestWithoutValue(t *testing.T) {
t.Run("clears field value", func(t *testing.T) {
form := WithValue("key")("value")(Default)
updated := WithoutValue("key")(form)
// WithoutValue sets the field to an empty array, not removing it entirely
assert.Equal(t, "", updated.Get("key"))
// The field still exists but with empty values
values := updated["key"]
assert.Equal(t, 0, len(values))
})
t.Run("is idempotent", func(t *testing.T) {
form := WithValue("key")("value")(Default)
removed1 := WithoutValue("key")(form)
removed2 := WithoutValue("key")(removed1)
assert.True(t, valuesEq.Equals(removed1, removed2))
})
t.Run("does not affect other fields", func(t *testing.T) {
form := F.Pipe2(
Default,
WithValue("key1")("value1"),
WithValue("key2")("value2"),
)
updated := WithoutValue("key1")(form)
assert.Equal(t, "", updated.Get("key1"))
assert.Equal(t, "value2", updated.Get("key2"))
})
t.Run("creates immutable transformation", func(t *testing.T) {
form := WithValue("key")("value")(Default)
updated := WithoutValue("key")(form)
assert.False(t, valuesEq.Equals(form, updated))
assert.Equal(t, "value", form.Get("key"))
assert.Equal(t, "", updated.Get("key"))
})
t.Run("handles non-existent field", func(t *testing.T) {
form := Default
updated := WithoutValue("nonexistent")(form)
assert.True(t, valuesEq.Equals(form, updated))
})
}
// TestMonoid tests the Monoid for Endomorphism
func TestMonoid(t *testing.T) {
t.Run("identity element", func(t *testing.T) {
form := F.Pipe1(
Default,
WithValue("key")("value"),
)
// Concatenating with identity should not change the result
result := Monoid.Concat(Monoid.Empty(), WithValue("key")("value"))(Default)
assert.True(t, valuesEq.Equals(form, result))
})
t.Run("concatenates transformations", func(t *testing.T) {
transform := Monoid.Concat(
WithValue("key1")("value1"),
WithValue("key2")("value2"),
)
result := transform(Default)
assert.Equal(t, "value1", result.Get("key1"))
assert.Equal(t, "value2", result.Get("key2"))
})
t.Run("concatenates multiple transformations", func(t *testing.T) {
transform := Monoid.Concat(
WithValue("key1")("value1"),
Monoid.Concat(
WithValue("key2")("value2"),
WithValue("key3")("value3"),
),
)
result := transform(Default)
assert.Equal(t, "value1", result.Get("key1"))
assert.Equal(t, "value2", result.Get("key2"))
assert.Equal(t, "value3", result.Get("key3"))
})
t.Run("respects transformation order", func(t *testing.T) {
// Monoid concatenation composes functions left-to-right
// So the first transformation is applied first, then the second
transform := Monoid.Concat(
WithValue("key")("first"),
WithValue("key")("second"),
)
result := transform(Default)
// The transformations are composed, so first is applied, then second overwrites it
// But since Monoid.Concat composes endomorphisms, we need to check actual behavior
assert.Equal(t, "first", result.Get("key"))
})
}
// TestValuesMonoid tests the ValuesMonoid
func TestValuesMonoid(t *testing.T) {
t.Run("identity element", func(t *testing.T) {
form := url.Values{"key": []string{"value"}}
result := ValuesMonoid.Concat(ValuesMonoid.Empty(), form)
assert.True(t, valuesEq.Equals(form, result))
})
t.Run("concatenates disjoint forms", func(t *testing.T) {
form1 := url.Values{"key1": []string{"value1"}}
form2 := url.Values{"key2": []string{"value2"}}
result := ValuesMonoid.Concat(form1, form2)
assert.Equal(t, "value1", result.Get("key1"))
assert.Equal(t, "value2", result.Get("key2"))
})
t.Run("concatenates arrays for same key", func(t *testing.T) {
form1 := url.Values{"key": []string{"value1"}}
form2 := url.Values{"key": []string{"value2"}}
result := ValuesMonoid.Concat(form1, form2)
values := result["key"]
assert.Equal(t, 2, len(values))
assert.Equal(t, "value1", values[0])
assert.Equal(t, "value2", values[1])
})
t.Run("is associative", func(t *testing.T) {
form1 := url.Values{"key": []string{"value1"}}
form2 := url.Values{"key": []string{"value2"}}
form3 := url.Values{"key": []string{"value3"}}
result1 := ValuesMonoid.Concat(ValuesMonoid.Concat(form1, form2), form3)
result2 := ValuesMonoid.Concat(form1, ValuesMonoid.Concat(form2, form3))
assert.True(t, valuesEq.Equals(result1, result2))
})
}
// TestAtValues tests the AtValues lens
func TestAtValues(t *testing.T) {
t.Run("gets values array", func(t *testing.T) {
form := url.Values{"key": []string{"value1", "value2"}}
lens := AtValues("key")
result := lens.Get(form)
assert.True(t, O.IsSome(result))
values := O.GetOrElse(F.Constant([]string{}))(result)
assert.Equal(t, 2, len(values))
assert.Equal(t, "value1", values[0])
assert.Equal(t, "value2", values[1])
})
t.Run("returns None for non-existent key", func(t *testing.T) {
lens := AtValues("nonexistent")
result := lens.Get(Default)
assert.True(t, O.IsNone(result))
})
t.Run("sets values array", func(t *testing.T) {
lens := AtValues("key")
form := lens.Set(O.Some([]string{"value1", "value2"}))(Default)
values := form["key"]
assert.Equal(t, 2, len(values))
assert.Equal(t, "value1", values[0])
assert.Equal(t, "value2", values[1])
})
t.Run("removes field with None", func(t *testing.T) {
form := url.Values{"key": []string{"value"}}
lens := AtValues("key")
updated := lens.Set(O.None[[]string]())(form)
assert.False(t, updated.Has("key"))
})
t.Run("creates immutable transformation", func(t *testing.T) {
form := url.Values{"key": []string{"value1"}}
lens := AtValues("key")
updated := lens.Set(O.Some([]string{"value2"}))(form)
assert.False(t, valuesEq.Equals(form, updated))
assert.Equal(t, "value1", form.Get("key"))
assert.Equal(t, "value2", updated.Get("key"))
})
}
// TestAtValue tests the AtValue lens
func TestAtValue(t *testing.T) {
t.Run("gets first value", func(t *testing.T) {
form := url.Values{"key": []string{"value1", "value2"}}
lens := AtValue("key")
result := lens.Get(form)
assert.True(t, O.IsSome(result))
assert.Equal(t, "value1", O.GetOrElse(F.Constant(""))(result))
})
t.Run("returns None for non-existent key", func(t *testing.T) {
lens := AtValue("nonexistent")
result := lens.Get(Default)
assert.True(t, O.IsNone(result))
})
t.Run("returns None for empty array", func(t *testing.T) {
form := url.Values{"key": []string{}}
lens := AtValue("key")
result := lens.Get(form)
assert.True(t, O.IsNone(result))
})
t.Run("sets first value", func(t *testing.T) {
lens := AtValue("key")
form := lens.Set(O.Some("value"))(Default)
assert.Equal(t, "value", form.Get("key"))
})
t.Run("replaces first value in array", func(t *testing.T) {
form := url.Values{"key": []string{"old1", "old2"}}
lens := AtValue("key")
updated := lens.Set(O.Some("new"))(form)
values := updated["key"]
// AtValue modifies the head of the array, keeping other elements
assert.Equal(t, 2, len(values))
assert.Equal(t, "new", values[0])
assert.Equal(t, "old2", values[1])
})
t.Run("clears field with None", func(t *testing.T) {
form := url.Values{"key": []string{"value"}}
lens := AtValue("key")
updated := lens.Set(O.None[string]())(form)
// Setting to None creates an empty array, not removing the key
values := updated["key"]
assert.Equal(t, 0, len(values))
})
}
// Example tests demonstrating package usage
// ExampleWithValue demonstrates how to set form field values
func ExampleWithValue() {
// Create a form with a single field
form := WithValue("username")("john")(Default)
fmt.Println(form.Get("username"))
// Output: john
}
// ExampleWithValue_composition demonstrates composing multiple field assignments
func ExampleWithValue_composition() {
// Build a form with multiple fields using Pipe
form := F.Pipe3(
Default,
WithValue("username")("john"),
WithValue("email")("john@example.com"),
WithValue("age")("30"),
)
fmt.Println(form.Get("username"))
fmt.Println(form.Get("email"))
fmt.Println(form.Get("age"))
// Output:
// john
// john@example.com
// 30
}
// ExampleWithoutValue demonstrates clearing a form field value
func ExampleWithoutValue() {
// Create a form and then clear a field
form := F.Pipe2(
Default,
WithValue("username")("john"),
WithValue("password")("secret"),
)
// Clear the password field (sets it to empty array)
sanitized := WithoutValue("password")(form)
fmt.Println(sanitized.Get("username"))
fmt.Println(sanitized.Get("password"))
// Output:
// john
//
}
// ExampleAtValue demonstrates using the AtValue lens
func ExampleAtValue() {
form := WithValue("username")("john")(Default)
// Get a value using the lens
lens := AtValue("username")
value := lens.Get(form)
fmt.Println(O.IsSome(value))
fmt.Println(O.GetOrElse(F.Constant("default"))(value))
// Output:
// true
// john
}
// ExampleAtValue_set demonstrates setting a value using the AtValue lens
func ExampleAtValue_set() {
form := WithValue("username")("john")(Default)
// Update the value using the lens
lens := AtValue("username")
updated := lens.Set(O.Some("jane"))(form)
fmt.Println(updated.Get("username"))
// Output: jane
}
// ExampleMonoid demonstrates combining form transformations
func ExampleMonoid() {
// Combine multiple transformations into one
transform := Monoid.Concat(
WithValue("field1")("value1"),
WithValue("field2")("value2"),
)
result := transform(Default)
fmt.Println(result.Get("field1"))
fmt.Println(result.Get("field2"))
// Output:
// value1
// value2
}
// ExampleValuesMonoid demonstrates merging form data
func ExampleValuesMonoid() {
form1 := url.Values{"key1": []string{"value1"}}
form2 := url.Values{"key2": []string{"value2"}}
merged := ValuesMonoid.Concat(form1, form2)
fmt.Println(merged.Get("key1"))
fmt.Println(merged.Get("key2"))
// Output:
// value1
// value2
}
// ExampleValuesMonoid_concatenation demonstrates array concatenation for same keys
func ExampleValuesMonoid_concatenation() {
form1 := url.Values{"tags": []string{"go"}}
form2 := url.Values{"tags": []string{"functional"}}
merged := ValuesMonoid.Concat(form1, form2)
tags := merged["tags"]
fmt.Println(len(tags))
fmt.Println(tags[0])
fmt.Println(tags[1])
// Output:
// 2
// go
// functional
}
// ExampleAtValues demonstrates working with multiple values
func ExampleAtValues() {
form := url.Values{"tags": []string{"go", "functional", "programming"}}
lens := AtValues("tags")
values := lens.Get(form)
if O.IsSome(values) {
tags := O.GetOrElse(F.Constant([]string{}))(values)
fmt.Println(len(tags))
fmt.Println(tags[0])
}
// Output:
// 3
// go
}

View File

@@ -21,54 +21,261 @@ import (
"github.com/IBM/fp-go/v2/internal/functor"
)
// MonadAp applies a function to a value in the Identity monad context.
// Since Identity has no computational context, this is just function application.
//
// This is the uncurried version of Ap.
//
// Implements the Fantasy Land Apply specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#apply
//
// Example:
//
// result := identity.MonadAp(func(n int) int { return n * 2 }, 21)
// // result is 42
func MonadAp[B, A any](fab func(A) B, fa A) B {
return fab(fa)
}
// Ap applies a wrapped function to a wrapped value.
// Returns a function that takes a function and applies the value to it.
//
// This is the curried version of MonadAp, useful for composition with Pipe.
//
// Implements the Fantasy Land Apply specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#apply
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// double := func(n int) int { return n * 2 }
// result := F.Pipe1(double, identity.Ap[int](21))
// // result is 42
func Ap[B, A any](fa A) Operator[func(A) B, B] {
return function.Bind2nd(MonadAp[B, A], fa)
}
// MonadMap transforms a value using a function in the Identity monad context.
// Since Identity has no computational context, this is just function application.
//
// This is the uncurried version of Map.
//
// Implements the Fantasy Land Functor specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#functor
//
// Example:
//
// result := identity.MonadMap(21, func(n int) int { return n * 2 })
// // result is 42
func MonadMap[A, B any](fa A, f func(A) B) B {
return f(fa)
}
// Map transforms a value using a function.
// Returns the function itself since Identity adds no context.
//
// This is the curried version of MonadMap, useful for composition with Pipe.
//
// Implements the Fantasy Land Functor specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#functor
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe1(21, identity.Map(func(n int) int { return n * 2 }))
// // result is 42
func Map[A, B any](f func(A) B) Operator[A, B] {
return f
}
// MonadMapTo replaces a value with a constant, ignoring the input.
//
// This is the uncurried version of MapTo.
//
// Example:
//
// result := identity.MonadMapTo("ignored", 42)
// // result is 42
func MonadMapTo[A, B any](_ A, b B) B {
return b
}
// MapTo replaces any value with a constant value.
// Returns a function that ignores its input and returns the constant.
//
// This is the curried version of MonadMapTo, useful for composition with Pipe.
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe1("ignored", identity.MapTo[string](42))
// // result is 42
func MapTo[A, B any](b B) func(A) B {
return function.Constant1[A](b)
}
// Of wraps a value in the Identity monad.
// Since Identity has no computational context, this is just the identity function.
//
// This is the Pointed/Applicative "pure" operation.
//
// Implements the Fantasy Land Applicative specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#applicative
//
// Example:
//
// value := identity.Of(42)
// // value is 42
//
//go:inline
func Of[A any](a A) A {
return a
}
// MonadChain applies a Kleisli arrow to a value in the Identity monad context.
// Since Identity has no computational context, this is just function application.
//
// This is the uncurried version of Chain, also known as "bind" or "flatMap".
//
// Implements the Fantasy Land Chain specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#chain
//
// Example:
//
// result := identity.MonadChain(21, func(n int) int { return n * 2 })
// // result is 42
func MonadChain[A, B any](ma A, f Kleisli[A, B]) B {
return f(ma)
}
// Chain applies a Kleisli arrow to a value.
// Returns the function itself since Identity adds no context.
//
// This is the curried version of MonadChain, also known as "bind" or "flatMap".
// Useful for composition with Pipe.
//
// Implements the Fantasy Land Chain specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#chain
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe1(21, identity.Chain(func(n int) int { return n * 2 }))
// // result is 42
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return f
}
// MonadChainFirst executes a computation for its effect but returns the original value.
// Useful for side effects like logging while preserving the original value.
//
// This is the uncurried version of ChainFirst.
//
// Example:
//
// result := identity.MonadChainFirst(42, func(n int) string {
// fmt.Printf("Value: %d\n", n)
// return "logged"
// })
// // result is 42 (original value preserved)
func MonadChainFirst[A, B any](fa A, f Kleisli[A, B]) A {
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
}
// ChainFirst executes a computation for its effect but returns the original value.
// Useful for side effects like logging while preserving the original value.
//
// This is the curried version of MonadChainFirst, useful for composition with Pipe.
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe1(
// 42,
// identity.ChainFirst(func(n int) string {
// fmt.Printf("Value: %d\n", n)
// return "logged"
// }),
// )
// // result is 42 (original value preserved)
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(Chain[A, A], Map[B, A], f)
}
// MonadFlap applies a value to a function, flipping the normal application order.
// Instead of applying a function to a value, it applies a value to a function.
//
// This is the uncurried version of Flap.
//
// Example:
//
// double := func(n int) int { return n * 2 }
// result := identity.MonadFlap(double, 21)
// // result is 42
func MonadFlap[B, A any](fab func(A) B, a A) B {
return functor.MonadFlap(MonadMap[func(A) B, B], fab, a)
}
// Flap applies a value to a function, flipping the normal application order.
// Returns a function that takes a function and applies the value to it.
//
// This is the curried version of MonadFlap, useful for composition with Pipe.
// Useful when you have a value and want to apply it to multiple functions.
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// double := func(n int) int { return n * 2 }
// result := F.Pipe1(double, identity.Flap[int](21))
// // result is 42
//
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return functor.Flap(Map[func(A) B, B], a)
}
// Extract extracts the value from the Identity monad.
// Since Identity has no computational context, this is just the identity function.
//
// This is the Comonad "extract" operation.
//
// Implements the Fantasy Land Comonad specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#comonad
//
// Example:
//
// value := identity.Extract(42)
// // value is 42
//
//go:inline
func Extract[A any](a A) A {
return a
}
// Extend extends a computation over the Identity monad.
// Since Identity has no computational context, this is just function application.
//
// This is the Comonad "extend" operation, also known as "cobind".
//
// Implements the Fantasy Land Extend specification:
// https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#extend
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe1(21, identity.Extend(func(n int) int { return n * 2 }))
// // result is 42
//
//go:inline
func Extend[A, B any](f func(A) B) Operator[A, B] {
return f
}

View File

@@ -723,3 +723,99 @@ func TestTraverseTuple10(t *testing.T) {
assert.Equal(t, T.MakeTuple10(2, 4, 6, 8, 10, 12, 14, 16, 18, 20), result)
})
}
func TestExtract(t *testing.T) {
t.Run("extracts int value", func(t *testing.T) {
result := Extract(42)
assert.Equal(t, 42, result)
})
t.Run("extracts string value", func(t *testing.T) {
result := Extract("hello")
assert.Equal(t, "hello", result)
})
t.Run("extracts struct value", func(t *testing.T) {
type Person struct{ Name string }
p := Person{Name: "Alice"}
result := Extract(p)
assert.Equal(t, p, result)
})
t.Run("extracts pointer value", func(t *testing.T) {
value := 100
ptr := &value
result := Extract(ptr)
assert.Equal(t, ptr, result)
assert.Equal(t, 100, *result)
})
}
func TestExtend(t *testing.T) {
t.Run("extends with transformation", func(t *testing.T) {
result := F.Pipe1(21, Extend(utils.Double))
assert.Equal(t, 42, result)
})
t.Run("extends with type change", func(t *testing.T) {
result := F.Pipe1(42, Extend(S.Format[int]("Number: %d")))
assert.Equal(t, "Number: 42", result)
})
t.Run("chains multiple extends", func(t *testing.T) {
result := F.Pipe2(
5,
Extend(N.Mul(2)),
Extend(N.Add(10)),
)
assert.Equal(t, 20, result)
})
t.Run("extends with complex computation", func(t *testing.T) {
result := F.Pipe1(
10,
Extend(func(n int) string {
doubled := n * 2
return fmt.Sprintf("Result: %d", doubled)
}),
)
assert.Equal(t, "Result: 20", result)
})
}
// Test Comonad laws
func TestComonadLaws(t *testing.T) {
t.Run("left identity", func(t *testing.T) {
// Extract(Extend(f)(w)) === f(w)
w := 42
f := N.Mul(2)
left := Extract(F.Pipe1(w, Extend(f)))
right := f(w)
assert.Equal(t, right, left)
})
t.Run("right identity", func(t *testing.T) {
// Extend(Extract)(w) === w
w := 42
result := F.Pipe1(w, Extend(Extract[int]))
assert.Equal(t, w, result)
})
t.Run("associativity", func(t *testing.T) {
// Extend(f)(Extend(g)(w)) === Extend(x => f(Extend(g)(x)))(w)
w := 5
f := N.Mul(2)
g := N.Add(10)
left := F.Pipe2(w, Extend(g), Extend(f))
right := F.Pipe1(w, Extend(func(x int) int {
return f(F.Pipe1(x, Extend(g)))
}))
assert.Equal(t, right, left)
})
}

View File

@@ -0,0 +1,59 @@
package readerresult
import (
"context"
"github.com/IBM/fp-go/v2/function"
)
// Promap is the profunctor map operation that transforms both the input and output of a ReaderResult.
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Adapt the context before passing it to the ReaderResult (via f)
// - Transform the success value after the computation completes (via g)
//
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - A: The original success type produced by the ReaderResult
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input context, returning a new context and cancel function (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - An Operator that takes a ReaderResult[A] and returns a ReaderResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
)
}
// Contramap changes the value of the local context during the execution of a ReaderResult.
// This is the contravariant functor operation that transforms the input context.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is useful for adapting a ReaderResult to work with a modified context
// by providing a function that creates a new context (and optional cancel function) from the current one.
//
// Type Parameters:
// - A: The success type (unchanged)
//
// Parameters:
// - f: Function to transform the context, returning a new context and cancel function
//
// Returns:
// - A Kleisli arrow that takes a ReaderResult[A] and returns a ReaderResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Kleisli[ReaderResult[A], A] {
return Local[A](f)
}

View File

@@ -0,0 +1,187 @@
// Copyright (c) 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 readerresult
import (
"context"
"fmt"
"strconv"
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both context and output", func(t *testing.T) {
// ReaderResult that reads a value from context
getValue := func(ctx context.Context) (int, error) {
if val := ctx.Value("port"); val != nil {
return val.(int), nil
}
return 0, fmt.Errorf("port not found")
}
// Transform context to add a value and int to string
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 8080), func() {}
}
toString := strconv.Itoa
adapted := Promap(addPort, toString)(getValue)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, "8080", result)
})
t.Run("handles error case", func(t *testing.T) {
// ReaderResult that returns an error
getError := func(ctx context.Context) (int, error) {
return 0, fmt.Errorf("error occurred")
}
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 8080), func() {}
}
toString := strconv.Itoa
adapted := Promap(addPort, toString)(getError)
_, err := adapted(context.Background())
assert.Error(t, err)
assert.Equal(t, "error occurred", err.Error())
})
t.Run("context transformation with cancellation", func(t *testing.T) {
getValue := func(ctx context.Context) (string, error) {
if val := ctx.Value("key"); val != nil {
return val.(string), nil
}
return "", fmt.Errorf("key not found")
}
addValue := func(ctx context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
return context.WithValue(ctx, "key", "value"), cancel
}
toUpper := func(s string) string {
return "UPPER_" + s
}
adapted := Promap(addValue, toUpper)(getValue)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, "UPPER_value", result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("context adaptation", func(t *testing.T) {
// ReaderResult that reads from context
getPort := func(ctx context.Context) (int, error) {
if val := ctx.Value("port"); val != nil {
return val.(int), nil
}
return 0, fmt.Errorf("port not found")
}
// Adapt context to add port value
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 9000), func() {}
}
adapted := Contramap[int](addPort)(getPort)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, 9000, result)
})
t.Run("preserves error", func(t *testing.T) {
getError := func(ctx context.Context) (int, error) {
return 0, fmt.Errorf("config error")
}
addPort := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "port", 9000), func() {}
}
adapted := Contramap[int](addPort)(getError)
_, err := adapted(context.Background())
assert.Error(t, err)
assert.Equal(t, "config error", err.Error())
})
t.Run("multiple context values", func(t *testing.T) {
getValues := func(ctx context.Context) (string, error) {
host := ctx.Value("host")
port := ctx.Value("port")
if host != nil && port != nil {
return fmt.Sprintf("%s:%d", host, port), nil
}
return "", fmt.Errorf("missing values")
}
addValues := func(ctx context.Context) (context.Context, context.CancelFunc) {
ctx = context.WithValue(ctx, "host", "localhost")
ctx = context.WithValue(ctx, "port", 8080)
return ctx, func() {}
}
adapted := Contramap[string](addValues)(getValues)
result, err := adapted(context.Background())
assert.NoError(t, err)
assert.Equal(t, "localhost:8080", result)
})
}
// TestPromapComposition tests that Promap can be composed
func TestPromapComposition(t *testing.T) {
t.Run("compose two Promap transformations", func(t *testing.T) {
reader := func(ctx context.Context) (int, error) {
if val := ctx.Value("value"); val != nil {
return val.(int), nil
}
return 0, fmt.Errorf("value not found")
}
f1 := func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, "value", 5), func() {}
}
g1 := N.Mul(2)
f2 := func(ctx context.Context) (context.Context, context.CancelFunc) {
return ctx, func() {}
}
g2 := N.Add(10)
// Apply two Promap transformations
step1 := Promap(f1, g1)(reader)
step2 := Promap(f2, g2)(step1)
result, err := step2(context.Background())
// (5 * 2) + 10 = 20
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 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 readerioresult
import (
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/reader"
)
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOResult.
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Adapt the environment type before passing it to the ReaderIOResult (via f)
// - Transform the success value after the IO effect completes (via g)
//
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - E: The original environment type expected by the ReaderIOResult
// - A: The original success type produced by the ReaderIOResult
// - D: The new input environment type
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input environment from D to E (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[E, A] and returns a ReaderIOResult[D, B]
//
//go:inline
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderIOResult[E, A], B] {
return reader.Promap(f, ioresult.Map(g))
}
// Contramap changes the value of the local environment during the execution of a ReaderIOResult.
// This is the contravariant functor operation that transforms the input environment.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is useful for adapting a ReaderIOResult to work with a different environment type
// by providing a function that converts the new environment to the expected one.
//
// Type Parameters:
// - A: The success type (unchanged)
// - R2: The new input environment type
// - R1: The original environment type expected by the ReaderIOResult
//
// Parameters:
// - f: Function to transform the environment from R2 to R1
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
//go:inline
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOResult[R1, A], A] {
return Local[A](f)
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) 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 readerioresult
import (
"fmt"
"strconv"
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
type SimpleConfig struct {
Port int
}
type DetailedConfig struct {
Host string
Port int
}
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both input and output", func(t *testing.T) {
// ReaderIOResult that reads port from SimpleConfig
getPort := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return c.Port, nil
}
}
// Transform DetailedConfig to SimpleConfig and int to string
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.NoError(t, err)
assert.Equal(t, "8080", result)
})
t.Run("handles error case", func(t *testing.T) {
// ReaderIOResult that returns an error
getError := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return 0, fmt.Errorf("error occurred")
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.Error(t, err)
assert.Equal(t, "error occurred", err.Error())
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("environment adaptation", func(t *testing.T) {
// ReaderIOResult that reads from SimpleConfig
getPort := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return c.Port, nil
}
}
// Adapt to work with DetailedConfig
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
assert.NoError(t, err)
assert.Equal(t, 9000, result)
})
t.Run("preserves error", func(t *testing.T) {
getError := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
return 0, fmt.Errorf("config error")
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
assert.Error(t, err)
assert.Equal(t, "config error", err.Error())
})
}
// TestPromapWithIO tests Promap with actual IO effects
func TestPromapWithIO(t *testing.T) {
t.Run("transform IO result", func(t *testing.T) {
counter := 0
// ReaderIOResult with side effect
getPortWithEffect := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
counter++
return c.Port, nil
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getPortWithEffect)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.NoError(t, err)
assert.Equal(t, "8080", result)
assert.Equal(t, 1, counter) // Side effect occurred
})
t.Run("side effect occurs even on error", func(t *testing.T) {
counter := 0
getErrorWithEffect := func(c SimpleConfig) func() (int, error) {
return func() (int, error) {
counter++
return 0, fmt.Errorf("io error")
}
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getErrorWithEffect)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
assert.Error(t, err)
assert.Equal(t, 1, counter) // Side effect occurred before error
})
}
// TestPromapComposition tests that Promap can be composed
func TestPromapComposition(t *testing.T) {
t.Run("compose two Promap transformations", func(t *testing.T) {
type Config1 struct{ Value int }
type Config2 struct{ Value int }
type Config3 struct{ Value int }
reader := func(c Config1) func() (int, error) {
return func() (int, error) {
return c.Value, nil
}
}
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
g1 := N.Mul(2)
f2 := func(c3 Config3) Config2 { return Config2{Value: c3.Value} }
g2 := N.Add(10)
// Apply two Promap transformations
step1 := Promap(f1, g1)(reader)
step2 := Promap(f2, g2)(step1)
result, err := step2(Config3{Value: 5})()
// (5 * 2) + 10 = 20
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) 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 readerresult
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Promap is the profunctor map operation that transforms both the input and output of a ReaderResult.
// It applies f to the input environment (contravariantly) and g to the output value (covariantly).
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// This operation allows you to:
// - Adapt the environment type before passing it to the ReaderResult (via f)
// - Transform the success value after the computation completes (via g)
//
// The error type is fixed as error and remains unchanged through the transformation.
//
// Type Parameters:
// - E: The original environment type expected by the ReaderResult
// - A: The original success type produced by the ReaderResult
// - D: The new input environment type
// - B: The new output success type
//
// Parameters:
// - f: Function to transform the input environment from D to E (contravariant)
// - g: Function to transform the output success value from A to B (covariant)
//
// Returns:
// - A Kleisli arrow that takes a ReaderResult[E, A] and returns a ReaderResult[D, B]
//
//go:inline
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderResult[E, A], B] {
mp := result.Map(g)
return func(rr ReaderResult[E, A]) ReaderResult[D, B] {
return func(d D) (B, error) {
return mp(rr(f(d)))
}
}
}
// Contramap changes the value of the local environment during the execution of a ReaderResult.
// This is the contravariant functor operation that transforms the input environment.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Contramap is useful for adapting a ReaderResult to work with a different environment type
// by providing a function that converts the new environment to the expected one.
//
// Type Parameters:
// - A: The success type (unchanged)
// - R2: The new input environment type
// - R1: The original environment type expected by the ReaderResult
//
// Parameters:
// - f: Function to transform the environment from R2 to R1
//
// Returns:
// - A Kleisli arrow that takes a ReaderResult[R1, A] and returns a ReaderResult[R2, A]
//
//go:inline
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderResult[R1, A], A] {
return Local[A](f)
}

View File

@@ -0,0 +1,238 @@
// Copyright (c) 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 readerresult
import (
"fmt"
"strconv"
"testing"
N "github.com/IBM/fp-go/v2/number"
R "github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
type SimpleConfig struct {
Port int
}
type DetailedConfig struct {
Host string
Port int
}
// TestPromapBasic tests basic Promap functionality
func TestPromapBasic(t *testing.T) {
t.Run("transform both input and output", func(t *testing.T) {
// ReaderResult that reads port from SimpleConfig
getPort := func(c SimpleConfig) (int, error) {
return c.Port, nil
}
// Transform DetailedConfig to SimpleConfig and int to string
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})
assert.NoError(t, err)
assert.Equal(t, "8080", result)
})
t.Run("handles error case", func(t *testing.T) {
// ReaderResult that returns an error
getError := func(c SimpleConfig) (int, error) {
return 0, fmt.Errorf("error occurred")
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
toString := strconv.Itoa
adapted := Promap(simplify, toString)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 8080})
assert.Error(t, err)
assert.Equal(t, "error occurred", err.Error())
})
t.Run("environment transformation with complex types", func(t *testing.T) {
type Database struct {
ConnectionString string
}
type AppConfig struct {
DB Database
}
getConnection := func(db Database) (string, error) {
if db.ConnectionString == "" {
return "", fmt.Errorf("empty connection string")
}
return db.ConnectionString, nil
}
extractDB := func(cfg AppConfig) Database {
return cfg.DB
}
addPrefix := func(s string) string {
return "postgres://" + s
}
adapted := Promap(extractDB, addPrefix)(getConnection)
result, err := adapted(AppConfig{DB: Database{ConnectionString: "localhost:5432"}})
assert.NoError(t, err)
assert.Equal(t, "postgres://localhost:5432", result)
})
}
// TestContramapBasic tests basic Contramap functionality
func TestContramapBasic(t *testing.T) {
t.Run("environment adaptation", func(t *testing.T) {
// ReaderResult that reads from SimpleConfig
getPort := func(c SimpleConfig) (int, error) {
return c.Port, nil
}
// Adapt to work with DetailedConfig
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getPort)
result, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})
assert.NoError(t, err)
assert.Equal(t, 9000, result)
})
t.Run("preserves error", func(t *testing.T) {
getError := func(c SimpleConfig) (int, error) {
return 0, fmt.Errorf("config error")
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Port: d.Port}
}
adapted := Contramap[int](simplify)(getError)
_, err := adapted(DetailedConfig{Host: "localhost", Port: 9000})
assert.Error(t, err)
assert.Equal(t, "config error", err.Error())
})
t.Run("multiple field extraction", func(t *testing.T) {
type FullConfig struct {
Host string
Port int
Protocol string
}
getURL := func(c DetailedConfig) (string, error) {
return fmt.Sprintf("%s:%d", c.Host, c.Port), nil
}
extractHostPort := func(fc FullConfig) DetailedConfig {
return DetailedConfig{Host: fc.Host, Port: fc.Port}
}
adapted := Contramap[string](extractHostPort)(getURL)
result, err := adapted(FullConfig{Host: "example.com", Port: 443, Protocol: "https"})
assert.NoError(t, err)
assert.Equal(t, "example.com:443", result)
})
}
// TestPromapComposition tests that Promap can be composed
func TestPromapComposition(t *testing.T) {
t.Run("compose two Promap transformations", func(t *testing.T) {
type Config1 struct{ Value int }
type Config2 struct{ Value int }
type Config3 struct{ Value int }
reader := func(c Config1) (int, error) {
return c.Value, nil
}
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
g1 := N.Mul(2)
f2 := func(c3 Config3) Config2 { return Config2{Value: c3.Value} }
g2 := N.Add(10)
// Apply two Promap transformations
step1 := Promap(f1, g1)(reader)
step2 := Promap(f2, g2)(step1)
result, err := step2(Config3{Value: 5})
// (5 * 2) + 10 = 20
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("compose Promap and Contramap", func(t *testing.T) {
type Config1 struct{ Value int }
type Config2 struct{ Value int }
reader := func(c Config1) (int, error) {
return c.Value * 3, nil
}
// First apply Contramap
f1 := func(c2 Config2) Config1 { return Config1{Value: c2.Value} }
step1 := Contramap[int](f1)(reader)
// Then apply Promap
f2 := func(c2 Config2) Config2 { return c2 }
g2 := func(n int) string { return fmt.Sprintf("result: %d", n) }
step2 := Promap(f2, g2)(step1)
result, err := step2(Config2{Value: 7})
// 7 * 3 = 21
assert.NoError(t, err)
assert.Equal(t, "result: 21", result)
})
}
// TestPromapIdentityLaws tests profunctor identity laws
func TestPromapIdentityLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// Promap with identity functions should be identity
reader := func(c SimpleConfig) (int, error) {
return c.Port, nil
}
identity := R.Ask[SimpleConfig]()
identityInt := R.Ask[int]()
adapted := Promap(identity, identityInt)(reader)
config := SimpleConfig{Port: 8080}
result1, err1 := reader(config)
result2, err2 := adapted(config)
assert.Equal(t, err1, err2)
assert.Equal(t, result1, result2)
})
}

View File

@@ -501,7 +501,7 @@ func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
// rr := readerresult.Of[Config](42)
// adapted := readerresult.Local[int](toConfig)(rr)
// // adapted now accepts DB instead of Config
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderResult[R1, A]) ReaderResult[R2, A] {
return func(rr ReaderResult[R1, A]) ReaderResult[R2, A] {
return func(r R2) (A, error) {
return rr(f(r))

View File

@@ -22,6 +22,7 @@ import (
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
S "github.com/IBM/fp-go/v2/semigroup"
STR "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
@@ -267,7 +268,7 @@ func TestApV_ZeroValues(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[int, int](sg)
identity := func(x int) int { return x }
identity := reader.Ask[int]()
value, verr := Right(0)
fn, ferr := Right(identity)

View File

@@ -21,20 +21,68 @@ import (
type (
ioApplicative[A, B any] struct{}
// IOApplicative represents the applicative functor type class for IO.
// It combines the capabilities of Functor (Map) and Pointed (Of) with
// the ability to apply wrapped functions to wrapped values (Ap).
//
// An applicative functor is a functor with two additional operations:
// - Of: lifts a pure value into the IO context
// - Ap: applies a wrapped function to a wrapped value
//
// This allows for function application within the IO context while maintaining
// the computational structure. The Ap operation uses parallel execution by default
// for better performance.
//
// Type parameters:
// - A: the input type
// - B: the output type
IOApplicative[A, B any] = applicative.Applicative[A, B, IO[A], IO[B], IO[func(A) B]]
)
// Of lifts a pure value into the IO context.
// This is the pointed functor operation that wraps a value in an IO computation.
//
// Example:
//
// app := io.Applicative[int, string]()
// ioValue := app.Of(42) // IO[int] that returns 42
// result := ioValue() // 42
func (o *ioApplicative[A, B]) Of(a A) IO[A] {
return Of(a)
}
// Map transforms the result of an IO computation by applying a function to it.
// This is the functor operation that allows mapping over wrapped values.
//
// Example:
//
// app := io.Applicative[int, string]()
// double := func(x int) int { return x * 2 }
// ioValue := app.Of(21)
// doubled := app.Map(double)(ioValue)
// result := doubled() // 42
func (o *ioApplicative[A, B]) Map(f func(A) B) Operator[A, B] {
return Map(f)
}
// Ap applies a wrapped function to a wrapped value, both in the IO context.
// This operation uses parallel execution by default, running the function and
// value computations concurrently for better performance.
//
// The Ap operation is useful for applying multi-argument functions in a curried
// fashion within the IO context.
//
// Example:
//
// app := io.Applicative[int, int]()
// add := func(a int) func(int) int {
// return func(b int) int { return a + b }
// }
// ioFunc := app.Of(add(10)) // IO[func(int) int]
// ioValue := app.Of(32) // IO[int]
// result := app.Ap(ioValue)(ioFunc)
// value := result() // 42
func (o *ioApplicative[A, B]) Ap(fa IO[A]) Operator[func(A) B, B] {
return Ap[B](fa)
}
@@ -43,10 +91,45 @@ func (o *ioApplicative[A, B]) Ap(fa IO[A]) Operator[func(A) B, B] {
// This provides a structured way to access applicative operations (Of, Map, Ap)
// for IO computations.
//
// Example:
// The applicative pattern is useful when you need to:
// - Apply functions with multiple arguments to wrapped values
// - Combine multiple independent IO computations
// - Maintain the computational structure while transforming values
//
// Type parameters:
// - A: the input type for the applicative operations
// - B: the output type for the applicative operations
//
// Example - Basic usage:
//
// app := io.Applicative[int, string]()
// result := app.Map(strconv.Itoa)(app.Of(42))
// value := result() // "42"
//
// Example - Applying curried functions:
//
// app := io.Applicative[int, int]()
// add := func(a int) func(int) int {
// return func(b int) int { return a + b }
// }
// // Create IO computations
// ioFunc := io.Map(add)(app.Of(10)) // IO[func(int) int]
// ioValue := app.Of(32) // IO[int]
// // Apply the function to the value
// result := app.Ap(ioValue)(ioFunc)
// value := result() // 42
//
// Example - Combining multiple IO computations:
//
// app := io.Applicative[int, int]()
// multiply := func(a int) func(int) int {
// return func(b int) int { return a * b }
// }
// io1 := app.Of(6)
// io2 := app.Of(7)
// ioFunc := io.Map(multiply)(io1)
// result := app.Ap(io2)(ioFunc)
// value := result() // 42
func Applicative[A, B any]() IOApplicative[A, B] {
return &ioApplicative[A, B]{}
}

360
v2/io/applicative_test.go Normal file
View File

@@ -0,0 +1,360 @@
// Copyright (c) 2024 - 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 io
import (
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/stretchr/testify/assert"
)
// TestApplicativeOf tests the Of operation of the Applicative type class
func TestApplicativeOf(t *testing.T) {
app := Applicative[int, string]()
t.Run("wraps a value in IO context", func(t *testing.T) {
ioValue := app.Of(42)
result := ioValue()
assert.Equal(t, 42, result)
})
t.Run("wraps string value", func(t *testing.T) {
app := Applicative[string, int]()
ioValue := app.Of("hello")
result := ioValue()
assert.Equal(t, "hello", result)
})
t.Run("wraps zero value", func(t *testing.T) {
ioValue := app.Of(0)
result := ioValue()
assert.Equal(t, 0, result)
})
}
// TestApplicativeMap tests the Map operation of the Applicative type class
func TestApplicativeMap(t *testing.T) {
app := Applicative[int, int]()
t.Run("maps a function over IO value", func(t *testing.T) {
double := func(x int) int { return x * 2 }
ioValue := app.Of(21)
result := app.Map(double)(ioValue)
assert.Equal(t, 42, result())
})
t.Run("maps type conversion", func(t *testing.T) {
app := Applicative[int, string]()
ioValue := app.Of(42)
result := app.Map(strconv.Itoa)(ioValue)
assert.Equal(t, "42", result())
})
t.Run("maps identity function", func(t *testing.T) {
identity := func(x int) int { return x }
ioValue := app.Of(42)
result := app.Map(identity)(ioValue)
assert.Equal(t, 42, result())
})
t.Run("maps constant function", func(t *testing.T) {
constant := func(x int) int { return 100 }
ioValue := app.Of(42)
result := app.Map(constant)(ioValue)
assert.Equal(t, 100, result())
})
}
// TestApplicativeAp tests the Ap operation of the Applicative type class
func TestApplicativeAp(t *testing.T) {
t.Run("applies wrapped function to wrapped value", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
ioFunc := Of(add(10))
ioValue := Of(32)
result := Ap[int](ioValue)(ioFunc)
assert.Equal(t, 42, result())
})
t.Run("applies multiplication function", func(t *testing.T) {
multiply := func(a int) func(int) int {
return func(b int) int { return a * b }
}
ioFunc := Of(multiply(6))
ioValue := Of(7)
result := Ap[int](ioValue)(ioFunc)
assert.Equal(t, 42, result())
})
t.Run("applies function with zero", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
ioFunc := Of(add(0))
ioValue := Of(42)
result := Ap[int](ioValue)(ioFunc)
assert.Equal(t, 42, result())
})
t.Run("applies with type conversion", func(t *testing.T) {
toStringAndAppend := func(suffix string) func(int) string {
return func(n int) string {
return strconv.Itoa(n) + suffix
}
}
ioFunc := Of(toStringAndAppend("!"))
ioValue := Of(42)
result := Ap[string](ioValue)(ioFunc)
assert.Equal(t, "42!", result())
})
}
// TestApplicativeComposition tests composition of applicative operations
func TestApplicativeComposition(t *testing.T) {
app := Applicative[int, int]()
t.Run("composes Map and Of", func(t *testing.T) {
double := func(x int) int { return x * 2 }
result := F.Pipe1(
app.Of(21),
app.Map(double),
)
assert.Equal(t, 42, result())
})
t.Run("composes multiple Map operations", func(t *testing.T) {
app := Applicative[int, string]()
double := func(x int) int { return x * 2 }
toString := func(x int) string { return strconv.Itoa(x) }
result := F.Pipe2(
app.Of(21),
Map(double),
app.Map(toString),
)
assert.Equal(t, "42", result())
})
t.Run("composes Map and Ap", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
ioFunc := F.Pipe1(
app.Of(5),
Map(add),
)
ioValue := app.Of(16)
result := Ap[int](ioValue)(ioFunc)
assert.Equal(t, 21, result())
})
}
// TestApplicativeLaws tests the applicative functor laws
func TestApplicativeLaws(t *testing.T) {
app := Applicative[int, int]()
t.Run("identity law: ap(Of(id), v) = v", func(t *testing.T) {
identity := func(x int) int { return x }
v := app.Of(42)
left := Ap[int](v)(Of(identity))
right := v
assert.Equal(t, right(), left())
})
t.Run("homomorphism law: ap(Of(f), Of(x)) = Of(f(x))", func(t *testing.T) {
f := func(x int) int { return x * 2 }
x := 21
left := Ap[int](app.Of(x))(Of(f))
right := app.Of(f(x))
assert.Equal(t, right(), left())
})
t.Run("interchange law: ap(u, Of(y)) = ap(Of(f => f(y)), u)", func(t *testing.T) {
double := func(x int) int { return x * 2 }
u := Of(double)
y := 21
left := Ap[int](app.Of(y))(u)
applyY := func(f func(int) int) int { return f(y) }
right := Ap[int](u)(Of(applyY))
assert.Equal(t, right(), left())
})
}
// TestApplicativeWithPipe tests applicative operations with pipe
func TestApplicativeWithPipe(t *testing.T) {
t.Run("pipes Of and Map", func(t *testing.T) {
app := Applicative[int, string]()
result := F.Pipe1(
app.Of(42),
app.Map(strconv.Itoa),
)
assert.Equal(t, "42", result())
})
t.Run("pipes complex transformation", func(t *testing.T) {
app := Applicative[int, int]()
add10 := func(x int) int { return x + 10 }
double := func(x int) int { return x * 2 }
result := F.Pipe2(
app.Of(16),
app.Map(add10),
app.Map(double),
)
assert.Equal(t, 52, result())
})
}
// TestApplicativeWithUtils tests applicative with utility functions
func TestApplicativeWithUtils(t *testing.T) {
app := Applicative[int, int]()
t.Run("uses utils.Double", func(t *testing.T) {
result := F.Pipe1(
app.Of(21),
app.Map(utils.Double),
)
assert.Equal(t, 42, result())
})
t.Run("uses utils.Inc", func(t *testing.T) {
result := F.Pipe1(
app.Of(41),
app.Map(utils.Inc),
)
assert.Equal(t, 42, result())
})
}
// TestApplicativeMultipleArguments tests applying functions with multiple arguments
func TestApplicativeMultipleArguments(t *testing.T) {
app := Applicative[int, int]()
t.Run("applies curried two-argument function", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
// Create IO with curried function
ioFunc := F.Pipe1(
app.Of(10),
Map(add),
)
// Apply to second argument
result := Ap[int](app.Of(32))(ioFunc)
assert.Equal(t, 42, result())
})
t.Run("applies curried three-argument function", func(t *testing.T) {
add3 := func(a int) func(int) func(int) int {
return func(b int) func(int) int {
return func(c int) int {
return a + b + c
}
}
}
// Build up the computation step by step
ioFunc1 := F.Pipe1(
app.Of(10),
Map(add3),
)
ioFunc2 := Ap[func(int) int](app.Of(20))(ioFunc1)
result := Ap[int](app.Of(12))(ioFunc2)
assert.Equal(t, 42, result())
})
}
// TestApplicativeParallelExecution tests that Ap uses parallel execution
func TestApplicativeParallelExecution(t *testing.T) {
t.Run("executes function and value in parallel", func(t *testing.T) {
// This test verifies that both computations happen
// The actual parallelism is tested by the implementation
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
ioFunc := Of(add(20))
ioValue := Of(22)
result := Ap[int](ioValue)(ioFunc)
assert.Equal(t, 42, result())
})
}
// TestApplicativeInstance tests that Applicative returns a valid instance
func TestApplicativeInstance(t *testing.T) {
t.Run("returns non-nil instance", func(t *testing.T) {
app := Applicative[int, string]()
assert.NotNil(t, app)
})
t.Run("multiple calls return independent instances", func(t *testing.T) {
app1 := Applicative[int, string]()
app2 := Applicative[int, string]()
// Both should work independently
result1 := app1.Of(42)
result2 := app2.Of(43)
assert.Equal(t, 42, result1())
assert.Equal(t, 43, result2())
})
}
// TestApplicativeWithDifferentTypes tests applicative with various type combinations
func TestApplicativeWithDifferentTypes(t *testing.T) {
t.Run("int to string", func(t *testing.T) {
app := Applicative[int, string]()
result := app.Map(strconv.Itoa)(app.Of(42))
assert.Equal(t, "42", result())
})
t.Run("string to int", func(t *testing.T) {
app := Applicative[string, int]()
toLength := func(s string) int { return len(s) }
result := app.Map(toLength)(app.Of("hello"))
assert.Equal(t, 5, result())
})
t.Run("bool to string", func(t *testing.T) {
app := Applicative[bool, string]()
toString := func(b bool) string {
if b {
return "true"
}
return "false"
}
result := app.Map(toString)(app.Of(true))
assert.Equal(t, "true", result())
})
}

View File

@@ -13,6 +13,51 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides IO operations for file system interactions.
//
// This package offers functional wrappers around common file operations,
// returning IO monads that encapsulate side effects. All operations are
// lazy and only execute when the returned IO is invoked.
//
// # Core Operations
//
// The package provides two main operations:
// - Close: Safely close io.Closer resources
// - Remove: Remove files from the file system
//
// Both operations ignore errors and return the original input, making them
// suitable for cleanup operations where errors should not interrupt the flow.
//
// # Basic Usage
//
// // Close a file
// file, _ := os.Open("data.txt")
// closeIO := file.Close(file)
// closeIO() // Closes the file, ignoring any error
//
// // Remove a file
// removeIO := file.Remove("temp.txt")
// removeIO() // Removes the file, ignoring any error
//
// # Composition with IO
//
// These operations can be composed with other IO operations:
//
// result := pipe.Pipe2(
// openFile("data.txt"),
// io.ChainFirst(processFile),
// io.Chain(file.Close),
// )
//
// # Error Handling
//
// Both Close and Remove intentionally ignore errors. This design is suitable
// for cleanup operations where:
// - The operation is best-effort
// - Errors should not interrupt the program flow
// - The resource state is not critical
//
// For operations requiring error handling, use ioeither or ioresult instead.
package file
import (
@@ -22,7 +67,36 @@ import (
IO "github.com/IBM/fp-go/v2/io"
)
// Close closes a closeable resource and ignores a potential error
// Close closes a closeable resource and ignores any potential error.
// Returns an IO that, when executed, closes the resource and returns it.
//
// This function is useful for cleanup operations where errors can be safely
// ignored, such as in defer statements or resource cleanup chains.
//
// Type Parameters:
// - R: Any type that implements io.Closer
//
// Parameters:
// - r: The resource to close
//
// Returns:
// - IO[R]: An IO computation that closes the resource and returns it
//
// Example:
//
// file, _ := os.Open("data.txt")
// defer file.Close(file)() // Close when function returns
//
// Example with IO composition:
//
// result := pipe.Pipe3(
// openFile("data.txt"),
// io.Chain(readContent),
// io.ChainFirst(file.Close),
// )
//
// Note: The #nosec comment is intentional - errors are deliberately ignored
// for cleanup operations where failure should not interrupt the flow.
func Close[R io.Closer](r R) IO.IO[R] {
return func() R {
r.Close() // #nosec: G104
@@ -30,7 +104,42 @@ func Close[R io.Closer](r R) IO.IO[R] {
}
}
// Remove removes a resource and ignores a potential error
// Remove removes a file or directory and ignores any potential error.
// Returns an IO that, when executed, removes the named file or directory
// and returns the name.
//
// This function is useful for cleanup operations where errors can be safely
// ignored, such as removing temporary files or cache directories.
//
// Parameters:
// - name: The path to the file or directory to remove
//
// Returns:
// - IO[string]: An IO computation that removes the file and returns the name
//
// Example:
//
// cleanup := file.Remove("temp.txt")
// cleanup() // Removes temp.txt, ignoring any error
//
// Example with multiple files:
//
// cleanup := pipe.Pipe2(
// file.Remove("temp1.txt"),
// io.ChainTo(file.Remove("temp2.txt")),
// )
// cleanup() // Removes both files
//
// Example in defer:
//
// tempFile := "temp.txt"
// defer file.Remove(tempFile)()
// // ... use tempFile ...
//
// Note: The #nosec comment is intentional - errors are deliberately ignored
// for cleanup operations where failure should not interrupt the flow.
// This function only removes the named file or empty directory. To remove
// a directory and its contents, use os.RemoveAll wrapped in an IO.
func Remove(name string) IO.IO[string] {
return func() string {
os.Remove(name) // #nosec: G104

405
v2/io/file/file_test.go Normal file
View File

@@ -0,0 +1,405 @@
// 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 file
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"testing"
IO "github.com/IBM/fp-go/v2/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockCloser is a mock implementation of io.Closer for testing
type mockCloser struct {
closed bool
closeErr error
closeFunc func() error
}
func (m *mockCloser) Close() error {
m.closed = true
if m.closeFunc != nil {
return m.closeFunc()
}
return m.closeErr
}
// TestClose_WithMockCloser tests the Close function with a mock closer
func TestClose_WithMockCloser(t *testing.T) {
t.Run("closes resource successfully", func(t *testing.T) {
mock := &mockCloser{}
closeIO := Close(mock)
result := closeIO()
assert.True(t, mock.closed, "resource should be closed")
assert.Equal(t, mock, result, "should return the same resource")
})
t.Run("ignores close error", func(t *testing.T) {
mock := &mockCloser{
closeErr: fmt.Errorf("close error"),
}
closeIO := Close(mock)
// Should not panic even with error
result := closeIO()
assert.True(t, mock.closed, "resource should be closed despite error")
assert.Equal(t, mock, result, "should return the same resource")
})
t.Run("can be called multiple times", func(t *testing.T) {
mock := &mockCloser{}
closeIO := Close(mock)
result1 := closeIO()
result2 := closeIO()
assert.True(t, mock.closed, "resource should be closed")
assert.Equal(t, result1, result2, "should return same resource each time")
})
}
// TestClose_WithBytesBuffer tests Close with bytes.Buffer (implements io.Closer)
func TestClose_WithBytesBuffer(t *testing.T) {
t.Run("closes bytes.Buffer", func(t *testing.T) {
buf := bytes.NewBuffer([]byte("test data"))
closeIO := Close(io.NopCloser(buf))
result := closeIO()
assert.NotNil(t, result, "should return the closer")
})
}
// TestClose_WithFile tests Close with actual file
func TestClose_WithFile(t *testing.T) {
t.Run("closes real file", func(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "test-close-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
// Write some data
_, err = tmpFile.WriteString("test data")
require.NoError(t, err)
// Close using our function
closeIO := Close(tmpFile)
result := closeIO()
assert.Equal(t, tmpFile, result, "should return the same file")
// Verify file is closed by trying to write (should fail)
_, err = tmpFile.WriteString("more data")
assert.Error(t, err, "writing to closed file should fail")
})
}
// TestClose_Composition tests Close in IO composition
func TestClose_Composition(t *testing.T) {
t.Run("composes with other IO operations", func(t *testing.T) {
mock := &mockCloser{}
// Create a pipeline that uses the resource and then closes it
step1 := IO.Of(mock)
step2 := IO.Map(func(m *mockCloser) *mockCloser {
// Simulate using the resource
return m
})(step1)
pipeline := IO.Chain(Close[*mockCloser])(step2)
result := pipeline()
assert.True(t, mock.closed, "resource should be closed in pipeline")
assert.Equal(t, mock, result, "should return the resource")
})
t.Run("works with ChainFirst", func(t *testing.T) {
mock := &mockCloser{}
data := "test data"
// Process data and close resource as side effect
pipeline := IO.ChainFirst(func(string) IO.IO[*mockCloser] {
return Close(mock)
})(IO.Of(data))
result := pipeline()
assert.True(t, mock.closed, "resource should be closed")
assert.Equal(t, data, result, "should return original data")
})
}
// TestRemove_BasicOperation tests basic Remove functionality
func TestRemove_BasicOperation(t *testing.T) {
t.Run("removes existing file", func(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "test-remove-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
// Verify file exists
_, err = os.Stat(tmpPath)
require.NoError(t, err, "file should exist before removal")
// Remove using our function
removeIO := Remove(tmpPath)
result := removeIO()
assert.Equal(t, tmpPath, result, "should return the file path")
// Verify file is removed
_, err = os.Stat(tmpPath)
assert.True(t, os.IsNotExist(err), "file should not exist after removal")
})
t.Run("ignores error for non-existent file", func(t *testing.T) {
nonExistentPath := filepath.Join(os.TempDir(), "non-existent-file-12345.txt")
// Should not panic even if file doesn't exist
removeIO := Remove(nonExistentPath)
result := removeIO()
assert.Equal(t, nonExistentPath, result, "should return the path")
})
t.Run("removes empty directory", func(t *testing.T) {
// Create a temporary directory
tmpDir, err := os.MkdirTemp("", "test-remove-dir-*")
require.NoError(t, err)
// Verify directory exists
_, err = os.Stat(tmpDir)
require.NoError(t, err, "directory should exist before removal")
// Remove using our function
removeIO := Remove(tmpDir)
result := removeIO()
assert.Equal(t, tmpDir, result, "should return the directory path")
// Verify directory is removed
_, err = os.Stat(tmpDir)
assert.True(t, os.IsNotExist(err), "directory should not exist after removal")
})
t.Run("ignores error for non-empty directory", func(t *testing.T) {
// Create a temporary directory with a file
tmpDir, err := os.MkdirTemp("", "test-remove-nonempty-*")
require.NoError(t, err)
defer os.RemoveAll(tmpDir) // Cleanup
tmpFile := filepath.Join(tmpDir, "file.txt")
err = os.WriteFile(tmpFile, []byte("data"), 0644)
require.NoError(t, err)
// Should not panic even if directory is not empty
removeIO := Remove(tmpDir)
result := removeIO()
assert.Equal(t, tmpDir, result, "should return the path")
// Directory should still exist (os.Remove doesn't remove non-empty dirs)
_, err = os.Stat(tmpDir)
assert.NoError(t, err, "non-empty directory should still exist")
})
}
// TestRemove_Composition tests Remove in IO composition
func TestRemove_Composition(t *testing.T) {
t.Run("composes with other IO operations", func(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "test-compose-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
// Create a pipeline that processes and removes the file
step1 := IO.Of(tmpPath)
step2 := IO.Map(func(path string) string {
// Simulate processing
return path
})(step1)
pipeline := IO.Chain(Remove)(step2)
result := pipeline()
assert.Equal(t, tmpPath, result, "should return the path")
// Verify file is removed
_, err = os.Stat(tmpPath)
assert.True(t, os.IsNotExist(err), "file should be removed")
})
t.Run("removes multiple files in sequence", func(t *testing.T) {
// Create temporary files
tmpFile1, err := os.CreateTemp("", "test-multi-1-*.txt")
require.NoError(t, err)
tmpPath1 := tmpFile1.Name()
tmpFile1.Close()
tmpFile2, err := os.CreateTemp("", "test-multi-2-*.txt")
require.NoError(t, err)
tmpPath2 := tmpFile2.Name()
tmpFile2.Close()
// Remove both files in sequence
pipeline := IO.ChainTo[string](Remove(tmpPath2))(Remove(tmpPath1))
result := pipeline()
assert.Equal(t, tmpPath2, result, "should return last path")
// Verify both files are removed
_, err = os.Stat(tmpPath1)
assert.True(t, os.IsNotExist(err), "first file should be removed")
_, err = os.Stat(tmpPath2)
assert.True(t, os.IsNotExist(err), "second file should be removed")
})
}
// TestRemove_CanBeCalledMultipleTimes tests idempotency
func TestRemove_CanBeCalledMultipleTimes(t *testing.T) {
t.Run("calling remove multiple times is safe", func(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "test-idempotent-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
tmpFile.Close()
removeIO := Remove(tmpPath)
// First call removes the file
result1 := removeIO()
assert.Equal(t, tmpPath, result1)
// Second call should not panic (file already removed)
result2 := removeIO()
assert.Equal(t, tmpPath, result2)
// Verify file is removed
_, err = os.Stat(tmpPath)
assert.True(t, os.IsNotExist(err), "file should be removed")
})
}
// TestCloseAndRemove_Together tests using both functions together
func TestCloseAndRemove_Together(t *testing.T) {
t.Run("close and remove file in sequence", func(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "test-close-remove-*.txt")
require.NoError(t, err)
tmpPath := tmpFile.Name()
// Write some data
_, err = tmpFile.WriteString("test data")
require.NoError(t, err)
// Close and remove in sequence
pipeline := IO.Chain(func(f *os.File) IO.IO[string] {
return Remove(f.Name())
})(Close(tmpFile))
result := pipeline()
assert.Equal(t, tmpPath, result, "should return the path")
// Verify file is removed
_, err = os.Stat(tmpPath)
assert.True(t, os.IsNotExist(err), "file should be removed")
})
}
// TestClose_TypeSafety tests that Close works with different io.Closer types
func TestClose_TypeSafety(t *testing.T) {
t.Run("works with different closer types", func(t *testing.T) {
// Test with different types that implement io.Closer
types := []io.Closer{
&mockCloser{},
io.NopCloser(bytes.NewBuffer(nil)),
}
for _, closer := range types {
closeIO := Close(closer)
result := closeIO()
assert.Equal(t, closer, result, "should return the same closer")
}
})
}
// Example_close demonstrates basic usage of Close
func Example_close() {
// Create a mock closer
mock := &mockCloser{}
// Create an IO that closes the resource
closeIO := Close(mock)
// Execute the IO
result := closeIO()
fmt.Printf("Closed: %v\n", result.closed)
// Output: Closed: true
}
// Example_remove demonstrates basic usage of Remove
func Example_remove() {
// Create a temporary file
tmpFile, _ := os.CreateTemp("", "example-*.txt")
tmpPath := tmpFile.Name()
tmpFile.Close()
// Create an IO that removes the file
removeIO := Remove(tmpPath)
// Execute the IO
path := removeIO()
// Check if file exists
_, err := os.Stat(path)
fmt.Printf("File removed: %v\n", os.IsNotExist(err))
// Output: File removed: true
}
// Example_closeAndRemove demonstrates using Close and Remove together
func Example_closeAndRemove() {
// Create a temporary file
tmpFile, _ := os.CreateTemp("", "example-*.txt")
// Create a pipeline that closes and removes the file
pipeline := IO.Chain(func(f *os.File) IO.IO[string] {
return Remove(f.Name())
})(Close(tmpFile))
// Execute the pipeline
path := pipeline()
// Check if file exists
_, err := os.Stat(path)
fmt.Printf("File removed: %v\n", os.IsNotExist(err))
// Output: File removed: true
}

View File

@@ -240,7 +240,7 @@ func TestCopyFileChaining(t *testing.T) {
// Chain two copy operations
result := F.Pipe1(
CopyFile(srcPath)(dst1Path),
IOE.Chain[error](func(string) IOEither[error, string] {
IOE.Chain(func(string) IOEither[error, string] {
return CopyFile(dst1Path)(dst2Path)
}),
)()

View File

@@ -141,7 +141,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
onNegative := func(n int) string { return "negative number" }
filter := FilterOrElse(isPositive, onNegative)
double := Map[string](func(n int) int { return n * 2 })
double := Map[string](N.Mul(2))
// Compose: filter then double
result1 := double(filter(Right[string](5)))()

View File

@@ -47,14 +47,14 @@ import (
// Example - Remove duplicate integers:
//
// seq := From(1, 2, 3, 2, 4, 1, 5)
// unique := Uniq(func(x int) int { return x })
// unique := Uniq(reader.Ask[int]())
// result := unique(seq)
// // yields: 1, 2, 3, 4, 5
//
// Example - Unique by string length:
//
// seq := From("a", "bb", "c", "dd", "eee")
// uniqueByLength := Uniq(func(s string) int { return len(s) })
// uniqueByLength := Uniq(S.Size)
// result := uniqueByLength(seq)
// // yields: "a", "bb", "eee" (first occurrence of each length)
//
@@ -82,14 +82,14 @@ import (
// Example - Empty sequence:
//
// seq := Empty[int]()
// unique := Uniq(func(x int) int { return x })
// unique := Uniq(reader.Ask[int]())
// result := unique(seq)
// // yields: nothing (empty sequence)
//
// Example - All duplicates:
//
// seq := From(1, 1, 1, 1)
// unique := Uniq(func(x int) int { return x })
// unique := Uniq(reader.Ask[int]())
// result := unique(seq)
// // yields: 1 (only first occurrence)
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {

View File

@@ -377,7 +377,7 @@ func ExampleUniq() {
func ExampleUniq_byLength() {
seq := From("a", "bb", "c", "dd", "eee")
uniqueByLength := Uniq(func(s string) int { return len(s) })
uniqueByLength := Uniq(S.Size)
result := uniqueByLength(seq)
for v := range result {

View File

@@ -25,6 +25,7 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
@@ -497,7 +498,7 @@ func TestMapComposition(t *testing.T) {
Of(5),
Map(N.Mul(2)),
Map(N.Add(10)),
Map(func(x int) int { return x }),
Map(reader.Ask[int]()),
)
assert.Equal(t, 20, result())

View File

@@ -154,7 +154,7 @@ FunctionMonoid - Creates a monoid for functions when the codomain has a monoid:
funcMonoid := monoid.FunctionMonoid[string, int](intAddMonoid)
f1 := func(s string) int { return len(s) }
f1 := S.Size
f2 := func(s string) int { return len(s) * 2 }
// Combine functions: result(x) = f1(x) + f2(x)

View File

@@ -49,7 +49,7 @@ import (
// funcMonoid := FunctionMonoid[string, int](intAddMonoid)
//
// // Define some functions
// f1 := func(s string) int { return len(s) }
// f1 := S.Size
// f2 := func(s string) int { return len(s) * 2 }
//
// // Combine functions: result(x) = f1(x) + f2(x)

260
v2/optics/codec/codec.go Normal file
View File

@@ -0,0 +1,260 @@
package codec
import (
"errors"
"fmt"
"reflect"
"strconv"
"github.com/IBM/fp-go/v2/array"
A "github.com/IBM/fp-go/v2/array"
"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/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
R "github.com/IBM/fp-go/v2/reflect"
"github.com/IBM/fp-go/v2/result"
)
// typeImpl is the internal implementation of the Type interface.
// It combines encoding, decoding, validation, and type checking capabilities.
type typeImpl[A, O, I any] struct {
name string
is Reader[any, Result[A]]
validate Validate[I, A]
encode Encode[A, O]
}
// MakeType creates a new Type with the given name, type checker, validator, and encoder.
//
// Parameters:
// - name: A descriptive name for this type (used in error messages)
// - is: A function that checks if a value is of type A
// - validate: A function that validates and decodes input I to type A
// - encode: A function that encodes type A to output O
//
// Returns a Type[A, O, I] that can both encode and decode values.
func MakeType[A, O, I any](
name string,
is Reader[any, Result[A]],
validate Validate[I, A],
encode Encode[A, O],
) Type[A, O, I] {
return &typeImpl[A, O, I]{
name: name,
is: is,
validate: validate,
encode: encode,
}
}
// Validate validates the input value in the context of a validation path.
// Returns a Reader that takes a Context and produces a Validation result.
func (t *typeImpl[A, O, I]) Validate(i I) Reader[Context, Validation[A]] {
return t.validate(i)
}
// Decode validates and decodes the input value, creating a new context with this type's name.
// This is a convenience method that calls Validate with a fresh context.
func (t *typeImpl[A, O, I]) Decode(i I) Validation[A] {
return t.validate(i)(array.Of(validation.ContextEntry{Type: t.name, Actual: i}))
}
// Encode transforms a value of type A into the output format O.
func (t *typeImpl[A, O, I]) Encode(a A) O {
return t.encode(a)
}
// AsDecoder returns this Type as a Decoder interface.
func (t *typeImpl[A, O, I]) AsDecoder() Decoder[I, A] {
return t
}
// AsEncoder returns this Type as an Encoder interface.
func (t *typeImpl[A, O, I]) AsEncoder() Encoder[A, O] {
return t
}
// Name returns the descriptive name of this type.
func (t *typeImpl[A, O, I]) Name() string {
return t.name
}
func (t *typeImpl[A, O, I]) Is(i any) Result[A] {
return t.is(i)
}
// Pipe composes two Types, creating a pipeline where:
// - Decoding: I -> A -> B (decode with 'this', then validate with 'ab')
// - Encoding: B -> A -> O (encode with 'ab', then encode with 'this')
//
// This allows building complex codecs from simpler ones.
//
// Example:
//
// stringToInt := codec.MakeType(...) // Type[int, string, string]
// intToPositive := codec.MakeType(...) // Type[PositiveInt, int, int]
// composed := codec.Pipe(intToPositive)(stringToInt) // Type[PositiveInt, string, string]
func Pipe[A, B, O, I any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
return func(this Type[A, O, I]) Type[B, O, I] {
return MakeType(
fmt.Sprintf("Pipe(%s, %s)", this.Name(), ab.Name()),
ab.Is,
F.Flow2(
this.Validate,
readereither.Chain(ab.Validate),
),
F.Flow2(
ab.Encode,
this.Encode,
),
)
}
}
// isNil checks if a value is nil, handling both typed and untyped nil values.
// It uses reflection to detect nil pointers, maps, slices, channels, functions, and interfaces.
func isNil(x any) bool {
if x == nil {
return true
}
v := reflect.ValueOf(x)
switch v.Kind() {
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
return v.IsNil()
default:
return false
}
}
// isTypedNil checks if a value is nil and returns it as a typed nil pointer.
// Returns Some(nil) if the value is nil, None otherwise.
func isTypedNil[A any](x any) Result[*A] {
if isNil(x) {
return result.Of[*A](nil)
}
return result.Left[*A](errors.New("expecting nil"))
}
func validateFromIs[A any](
is ReaderResult[any, A],
msg string,
) Reader[any, Reader[Context, Validation[A]]] {
return func(u any) Reader[Context, Validation[A]] {
return F.Pipe2(
u,
is,
result.Fold(
validation.FailureWithError[A](u, msg),
F.Flow2(
validation.Success[A],
reader.Of[Context],
),
),
)
}
}
// MakeNilType creates a Type that validates nil values.
// It accepts any input and validates that it is nil, returning a typed nil pointer.
//
// Example:
//
// nilType := codec.MakeNilType[string]()
// result := nilType.Decode(nil) // Success: Right((*string)(nil))
// result := nilType.Decode("not nil") // Failure: Left(errors)
func Nil[A any]() Type[*A, *A, any] {
is := isTypedNil[A]
return MakeType(
"nil",
is,
validateFromIs(is, "nil"),
F.Identity[*A],
)
}
func MakeSimpleType[A any]() Type[A, A, any] {
var zero A
name := fmt.Sprintf("%T", zero)
is := Is[A]()
return MakeType(
name,
is,
validateFromIs(is, name),
F.Identity[A],
)
}
func String() Type[string, string, any] {
return MakeSimpleType[string]()
}
func Int() Type[int, int, any] {
return MakeSimpleType[int]()
}
func Bool() Type[bool, bool, any] {
return MakeSimpleType[bool]()
}
func appendContext(key, typ string, actual any) Endomorphism[Context] {
return A.Push(validation.ContextEntry{Key: key, Type: typ, Actual: actual})
}
type validationPair[T any] = Pair[validation.Errors, T]
func pairToValidation[T any](p validationPair[T]) Validation[T] {
errors, value := pair.Unpack(p)
if A.IsNonEmpty(errors) {
return either.Left[T](errors)
}
return either.Of[validation.Errors](value)
}
func validateArray[T any](item Type[T, T, any]) func(u any) Reader[Context, Validation[[]T]] {
appendErrors := F.Flow2(
A.Concat,
pair.MapHead[[]T, validation.Errors],
)
appendValues := F.Flow2(
A.Push,
pair.MapTail[validation.Errors, []T],
)
itemName := item.Name()
return func(u any) Reader[Context, Validation[[]T]] {
val := reflect.ValueOf(u)
if !val.IsValid() {
return validation.FailureWithMessage[[]T](val, "invalid value")
}
kind := val.Kind()
switch kind {
case reflect.Array, reflect.Slice, reflect.String:
return func(c Context) Validation[[]T] {
return F.Pipe1(
R.MonadReduceWithIndex(val, func(i int, p validationPair[[]T], v reflect.Value) validationPair[[]T] {
return either.MonadFold[validation.Errors, T, Endomorphism[validationPair[[]T]]](
item.Validate(u)(appendContext(strconv.Itoa(i), itemName, u)(c)),
appendErrors,
appendValues,
)(p)
}, validationPair[[]T]{}),
pairToValidation,
)
}
default:
return validation.FailureWithMessage[[]T](val, fmt.Sprintf("type %s is not iterable", kind))
}
}
}

View File

@@ -0,0 +1,15 @@
package codec
import (
"log"
"testing"
)
func TestStringCoded(t *testing.T) {
sType := String()
res := sType.Decode(10)
log.Println(res)
}

57
v2/optics/codec/doc.go Normal file
View File

@@ -0,0 +1,57 @@
// Package codec provides a functional approach to encoding and decoding data with validation.
//
// The codec package combines the concepts of encoders and decoders into a unified Type that can
// both encode values to an output format and decode/validate values from an input format. This
// is particularly useful for data serialization, API validation, and type-safe transformations.
//
// # Core Concepts
//
// Type[A, O, I]: A bidirectional codec that can:
// - Decode input I to type A with validation
// - Encode type A to output O
// - Check if a value is of type A
//
// Validation: Decoding returns Either[Errors, A] which represents:
// - Left(Errors): Validation failed with detailed error information
// - Right(A): Successfully decoded and validated value
//
// Context: A stack of ContextEntry values that tracks the path through nested structures
// during validation, providing detailed error messages.
//
// # Basic Usage
//
// Creating a simple type:
//
// nilType := codec.MakeNilType[string]()
// result := nilType.Decode(nil) // Success
// result := nilType.Decode("not nil") // Failure
//
// Composing types with Pipe:
//
// composed := codec.Pipe(typeB)(typeA)
// // Decodes: I -> A -> B
// // Encodes: B -> A -> O
//
// # Type Parameters
//
// Most functions use three type parameters:
// - A: The domain type (the actual Go type being encoded/decoded)
// - O: The output type for encoding
// - I: The input type for decoding
//
// # Validation Errors
//
// ValidationError contains:
// - Value: The actual value that failed validation
// - Context: The path to the value in nested structures
// - Message: Human-readable error description
//
// # Integration
//
// This package integrates with:
// - optics/decoder: For decoding operations
// - optics/encoder: For encoding operations
// - either: For validation results
// - option: For optional type checking
// - reader: For context-dependent operations
package codec

83
v2/optics/codec/types.go Normal file
View File

@@ -0,0 +1,83 @@
package codec
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/decoder"
"github.com/IBM/fp-go/v2/optics/encoder"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerresult"
"github.com/IBM/fp-go/v2/result"
)
type (
ReaderResult[R, A any] = readerresult.ReaderResult[R, A]
// Lazy represents a lazily evaluated value.
Lazy[A any] = lazy.Lazy[A]
// Reader represents a computation that depends on an environment R and produces a value A.
Reader[R, A any] = reader.Reader[R, A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// Either represents a value that can be one of two types: Left (error) or Right (success).
Either[E, A any] = either.Either[E, A]
// Result represents a computation that may fail with an error.
Result[A any] = result.Result[A]
// Codec combines a Decoder and an Encoder for bidirectional transformations.
// It can decode input I to type A and encode type A to output O.
Codec[I, O, A any] struct {
Decode decoder.Decoder[I, A]
Encode encoder.Encoder[O, A]
}
Validation[A any] = validation.Validation[A]
Context = validation.Context
// Validate is a function that validates input I to produce type A.
// It takes an input and returns a Reader that depends on the validation Context.
Validate[I, A any] = Reader[I, Reader[Context, Validation[A]]]
// Decode is a function that decodes input I to type A with validation.
// It returns a Validation result directly.
Decode[I, A any] = Reader[I, Validation[A]]
// Encode is a function that encodes type A to output O.
Encode[A, O any] = Reader[A, O]
// Decoder is an interface for types that can decode and validate input.
Decoder[I, A any] interface {
Name() string
Validate(I) Reader[Context, Validation[A]]
Decode(I) Validation[A]
}
// Encoder is an interface for types that can encode values.
Encoder[A, O any] interface {
// Encode transforms a value of type A into output format O.
Encode(A) O
}
// Type is a bidirectional codec that combines encoding, decoding, validation,
// and type checking capabilities. It represents a complete specification of
// how to work with a particular type.
Type[A, O, I any] interface {
Decoder[I, A]
Encoder[A, O]
AsDecoder() Decoder[I, A]
AsEncoder() Encoder[A, O]
Is(any) Result[A]
}
Endomorphism[A any] = endomorphism.Endomorphism[A]
Pair[L, R any] = pair.Pair[L, R]
)

View File

@@ -0,0 +1,21 @@
package codec
import (
"fmt"
"github.com/IBM/fp-go/v2/result"
)
func onTypeError(expType string) func(any) error {
return func(u any) error {
return fmt.Errorf("expecting type [%s] but got [%T]", expType, u)
}
}
// Is checks if a value can be converted to type T.
// Returns Some(value) if the conversion succeeds, None otherwise.
// This is a type-safe cast operation.
func Is[T any]() func(any) Result[T] {
var zero T
return result.ToType[T](onTypeError(fmt.Sprintf("%T", zero)))
}

View File

@@ -0,0 +1,104 @@
// Package validation provides functional validation types and operations for the codec system.
//
// This package implements a validation monad that accumulates errors during validation operations,
// making it ideal for form validation, data parsing, and other scenarios where you want to collect
// all validation errors rather than failing on the first error.
//
// # Core Concepts
//
// Validation[A]: Represents the result of a validation operation as Either[Errors, A]:
// - Left(Errors): Validation failed with one or more errors
// - Right(A): Successfully validated value of type A
//
// ValidationError: A detailed error type that includes:
// - Value: The actual value that failed validation
// - Context: The path through nested structures (e.g., "user.address.zipCode")
// - Message: Human-readable error description
// - Cause: Optional underlying error
//
// Context: A stack of ContextEntry values that tracks the validation path through
// nested data structures, enabling precise error reporting.
//
// # Basic Usage
//
// Creating validation results:
//
// // Success case
// valid := validation.Success(42)
//
// // Failure case
// invalid := validation.Failures[int](validation.Errors{
// &validation.ValidationError{
// Value: "not a number",
// Message: "expected integer",
// Context: nil,
// },
// })
//
// Using with context:
//
// failWithMsg := validation.FailureWithMessage[int]("invalid", "must be positive")
// result := failWithMsg([]validation.ContextEntry{
// {Key: "age", Type: "int"},
// })
//
// # Applicative Validation
//
// The validation type supports applicative operations, allowing you to combine
// multiple validations and accumulate all errors:
//
// type User struct {
// Name string
// Email string
// Age int
// }
//
// validateName := func(s string) validation.Validation[string] {
// if len(s) > 0 {
// return validation.Success(s)
// }
// return validation.Failures[string](/* error */)
// }
//
// // Combine validations - all errors will be collected
// result := validation.Ap(validation.Ap(validation.Ap(
// validation.Of(func(name string) func(email string) func(age int) User {
// return func(email string) func(age int) User {
// return func(age int) User {
// return User{name, email, age}
// }
// }
// }),
// )(validateName("")))(validateEmail("")))(validateAge(-1))
//
// # Error Formatting
//
// ValidationError implements custom formatting for detailed error messages:
//
// err := &ValidationError{
// Value: "abc",
// Context: []ContextEntry{{Key: "user"}, {Key: "age"}},
// Message: "expected integer",
// }
//
// fmt.Printf("%v", err) // at user.age: expected integer
// fmt.Printf("%+v", err) // at user.age: expected integer
// // value: "abc"
//
// # Monoid Operations
//
// The package provides monoid instances for combining validations:
//
// // Combine validation results
// m := validation.ApplicativeMonoid(stringMonoid)
// combined := m.Concat(validation.Success("hello"), validation.Success(" world"))
// // Result: Success("hello world")
//
// # Integration
//
// This package integrates with:
// - either: Validation is built on Either for error handling
// - array: For collecting multiple errors
// - monoid: For combining validation results
// - reader: For context-dependent validation operations
package validation

View File

@@ -0,0 +1,112 @@
package validation
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/internal/applicative"
)
// Of creates a successful validation result containing the given value.
// This is the pure/return operation for the Validation monad.
//
// Example:
//
// valid := Of(42) // Validation[int] containing 42
func Of[A any](a A) Validation[A] {
return either.Of[Errors](a)
}
// Ap applies a validation containing a function to a validation containing a value.
// This is the applicative apply operation that accumulates errors from both validations.
// If either validation fails, all errors are collected. If both succeed, the function is applied.
//
// This enables combining multiple validations while collecting all errors:
//
// Example:
//
// // Validate multiple fields and collect all errors
// validateUser := Ap(Ap(Of(func(name string) func(age int) User {
// return func(age int) User { return User{name, age} }
// }))(validateName))(validateAge)
func Ap[B, A any](fa Validation[A]) Operator[func(A) B, B] {
return either.ApV[B, A](ErrorsMonoid())(fa)
}
// Map transforms the value inside a successful validation using the provided function.
// If the validation is a failure, the errors are preserved unchanged.
// This is the functor map operation for Validation.
//
// Example:
//
// doubled := Map(func(x int) int { return x * 2 })(Of(21))
// // Result: Success(42)
func Map[A, B any](f func(A) B) Operator[A, B] {
return either.Map[Errors](f)
}
// Applicative creates an Applicative instance for Validation with error accumulation.
//
// This returns a lawful Applicative that accumulates validation errors using the Errors monoid.
// Unlike the standard Either applicative which fails fast, this validation applicative collects
// all errors when combining independent validations with Ap.
//
// The returned instance satisfies all applicative laws:
// - Identity: Ap(Of(identity))(v) == v
// - Homomorphism: Ap(Of(f))(Of(x)) == Of(f(x))
// - Interchange: Ap(Of(f))(u) == Ap(Map(f => f(y))(u))(Of(y))
// - Composition: Ap(Ap(Map(compose)(f))(g))(x) == Ap(f)(Ap(g)(x))
//
// Key behaviors:
// - Of: lifts a value into a successful Validation (Right)
// - Map: transforms successful values, preserves failures (standard functor)
// - Ap: when both operands fail, combines all errors using the Errors monoid
//
// This is particularly useful for form validation, configuration validation, and any scenario
// where you want to collect all validation errors at once rather than stopping at the first failure.
//
// Example - Validating Multiple Fields:
//
// app := Applicative[string, User]()
//
// // Validate individual fields
// validateName := func(name string) Validation[string] {
// if len(name) < 3 {
// return Failure("Name must be at least 3 characters")
// }
// return Success(name)
// }
//
// validateAge := func(age int) Validation[int] {
// if age < 18 {
// return Failure("Must be 18 or older")
// }
// return Success(age)
// }
//
// // Create a curried constructor
// makeUser := func(name string) func(int) User {
// return func(age int) User {
// return User{Name: name, Age: age}
// }
// }
//
// // Combine validations - all errors are collected
// name := validateName("ab") // Failure: name too short
// age := validateAge(16) // Failure: age too low
//
// result := app.Ap(age)(app.Ap(name)(app.Of(makeUser)))
// // result contains both validation errors:
// // - "Name must be at least 3 characters"
// // - "Must be 18 or older"
//
// Type Parameters:
// - A: The input value type (Right value)
// - B: The output value type after transformation
//
// Returns:
//
// An Applicative instance with Of, Map, and Ap operations that accumulate errors
func Applicative[A, B any]() applicative.Applicative[A, B, Validation[A], Validation[B], Validation[func(A) B]] {
return either.ApplicativeV[Errors, A, B](
ErrorsMonoid(),
)
}

View File

@@ -0,0 +1,921 @@
package validation
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestOf(t *testing.T) {
t.Run("creates successful validation", func(t *testing.T) {
result := Of(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("works with different types", func(t *testing.T) {
strResult := Of("hello")
assert.True(t, either.IsRight(strResult))
boolResult := Of(true)
assert.True(t, either.IsRight(boolResult))
type Custom struct{ Value int }
customResult := Of(Custom{Value: 100})
assert.True(t, either.IsRight(customResult))
})
t.Run("is equivalent to Success", func(t *testing.T) {
value := 42
ofResult := Of(value)
successResult := Success(value)
assert.Equal(t, ofResult, successResult)
})
}
func TestMap(t *testing.T) {
t.Run("transforms successful validation", func(t *testing.T) {
double := func(x int) int { return x * 2 }
result := Map(double)(Of(21))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("preserves failure", func(t *testing.T) {
errs := Errors{&ValidationError{Messsage: "error"}}
failure := Failures[int](errs)
double := func(x int) int { return x * 2 }
result := Map(double)(failure)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "error", errors[0].Messsage)
})
t.Run("chains multiple maps", func(t *testing.T) {
add10 := func(x int) int { return x + 10 }
double := func(x int) int { return x * 2 }
toString := func(x int) string { return fmt.Sprintf("%d", x) }
result := F.Pipe3(
Of(5),
Map(add10),
Map(double),
Map(toString),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "30", value)
})
t.Run("type transformation", func(t *testing.T) {
length := func(s string) int { return len(s) }
result := Map(length)(Of("hello"))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 5, value)
})
}
func TestAp(t *testing.T) {
t.Run("applies function to value when both succeed", func(t *testing.T) {
double := func(x int) int { return x * 2 }
funcValidation := Of(double)
valueValidation := Of(21)
result := Ap[int, int](valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("accumulates errors when value fails", func(t *testing.T) {
double := func(x int) int { return x * 2 }
funcValidation := Of(double)
valueValidation := Failures[int](Errors{
&ValidationError{Messsage: "value error"},
})
result := Ap[int, int](valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "value error", errors[0].Messsage)
})
t.Run("accumulates errors when function fails", func(t *testing.T) {
funcValidation := Failures[func(int) int](Errors{
&ValidationError{Messsage: "function error"},
})
valueValidation := Of(21)
result := Ap[int, int](valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "function error", errors[0].Messsage)
})
t.Run("accumulates all errors when both fail", func(t *testing.T) {
funcValidation := Failures[func(int) int](Errors{
&ValidationError{Messsage: "function error"},
})
valueValidation := Failures[int](Errors{
&ValidationError{Messsage: "value error"},
})
result := Ap[int, int](valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "function error")
assert.Contains(t, messages, "value error")
})
t.Run("applies with string transformation", func(t *testing.T) {
toUpper := func(s string) string { return fmt.Sprintf("UPPER:%s", s) }
funcValidation := Of(toUpper)
valueValidation := Of("hello")
result := Ap[string, string](valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "UPPER:hello", value)
})
t.Run("accumulates multiple validation errors from different sources", func(t *testing.T) {
funcValidation := Failures[func(int) int](Errors{
&ValidationError{Messsage: "function error 1"},
&ValidationError{Messsage: "function error 2"},
})
valueValidation := Failures[int](Errors{
&ValidationError{Messsage: "value error 1"},
})
result := Ap[int, int](valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 3)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "function error 1")
assert.Contains(t, messages, "function error 2")
assert.Contains(t, messages, "value error 1")
})
}
func TestMonadLaws(t *testing.T) {
t.Run("functor identity law", func(t *testing.T) {
// Map(id) == id
value := Of(42)
mapped := Map(F.Identity[int])(value)
assert.Equal(t, value, mapped)
})
t.Run("functor composition law", func(t *testing.T) {
// Map(f . g) == Map(f) . Map(g)
f := func(x int) int { return x * 2 }
g := func(x int) int { return x + 10 }
composed := func(x int) int { return f(g(x)) }
value := Of(5)
left := Map(composed)(value)
right := F.Pipe2(value, Map(g), Map(f))
assert.Equal(t, left, right)
})
t.Run("applicative identity law", func(t *testing.T) {
// Ap(v)(Of(id)) == v
v := Of(42)
result := Ap[int, int](v)(Of(F.Identity[int]))
assert.Equal(t, v, result)
})
t.Run("applicative homomorphism law", func(t *testing.T) {
// Ap(Of(x))(Of(f)) == Of(f(x))
f := func(x int) int { return x * 2 }
x := 21
left := Ap[int, int](Of(x))(Of(f))
right := Of(f(x))
assert.Equal(t, left, right)
})
}
func TestMapWithOperator(t *testing.T) {
t.Run("Map returns an Operator", func(t *testing.T) {
double := func(x int) int { return x * 2 }
operator := Map(double)
// Operator can be applied to different validations
result1 := operator(Of(10))
result2 := operator(Of(20))
val1 := either.MonadFold(result1,
func(Errors) int { return 0 },
F.Identity[int],
)
val2 := either.MonadFold(result2,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 20, val1)
assert.Equal(t, 40, val2)
})
}
func TestApWithOperator(t *testing.T) {
t.Run("Ap returns an Operator", func(t *testing.T) {
valueValidation := Of(21)
operator := Ap[int, int](valueValidation)
// Operator can be applied to different function validations
double := func(x int) int { return x * 2 }
triple := func(x int) int { return x * 3 }
result1 := operator(Of(double))
result2 := operator(Of(triple))
val1 := either.MonadFold(result1,
func(Errors) int { return 0 },
F.Identity[int],
)
val2 := either.MonadFold(result2,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, val1)
assert.Equal(t, 63, val2)
})
}
func TestApplicative(t *testing.T) {
t.Run("returns non-nil instance", func(t *testing.T) {
app := Applicative[int, string]()
assert.NotNil(t, app)
})
t.Run("multiple calls return independent instances", func(t *testing.T) {
app1 := Applicative[int, string]()
app2 := Applicative[int, string]()
// Both should work independently
result1 := app1.Of(42)
result2 := app2.Of(43)
assert.True(t, either.IsRight(result1))
assert.True(t, either.IsRight(result2))
val1 := either.MonadFold(result1, func(Errors) int { return 0 }, F.Identity[int])
val2 := either.MonadFold(result2, func(Errors) int { return 0 }, F.Identity[int])
assert.Equal(t, 42, val1)
assert.Equal(t, 43, val2)
})
}
func TestApplicativeOf(t *testing.T) {
app := Applicative[int, string]()
t.Run("wraps a value in Validation context", func(t *testing.T) {
result := app.Of(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("wraps string value", func(t *testing.T) {
app := Applicative[string, int]()
result := app.Of("hello")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "hello", value)
})
t.Run("wraps zero value", func(t *testing.T) {
result := app.Of(0)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("wraps complex types", func(t *testing.T) {
type User struct {
Name string
Age int
}
app := Applicative[User, string]()
user := User{Name: "Alice", Age: 30}
result := app.Of(user)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) User { return User{} },
F.Identity[User],
)
assert.Equal(t, user, value)
})
}
func TestApplicativeMap(t *testing.T) {
app := Applicative[int, int]()
t.Run("maps a function over successful validation", func(t *testing.T) {
double := func(x int) int { return x * 2 }
result := app.Map(double)(app.Of(21))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("maps type conversion", func(t *testing.T) {
app := Applicative[int, string]()
toString := func(x int) string { return fmt.Sprintf("%d", x) }
result := app.Map(toString)(app.Of(42))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "42", value)
})
t.Run("maps identity function", func(t *testing.T) {
result := app.Map(F.Identity[int])(app.Of(42))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("preserves failure", func(t *testing.T) {
errs := Errors{&ValidationError{Messsage: "error"}}
failure := Failures[int](errs)
double := func(x int) int { return x * 2 }
result := app.Map(double)(failure)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "error", errors[0].Messsage)
})
t.Run("chains multiple maps", func(t *testing.T) {
app := Applicative[int, string]()
add10 := func(x int) int { return x + 10 }
double := func(x int) int { return x * 2 }
toString := func(x int) string { return fmt.Sprintf("%d", x) }
result := F.Pipe3(
app.Of(5),
Map(add10),
Map(double),
app.Map(toString),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "30", value)
})
}
func TestApplicativeAp(t *testing.T) {
app := Applicative[int, int]()
t.Run("applies wrapped function to wrapped value when both succeed", func(t *testing.T) {
double := func(x int) int { return x * 2 }
funcValidation := Of(double)
valueValidation := app.Of(21)
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("accumulates errors when value fails", func(t *testing.T) {
double := func(x int) int { return x * 2 }
funcValidation := Of(double)
valueValidation := Failures[int](Errors{
&ValidationError{Messsage: "value error"},
})
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "value error", errors[0].Messsage)
})
t.Run("accumulates errors when function fails", func(t *testing.T) {
funcValidation := Failures[func(int) int](Errors{
&ValidationError{Messsage: "function error"},
})
valueValidation := app.Of(21)
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "function error", errors[0].Messsage)
})
t.Run("accumulates all errors when both fail", func(t *testing.T) {
funcValidation := Failures[func(int) int](Errors{
&ValidationError{Messsage: "function error"},
})
valueValidation := Failures[int](Errors{
&ValidationError{Messsage: "value error"},
})
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "function error")
assert.Contains(t, messages, "value error")
})
t.Run("applies with type conversion", func(t *testing.T) {
app := Applicative[int, string]()
toString := func(x int) string { return fmt.Sprintf("value:%d", x) }
funcValidation := Of(toString)
valueValidation := app.Of(42)
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "value:42", value)
})
t.Run("accumulates multiple errors from different sources", func(t *testing.T) {
funcValidation := Failures[func(int) int](Errors{
&ValidationError{Messsage: "function error 1"},
&ValidationError{Messsage: "function error 2"},
})
valueValidation := Failures[int](Errors{
&ValidationError{Messsage: "value error 1"},
&ValidationError{Messsage: "value error 2"},
})
result := app.Ap(valueValidation)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 4)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "function error 1")
assert.Contains(t, messages, "function error 2")
assert.Contains(t, messages, "value error 1")
assert.Contains(t, messages, "value error 2")
})
}
func TestApplicativeComposition(t *testing.T) {
app := Applicative[int, int]()
t.Run("composes Map and Of", func(t *testing.T) {
double := func(x int) int { return x * 2 }
result := F.Pipe1(
app.Of(21),
app.Map(double),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("composes multiple Map operations", func(t *testing.T) {
app := Applicative[int, string]()
double := func(x int) int { return x * 2 }
toString := func(x int) string { return fmt.Sprintf("%d", x) }
result := F.Pipe2(
app.Of(21),
Map(double),
app.Map(toString),
)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "42", value)
})
t.Run("composes Map and Ap", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
ioFunc := F.Pipe1(
app.Of(5),
Map(add),
)
valueValidation := app.Of(16)
result := app.Ap(valueValidation)(ioFunc)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 21, value)
})
}
func TestApplicativeLawsWithInstance(t *testing.T) {
app := Applicative[int, int]()
t.Run("identity law: Ap(Of(id))(v) == v", func(t *testing.T) {
identity := func(x int) int { return x }
v := app.Of(42)
left := app.Ap(v)(Of(identity))
right := v
assert.Equal(t, right, left)
})
t.Run("homomorphism law: Ap(Of(x))(Of(f)) == Of(f(x))", func(t *testing.T) {
f := func(x int) int { return x * 2 }
x := 21
left := app.Ap(app.Of(x))(Of(f))
right := app.Of(f(x))
assert.Equal(t, right, left)
})
t.Run("interchange law: Ap(Of(y))(u) == Ap(u)(Of($ y))", func(t *testing.T) {
double := func(x int) int { return x * 2 }
u := Of(double)
y := 21
left := app.Ap(app.Of(y))(u)
applyY := func(f func(int) int) int { return f(y) }
right := Ap[int](u)(Of(applyY))
assert.Equal(t, right, left)
})
t.Run("identity law with failure", func(t *testing.T) {
identity := func(x int) int { return x }
v := Failures[int](Errors{&ValidationError{Messsage: "error"}})
left := app.Ap(v)(Of(identity))
right := v
assert.Equal(t, right, left)
})
}
func TestApplicativeMultipleArguments(t *testing.T) {
app := Applicative[int, int]()
t.Run("applies curried two-argument function", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
// Create validation with curried function
funcValidation := F.Pipe1(
app.Of(10),
Map(add),
)
// Apply to second argument
result := app.Ap(app.Of(32))(funcValidation)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("applies curried three-argument function", func(t *testing.T) {
add3 := func(a int) func(int) func(int) int {
return func(b int) func(int) int {
return func(c int) int {
return a + b + c
}
}
}
// Build up the computation step by step
funcValidation1 := F.Pipe1(
app.Of(10),
Map(add3),
)
funcValidation2 := Ap[func(int) int](app.Of(20))(funcValidation1)
result := Ap[int](app.Of(12))(funcValidation2)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("accumulates errors from multiple arguments", func(t *testing.T) {
add := func(a int) func(int) int {
return func(b int) int { return a + b }
}
// First argument fails
arg1 := Failures[int](Errors{&ValidationError{Messsage: "arg1 error"}})
// Second argument fails
arg2 := Failures[int](Errors{&ValidationError{Messsage: "arg2 error"}})
funcValidation := F.Pipe1(arg1, Map(add))
result := app.Ap(arg2)(funcValidation)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "arg1 error")
assert.Contains(t, messages, "arg2 error")
})
}
func TestApplicativeWithDifferentTypes(t *testing.T) {
t.Run("int to string", func(t *testing.T) {
app := Applicative[int, string]()
toString := func(x int) string { return fmt.Sprintf("%d", x) }
result := app.Map(toString)(app.Of(42))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "42", value)
})
t.Run("string to int", func(t *testing.T) {
app := Applicative[string, int]()
toLength := func(s string) int { return len(s) }
result := app.Map(toLength)(app.Of("hello"))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 5, value)
})
t.Run("bool to string", func(t *testing.T) {
app := Applicative[bool, string]()
toString := func(b bool) string {
if b {
return "true"
}
return "false"
}
result := app.Map(toString)(app.Of(true))
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "true", value)
})
}
func TestApplicativeRealWorldScenario(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
t.Run("validates user with all valid fields", func(t *testing.T) {
validateName := func(name string) Validation[string] {
if len(name) < 3 {
return Failures[string](Errors{&ValidationError{Messsage: "Name must be at least 3 characters"}})
}
return Success(name)
}
validateAge := func(age int) Validation[int] {
if age < 18 {
return Failures[int](Errors{&ValidationError{Messsage: "Must be 18 or older"}})
}
return Success(age)
}
validateEmail := func(email string) Validation[string] {
if len(email) == 0 {
return Failures[string](Errors{&ValidationError{Messsage: "Email is required"}})
}
return Success(email)
}
makeUser := func(name string) func(int) func(string) User {
return func(age int) func(string) User {
return func(email string) User {
return User{Name: name, Age: age, Email: email}
}
}
}
name := validateName("Alice")
age := validateAge(25)
email := validateEmail("alice@example.com")
// Use the standalone Ap function with proper type parameters
result := Ap[User](email)(Ap[func(string) User](age)(Ap[func(int) func(string) User](name)(Of(makeUser))))
assert.True(t, either.IsRight(result))
user := either.MonadFold(result,
func(Errors) User { return User{} },
F.Identity[User],
)
assert.Equal(t, "Alice", user.Name)
assert.Equal(t, 25, user.Age)
assert.Equal(t, "alice@example.com", user.Email)
})
t.Run("accumulates all validation errors", func(t *testing.T) {
validateName := func(name string) Validation[string] {
if len(name) < 3 {
return Failures[string](Errors{&ValidationError{Messsage: "Name must be at least 3 characters"}})
}
return Success(name)
}
validateAge := func(age int) Validation[int] {
if age < 18 {
return Failures[int](Errors{&ValidationError{Messsage: "Must be 18 or older"}})
}
return Success(age)
}
validateEmail := func(email string) Validation[string] {
if len(email) == 0 {
return Failures[string](Errors{&ValidationError{Messsage: "Email is required"}})
}
return Success(email)
}
makeUser := func(name string) func(int) func(string) User {
return func(age int) func(string) User {
return func(email string) User {
return User{Name: name, Age: age, Email: email}
}
}
}
// All validations fail
name := validateName("ab")
age := validateAge(16)
email := validateEmail("")
// Use the standalone Ap function with proper type parameters
result := Ap[User](email)(Ap[func(string) User](age)(Ap[func(int) func(string) User](name)(Of(makeUser))))
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(User) Errors { return nil },
)
assert.Len(t, errors, 3)
messages := make([]string, len(errors))
for i, err := range errors {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "Name must be at least 3 characters")
assert.Contains(t, messages, "Must be 18 or older")
assert.Contains(t, messages, "Email is required")
})
}

View File

@@ -0,0 +1,54 @@
package validation
import (
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
M "github.com/IBM/fp-go/v2/monoid"
)
// ErrorsMonoid returns a Monoid instance for Errors (array of ValidationError pointers).
// The monoid concatenates error arrays, with an empty array as the identity element.
// This is used internally by the applicative operations to accumulate validation errors.
//
// Example:
//
// m := ErrorsMonoid()
// combined := m.Concat(errors1, errors2) // Concatenates both error arrays
// empty := m.Empty() // Returns empty error array
func ErrorsMonoid() Monoid[Errors] {
return A.Monoid[*ValidationError]()
}
// ApplicativeMonoid creates a Monoid instance for Validation[A] given a Monoid for A.
// This allows combining validation results where the success values are also combined
// using the provided monoid. If any validation fails, all errors are accumulated.
//
// The resulting monoid:
// - Empty: Returns a successful validation with the empty value from the inner monoid
// - Concat: Combines two validations:
// - Both success: Combines values using the inner monoid
// - Any failure: Accumulates all errors
//
// Example:
//
// import "github.com/IBM/fp-go/v2/string"
//
// // Create a monoid for validations of strings
// m := ApplicativeMonoid(string.Monoid)
//
// v1 := Success("Hello")
// v2 := Success(" World")
// combined := m.Concat(v1, v2) // Success("Hello World")
//
// v3 := Failures[string](someErrors)
// failed := m.Concat(v1, v3) // Failures with accumulated errors
func ApplicativeMonoid[A any](m Monoid[A]) Monoid[Validation[A]] {
return M.ApplicativeMonoid(
Of,
either.MonadMap,
either.MonadApV[A, A](ErrorsMonoid()),
m,
)
}

View File

@@ -0,0 +1,353 @@
package validation
import (
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
MO "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestErrorsMonoid(t *testing.T) {
m := ErrorsMonoid()
t.Run("empty returns empty array", func(t *testing.T) {
empty := m.Empty()
assert.NotNil(t, empty)
assert.Len(t, empty, 0)
})
t.Run("concat combines error arrays", func(t *testing.T) {
errs1 := Errors{
&ValidationError{Messsage: "error 1"},
&ValidationError{Messsage: "error 2"},
}
errs2 := Errors{
&ValidationError{Messsage: "error 3"},
}
result := m.Concat(errs1, errs2)
assert.Len(t, result, 3)
assert.Equal(t, "error 1", result[0].Messsage)
assert.Equal(t, "error 2", result[1].Messsage)
assert.Equal(t, "error 3", result[2].Messsage)
})
t.Run("concat with empty preserves errors", func(t *testing.T) {
errs := Errors{
&ValidationError{Messsage: "error"},
}
empty := m.Empty()
result1 := m.Concat(errs, empty)
result2 := m.Concat(empty, errs)
assert.Equal(t, errs, result1)
assert.Equal(t, errs, result2)
})
t.Run("concat is associative", func(t *testing.T) {
errs1 := Errors{&ValidationError{Messsage: "1"}}
errs2 := Errors{&ValidationError{Messsage: "2"}}
errs3 := Errors{&ValidationError{Messsage: "3"}}
// (a + b) + c
left := m.Concat(m.Concat(errs1, errs2), errs3)
// a + (b + c)
right := m.Concat(errs1, m.Concat(errs2, errs3))
assert.Len(t, left, 3)
assert.Len(t, right, 3)
for i := 0; i < 3; i++ {
assert.Equal(t, left[i].Messsage, right[i].Messsage)
}
})
}
func TestApplicativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := ApplicativeMonoid(S.Monoid)
t.Run("empty returns successful validation with empty string", func(t *testing.T) {
empty := m.Empty()
assert.True(t, either.IsRight(empty))
value := either.MonadFold(empty,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
})
t.Run("concat combines successful validations", func(t *testing.T) {
v1 := Success("Hello")
v2 := Success(" World")
result := m.Concat(v1, v2)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "Hello World", value)
})
t.Run("concat with failure returns failure", func(t *testing.T) {
v1 := Success("Hello")
v2 := Failures[string](Errors{
&ValidationError{Messsage: "error"},
})
result := m.Concat(v1, v2)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 1)
assert.Equal(t, "error", errors[0].Messsage)
})
t.Run("concat accumulates all errors from both failures", func(t *testing.T) {
v1 := Failures[string](Errors{
&ValidationError{Messsage: "error 1"},
})
v2 := Failures[string](Errors{
&ValidationError{Messsage: "error 2"},
})
result := m.Concat(v1, v2)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 2)
messages := []string{errors[0].Messsage, errors[1].Messsage}
assert.Contains(t, messages, "error 1")
assert.Contains(t, messages, "error 2")
})
t.Run("concat with empty preserves validation", func(t *testing.T) {
v := Success("test")
empty := m.Empty()
result1 := m.Concat(v, empty)
result2 := m.Concat(empty, v)
val1 := either.MonadFold(result1,
func(Errors) string { return "" },
F.Identity[string],
)
val2 := either.MonadFold(result2,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "test", val1)
assert.Equal(t, "test", val2)
})
})
t.Run("with int addition monoid", func(t *testing.T) {
intMonoid := MO.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
m := ApplicativeMonoid(intMonoid)
t.Run("empty returns zero", func(t *testing.T) {
empty := m.Empty()
value := either.MonadFold(empty,
func(Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("concat adds values", func(t *testing.T) {
v1 := Success(10)
v2 := Success(32)
result := m.Concat(v1, v2)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("multiple concat operations", func(t *testing.T) {
v1 := Success(1)
v2 := Success(2)
v3 := Success(3)
v4 := Success(4)
result := m.Concat(m.Concat(m.Concat(v1, v2), v3), v4)
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 10, value)
})
})
}
func TestMonoidLaws(t *testing.T) {
t.Run("ErrorsMonoid satisfies monoid laws", func(t *testing.T) {
m := ErrorsMonoid()
errs1 := Errors{&ValidationError{Messsage: "1"}}
errs2 := Errors{&ValidationError{Messsage: "2"}}
t.Run("left identity", func(t *testing.T) {
// empty + a = a
result := m.Concat(m.Empty(), errs1)
assert.Equal(t, errs1, result)
})
t.Run("right identity", func(t *testing.T) {
// a + empty = a
result := m.Concat(errs1, m.Empty())
assert.Equal(t, errs1, result)
})
t.Run("associativity", func(t *testing.T) {
errs3 := Errors{&ValidationError{Messsage: "3"}}
// (a + b) + c = a + (b + c)
left := m.Concat(m.Concat(errs1, errs2), errs3)
right := m.Concat(errs1, m.Concat(errs2, errs3))
assert.Len(t, left, 3)
assert.Len(t, right, 3)
for i := 0; i < 3; i++ {
assert.Equal(t, left[i].Messsage, right[i].Messsage)
}
})
})
t.Run("ApplicativeMonoid satisfies monoid laws", func(t *testing.T) {
m := ApplicativeMonoid(S.Monoid)
v1 := Success("a")
v2 := Success("b")
t.Run("left identity", func(t *testing.T) {
// empty + a = a
result := m.Concat(m.Empty(), v1)
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("right identity", func(t *testing.T) {
// a + empty = a
result := m.Concat(v1, m.Empty())
value := either.MonadFold(result,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "a", value)
})
t.Run("associativity", func(t *testing.T) {
v3 := Success("c")
// (a + b) + c = a + (b + c)
left := m.Concat(m.Concat(v1, v2), v3)
right := m.Concat(v1, m.Concat(v2, v3))
leftVal := either.MonadFold(left,
func(Errors) string { return "" },
F.Identity[string],
)
rightVal := either.MonadFold(right,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "abc", leftVal)
assert.Equal(t, "abc", rightVal)
})
})
}
func TestApplicativeMonoidWithFailures(t *testing.T) {
m := ApplicativeMonoid(S.Monoid)
t.Run("failure propagates through concat", func(t *testing.T) {
v1 := Success("a")
v2 := Failures[string](Errors{&ValidationError{Messsage: "error"}})
v3 := Success("c")
result := m.Concat(m.Concat(v1, v2), v3)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 1)
})
t.Run("multiple failures accumulate", func(t *testing.T) {
v1 := Failures[string](Errors{&ValidationError{Messsage: "error 1"}})
v2 := Failures[string](Errors{&ValidationError{Messsage: "error 2"}})
v3 := Failures[string](Errors{&ValidationError{Messsage: "error 3"}})
result := m.Concat(m.Concat(v1, v2), v3)
errors := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
assert.Len(t, errors, 3)
})
}
func TestApplicativeMonoidEdgeCases(t *testing.T) {
t.Run("with custom struct monoid", func(t *testing.T) {
type Counter struct{ Count int }
counterMonoid := MO.MakeMonoid(
func(a, b Counter) Counter { return Counter{Count: a.Count + b.Count} },
Counter{Count: 0},
)
m := ApplicativeMonoid(counterMonoid)
v1 := Success(Counter{Count: 5})
v2 := Success(Counter{Count: 10})
result := m.Concat(v1, v2)
value := either.MonadFold(result,
func(Errors) Counter { return Counter{} },
F.Identity[Counter],
)
assert.Equal(t, 15, value.Count)
})
t.Run("empty concat empty", func(t *testing.T) {
m := ApplicativeMonoid(S.Monoid)
result := m.Concat(m.Empty(), m.Empty())
value := either.MonadFold(result,
func(Errors) string { return "ERROR" },
F.Identity[string],
)
assert.Equal(t, "", value)
})
}

View File

@@ -0,0 +1,49 @@
package validation
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/reader"
)
type (
// Either represents a value that can be one of two types: Left (error) or Right (success).
Either[E, A any] = either.Either[E, A]
// ContextEntry represents a single entry in the validation context path.
// It tracks the location and type information during nested validation.
ContextEntry struct {
Key string // The key or field name (for objects/maps)
Type string // The expected type name
Actual any // The actual value being validated
}
// Context is a stack of ContextEntry values representing the path through
// nested structures during validation. Used to provide detailed error messages.
Context = []ContextEntry
// ValidationError represents a single validation failure with context.
ValidationError struct {
Value any // The value that failed validation
Context Context // The path to the value in nested structures
Messsage string // Human-readable error message
Cause error
}
// Errors is a collection of validation errors.
Errors = []*ValidationError
// Validation represents the result of a validation operation.
// Left contains validation errors, Right contains the successfully validated value.
Validation[A any] = Either[Errors, A]
// Reader represents a computation that depends on an environment R and produces a value A.
Reader[R, A any] = reader.Reader[R, A]
Kleisli[A, B any] = Reader[A, Validation[B]]
Operator[A, B any] = Kleisli[Validation[A], B]
Monoid[A any] = monoid.Monoid[A]
)

View File

@@ -0,0 +1,125 @@
package validation
import (
"fmt"
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/either"
)
// Error implements the error interface for ValidationError.
// Returns a generic error message indicating this is a validation error.
// For detailed error information, use String() or Format() methods.
// Error implements the error interface for ValidationError.
// Returns a generic error message.
func (v *ValidationError) Error() string {
return "ValidationError"
}
// Unwrap returns the underlying cause error if present.
// This allows ValidationError to work with errors.Is and errors.As.
func (v *ValidationError) Unwrap() error {
return v.Cause
}
// String returns a simple string representation of the validation error.
// Returns the error message prefixed with "ValidationError: ".
func (v *ValidationError) String() string {
return fmt.Sprintf("ValidationError: %s", v.Messsage)
}
// Format implements fmt.Formatter for custom formatting of ValidationError.
// It includes the context path, message, and optionally the cause error.
// Supports verbs: %s, %v, %+v (with additional details)
func (v *ValidationError) Format(s fmt.State, verb rune) {
// Build the context path
path := ""
for i, entry := range v.Context {
if i > 0 {
path += "."
}
if entry.Key != "" {
path += entry.Key
} else {
path += entry.Type
}
}
// Start with the path if available
result := ""
if path != "" {
result = fmt.Sprintf("at %s: ", path)
}
// Add the message
result += v.Messsage
// Add the cause if present
if v.Cause != nil {
if s.Flag('+') && verb == 'v' {
// Verbose format with detailed cause
result += fmt.Sprintf("\n caused by: %+v", v.Cause)
} else {
result += fmt.Sprintf(" (caused by: %v)", v.Cause)
}
}
// Add value information for verbose format
if s.Flag('+') && verb == 'v' {
result += fmt.Sprintf("\n value: %#v", v.Value)
}
fmt.Fprint(s, result)
}
// Failures creates a validation failure from a collection of errors.
// Returns a Left Either containing the errors.
func Failures[T any](err Errors) Validation[T] {
return either.Left[T](err)
}
// FailureWithMessage creates a validation failure with a custom message.
// Returns a Reader that takes a Context and produces a Validation[T] failure.
// This is useful for creating context-aware validation errors.
//
// Example:
//
// fail := FailureWithMessage[int]("abc", "expected integer")
// result := fail([]ContextEntry{{Key: "age", Type: "int"}})
func FailureWithMessage[T any](value any, message string) Reader[Context, Validation[T]] {
return func(context Context) Validation[T] {
return Failures[T](A.Of(&ValidationError{
Value: value,
Context: context,
Messsage: message,
}))
}
}
// FailureWithError creates a validation failure with a custom message and underlying cause.
// Returns a Reader that takes an error, then a Context, and produces a Validation[T] failure.
// This is useful for wrapping errors from other operations while maintaining validation context.
//
// Example:
//
// fail := FailureWithError[int]("abc", "parse failed")
// result := fail(parseErr)([]ContextEntry{{Key: "count", Type: "int"}})
func FailureWithError[T any](value any, message string) Reader[error, Reader[Context, Validation[T]]] {
return func(err error) Reader[Context, Validation[T]] {
return func(context Context) Validation[T] {
return Failures[T](A.Of(&ValidationError{
Value: value,
Context: context,
Messsage: message,
Cause: err,
}))
}
}
}
// Success creates a successful validation result.
// Returns a Right Either containing the validated value.
func Success[T any](value T) Validation[T] {
return either.Of[Errors](value)
}

View File

@@ -0,0 +1,419 @@
package validation
import (
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidationError_Error(t *testing.T) {
err := &ValidationError{
Value: "test",
Messsage: "invalid value",
}
assert.Equal(t, "ValidationError", err.Error())
}
func TestValidationError_String(t *testing.T) {
err := &ValidationError{
Value: "test",
Messsage: "invalid value",
}
expected := "ValidationError: invalid value"
assert.Equal(t, expected, err.String())
}
func TestValidationError_Unwrap(t *testing.T) {
t.Run("with cause", func(t *testing.T) {
cause := errors.New("underlying error")
err := &ValidationError{
Value: "test",
Messsage: "invalid value",
Cause: cause,
}
assert.Equal(t, cause, err.Unwrap())
})
t.Run("without cause", func(t *testing.T) {
err := &ValidationError{
Value: "test",
Messsage: "invalid value",
}
assert.Nil(t, err.Unwrap())
})
}
func TestValidationError_Format(t *testing.T) {
t.Run("simple format without context", func(t *testing.T) {
err := &ValidationError{
Value: "test",
Messsage: "invalid value",
}
result := fmt.Sprintf("%v", err)
assert.Equal(t, "invalid value", result)
})
t.Run("with context path", func(t *testing.T) {
err := &ValidationError{
Value: "test",
Context: []ContextEntry{{Key: "user"}, {Key: "name"}},
Messsage: "must not be empty",
}
result := fmt.Sprintf("%v", err)
assert.Equal(t, "at user.name: must not be empty", result)
})
t.Run("with context using type", func(t *testing.T) {
err := &ValidationError{
Value: 123,
Context: []ContextEntry{{Type: "User"}, {Key: "age"}},
Messsage: "must be positive",
}
result := fmt.Sprintf("%v", err)
assert.Equal(t, "at User.age: must be positive", result)
})
t.Run("with cause - simple format", func(t *testing.T) {
cause := errors.New("parse error")
err := &ValidationError{
Value: "abc",
Messsage: "invalid number",
Cause: cause,
}
result := fmt.Sprintf("%v", err)
assert.Equal(t, "invalid number (caused by: parse error)", result)
})
t.Run("with cause - verbose format", func(t *testing.T) {
cause := errors.New("parse error")
err := &ValidationError{
Value: "abc",
Messsage: "invalid number",
Cause: cause,
}
result := fmt.Sprintf("%+v", err)
assert.Contains(t, result, "invalid number")
assert.Contains(t, result, "caused by: parse error")
assert.Contains(t, result, `value: "abc"`)
})
t.Run("verbose format shows value", func(t *testing.T) {
err := &ValidationError{
Value: 42,
Messsage: "out of range",
}
result := fmt.Sprintf("%+v", err)
assert.Contains(t, result, "out of range")
assert.Contains(t, result, "value: 42")
})
t.Run("complex context path", func(t *testing.T) {
err := &ValidationError{
Value: "invalid",
Context: []ContextEntry{
{Key: "user"},
{Key: "address"},
{Key: "zipCode"},
},
Messsage: "invalid format",
}
result := fmt.Sprintf("%v", err)
assert.Equal(t, "at user.address.zipCode: invalid format", result)
})
}
func TestFailures(t *testing.T) {
t.Run("creates left either with errors", func(t *testing.T) {
errs := Errors{
&ValidationError{Value: "test", Messsage: "error 1"},
&ValidationError{Value: "test", Messsage: "error 2"},
}
result := Failures[int](errs)
assert.True(t, either.IsLeft(result))
left := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
assert.Len(t, left, 2)
assert.Equal(t, "error 1", left[0].Messsage)
assert.Equal(t, "error 2", left[1].Messsage)
})
t.Run("preserves error details", func(t *testing.T) {
errs := Errors{
&ValidationError{
Value: "abc",
Context: []ContextEntry{{Key: "field"}},
Messsage: "invalid",
Cause: errors.New("cause"),
},
}
result := Failures[string](errs)
left := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
require.Len(t, left, 1)
assert.Equal(t, "abc", left[0].Value)
assert.Equal(t, "invalid", left[0].Messsage)
assert.NotNil(t, left[0].Cause)
assert.Len(t, left[0].Context, 1)
})
}
func TestSuccess(t *testing.T) {
t.Run("creates right either with value", func(t *testing.T) {
result := Success(42)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("works with different types", func(t *testing.T) {
strResult := Success("hello")
str := either.MonadFold(strResult,
func(Errors) string { return "" },
F.Identity[string],
)
assert.Equal(t, "hello", str)
boolResult := Success(true)
b := either.MonadFold(boolResult,
func(Errors) bool { return false },
F.Identity[bool],
)
assert.Equal(t, true, b)
type Custom struct{ Name string }
customResult := Success(Custom{Name: "test"})
custom := either.MonadFold(customResult,
func(Errors) Custom { return Custom{} },
F.Identity[Custom],
)
assert.Equal(t, "test", custom.Name)
})
}
func TestFailureWithMessage(t *testing.T) {
t.Run("creates failure with context", func(t *testing.T) {
fail := FailureWithMessage[int]("abc", "expected integer")
context := []ContextEntry{{Key: "age", Type: "int"}}
result := fail(context)
assert.True(t, either.IsLeft(result))
errs := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
require.Len(t, errs, 1)
assert.Equal(t, "abc", errs[0].Value)
assert.Equal(t, "expected integer", errs[0].Messsage)
assert.Equal(t, context, errs[0].Context)
assert.Nil(t, errs[0].Cause)
})
t.Run("works with empty context", func(t *testing.T) {
fail := FailureWithMessage[string](123, "wrong type")
result := fail(nil)
errs := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
require.Len(t, errs, 1)
assert.Equal(t, 123, errs[0].Value)
assert.Nil(t, errs[0].Context)
})
t.Run("preserves complex context", func(t *testing.T) {
fail := FailureWithMessage[bool]("not a bool", "type mismatch")
context := []ContextEntry{
{Key: "user"},
{Key: "settings"},
{Key: "enabled"},
}
result := fail(context)
errs := either.MonadFold(result,
F.Identity[Errors],
func(bool) Errors { return nil },
)
require.Len(t, errs, 1)
assert.Equal(t, context, errs[0].Context)
})
}
func TestFailureWithError(t *testing.T) {
t.Run("creates failure with cause and context", func(t *testing.T) {
cause := errors.New("parse failed")
fail := FailureWithError[int]("abc", "invalid number")
context := []ContextEntry{{Key: "count"}}
result := fail(cause)(context)
assert.True(t, either.IsLeft(result))
errs := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
require.Len(t, errs, 1)
assert.Equal(t, "abc", errs[0].Value)
assert.Equal(t, "invalid number", errs[0].Messsage)
assert.Equal(t, context, errs[0].Context)
assert.Equal(t, cause, errs[0].Cause)
})
t.Run("cause is unwrappable", func(t *testing.T) {
cause := errors.New("underlying")
fail := FailureWithError[string](nil, "wrapper")
result := fail(cause)(nil)
errs := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
require.Len(t, errs, 1)
assert.True(t, errors.Is(errs[0], cause))
})
t.Run("works with complex error chain", func(t *testing.T) {
root := errors.New("root cause")
wrapped := fmt.Errorf("wrapped: %w", root)
fail := FailureWithError[int](0, "validation failed")
result := fail(wrapped)([]ContextEntry{{Key: "field"}})
errs := either.MonadFold(result,
F.Identity[Errors],
func(int) Errors { return nil },
)
require.Len(t, errs, 1)
assert.True(t, errors.Is(errs[0], root))
assert.True(t, errors.Is(errs[0], wrapped))
})
}
func TestValidationIntegration(t *testing.T) {
t.Run("success and failure can be combined", func(t *testing.T) {
success := Success(42)
failure := Failures[int](Errors{
&ValidationError{Value: "bad", Messsage: "error"},
})
assert.True(t, either.IsRight(success))
assert.True(t, either.IsLeft(failure))
})
t.Run("context provides meaningful error paths", func(t *testing.T) {
fail := FailureWithMessage[string](nil, "required field")
context := []ContextEntry{
{Key: "request"},
{Key: "body"},
{Key: "user"},
{Key: "email"},
}
result := fail(context)
errs := either.MonadFold(result,
F.Identity[Errors],
func(string) Errors { return nil },
)
formatted := fmt.Sprintf("%v", errs[0])
assert.Contains(t, formatted, "request.body.user.email")
assert.Contains(t, formatted, "required field")
})
t.Run("multiple errors can be collected", func(t *testing.T) {
errs := Errors{
&ValidationError{
Context: []ContextEntry{{Key: "name"}},
Messsage: "too short",
},
&ValidationError{
Context: []ContextEntry{{Key: "age"}},
Messsage: "must be positive",
},
&ValidationError{
Context: []ContextEntry{{Key: "email"}},
Messsage: "invalid format",
},
}
result := Failures[any](errs)
collected := either.MonadFold(result,
F.Identity[Errors],
func(any) Errors { return nil },
)
assert.Len(t, collected, 3)
messages := make([]string, len(collected))
for i, err := range collected {
messages[i] = err.Messsage
}
assert.Contains(t, messages, "too short")
assert.Contains(t, messages, "must be positive")
assert.Contains(t, messages, "invalid format")
})
}
func TestValidationError_FormatEdgeCases(t *testing.T) {
t.Run("empty message", func(t *testing.T) {
err := &ValidationError{
Value: "test",
Messsage: "",
}
result := fmt.Sprintf("%v", err)
assert.Equal(t, "", result)
})
t.Run("context with empty keys", func(t *testing.T) {
err := &ValidationError{
Value: "test",
Context: []ContextEntry{{Key: ""}, {Type: "Type"}, {Key: ""}},
Messsage: "error",
}
result := fmt.Sprintf("%v", err)
// Should handle empty keys gracefully
assert.Contains(t, result, "error")
})
t.Run("nil value", func(t *testing.T) {
err := &ValidationError{
Value: nil,
Messsage: "nil not allowed",
}
result := fmt.Sprintf("%+v", err)
assert.Contains(t, result, "nil not allowed")
assert.Contains(t, result, "value: <nil>")
})
}

View File

@@ -0,0 +1,11 @@
package decoder
import (
"github.com/IBM/fp-go/v2/result"
)
type (
Result[A any] = result.Result[A]
Decoder[I, A any] = result.Kleisli[I, A]
)

View File

@@ -0,0 +1,7 @@
package encoder
import "github.com/IBM/fp-go/v2/reader"
type (
Encoder[O, A any] = reader.Reader[A, O]
)

View File

@@ -262,7 +262,7 @@ func imap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](sa Prism[S, A], ab AB,
//
// intPrism := MakePrism(...) // Prism[Result, int]
// stringPrism := IMap[Result](
// func(n int) string { return strconv.Itoa(n) },
// strconv.Itoa,
// func(s string) int { n, _ := strconv.Atoi(s); return n },
// )(intPrism) // Prism[Result, string]
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[S, A, B] {

View File

@@ -59,8 +59,5 @@ func Compose[
return G.Compose[
G.Traversal[A, B, HKTA, HKTB],
G.Traversal[S, A, HKTS, HKTA],
G.Traversal[S, B, HKTS, HKTB],
S, A, B,
HKTS, HKTA, HKTB,
](ab)
G.Traversal[S, B, HKTS, HKTB]](ab)
}

View File

@@ -475,3 +475,41 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
return None[B]()
}
}
// Zero returns the zero value of an [Option], which is None.
// This function is useful as an identity element in monoid operations or for creating an empty Option.
//
// The zero value for Option[A] is always None, representing the absence of a value.
// This is consistent with the Option monad's semantics where None represents "no value"
// and Some represents "a value".
//
// Important: Zero() returns the same value as the default initialization of Option[A].
// When you declare `var o Option[A]` without initialization, it has the same value as Zero[A]().
//
// Note: Unlike other types where zero might be a default value, Option's zero is explicitly
// the absence of any value (None), not Some with a zero value.
//
// Example:
//
// // Zero Option of any type is always None
// o1 := option.Zero[int]() // None
// o2 := option.Zero[string]() // None
// o3 := option.Zero[*int]() // None
//
// // Zero equals default initialization
// var defaultInit Option[int]
// zero := option.Zero[int]()
// assert.Equal(t, defaultInit, zero) // true
//
// // Verify it's None
// o := option.Zero[int]()
// assert.True(t, option.IsNone(o)) // true
// assert.False(t, option.IsSome(o)) // false
//
// // Different from Some with zero value
// someZero := option.Some(0) // Some(0)
// zero := option.Zero[int]() // None
// assert.NotEqual(t, someZero, zero) // they are different
func Zero[A any]() Option[A] {
return None[A]()
}

View File

@@ -174,3 +174,199 @@ func TestAlt(t *testing.T) {
assert.Equal(t, Some(1), F.Pipe1(None[int](), Alt(F.Constant(Some(1)))))
assert.Equal(t, None[int](), F.Pipe1(None[int](), Alt(F.Constant(None[int]()))))
}
// TestZeroWithIntegers tests Zero function with integer types
func TestZeroWithIntegers(t *testing.T) {
o := Zero[int]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithStrings tests Zero function with string types
func TestZeroWithStrings(t *testing.T) {
o := Zero[string]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithBooleans tests Zero function with boolean types
func TestZeroWithBooleans(t *testing.T) {
o := Zero[bool]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithFloats tests Zero function with float types
func TestZeroWithFloats(t *testing.T) {
o := Zero[float64]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithPointers tests Zero function with pointer types
func TestZeroWithPointers(t *testing.T) {
o := Zero[*int]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithSlices tests Zero function with slice types
func TestZeroWithSlices(t *testing.T) {
o := Zero[[]int]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithMaps tests Zero function with map types
func TestZeroWithMaps(t *testing.T) {
o := Zero[map[string]int]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithStructs tests Zero function with struct types
func TestZeroWithStructs(t *testing.T) {
type TestStruct struct {
Field1 int
Field2 string
}
o := Zero[TestStruct]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithInterfaces tests Zero function with interface types
func TestZeroWithInterfaces(t *testing.T) {
o := Zero[interface{}]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroIsNotSomeWithZeroValue tests that Zero is different from Some(zero value)
func TestZeroIsNotSomeWithZeroValue(t *testing.T) {
// Zero returns None
zero := Zero[int]()
assert.True(t, IsNone(zero), "Zero should be None")
// Some with zero value is different
someZero := Some(0)
assert.True(t, IsSome(someZero), "Some(0) should be Some")
// They are not equal
assert.NotEqual(t, zero, someZero, "Zero (None) should not equal Some(0)")
}
// TestZeroCanBeUsedWithOtherFunctions tests that Zero Options work with other option functions
func TestZeroCanBeUsedWithOtherFunctions(t *testing.T) {
o := Zero[int]()
// Test with Map - should remain None
mapped := MonadMap(o, func(n int) string {
return fmt.Sprintf("%d", n)
})
assert.True(t, IsNone(mapped), "Mapped Zero should still be None")
// Test with Chain - should remain None
chained := MonadChain(o, func(n int) Option[string] {
return Some(fmt.Sprintf("value: %d", n))
})
assert.True(t, IsNone(chained), "Chained Zero should still be None")
// Test with Fold - should use onNone branch
folded := MonadFold(o,
func() string { return "none" },
func(n int) string { return fmt.Sprintf("some: %d", n) },
)
assert.Equal(t, "none", folded, "Folded Zero should use onNone branch")
// Test with GetOrElse
value := GetOrElse(func() int { return 42 })(o)
assert.Equal(t, 42, value, "GetOrElse on Zero should return default value")
}
// TestZeroEquality tests that multiple Zero calls produce equal Options
func TestZeroEquality(t *testing.T) {
o1 := Zero[int]()
o2 := Zero[int]()
assert.Equal(t, IsNone(o1), IsNone(o2), "Both should be None")
assert.Equal(t, IsSome(o1), IsSome(o2), "Both should not be Some")
assert.Equal(t, o1, o2, "Zero values should be equal")
}
// TestZeroWithComplexTypes tests Zero with more complex nested types
func TestZeroWithComplexTypes(t *testing.T) {
type ComplexType struct {
Nested map[string][]int
Ptr *string
}
o := Zero[ComplexType]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroWithNestedOption tests Zero with nested Option type
func TestZeroWithNestedOption(t *testing.T) {
o := Zero[Option[int]]()
assert.True(t, IsNone(o), "Zero should create a None value")
assert.False(t, IsSome(o), "Zero should not create a Some value")
}
// TestZeroIsAlwaysNone tests that Zero never creates a Some value
func TestZeroIsAlwaysNone(t *testing.T) {
// Test with various types
o1 := Zero[int]()
o2 := Zero[string]()
o3 := Zero[bool]()
o4 := Zero[*int]()
o5 := Zero[[]string]()
assert.True(t, IsNone(o1), "Zero should always be None")
assert.True(t, IsNone(o2), "Zero should always be None")
assert.True(t, IsNone(o3), "Zero should always be None")
assert.True(t, IsNone(o4), "Zero should always be None")
assert.True(t, IsNone(o5), "Zero should always be None")
assert.False(t, IsSome(o1), "Zero should never be Some")
assert.False(t, IsSome(o2), "Zero should never be Some")
assert.False(t, IsSome(o3), "Zero should never be Some")
assert.False(t, IsSome(o4), "Zero should never be Some")
assert.False(t, IsSome(o5), "Zero should never be Some")
}
// TestZeroEqualsNone tests that Zero is equivalent to None
func TestZeroEqualsNone(t *testing.T) {
zero := Zero[int]()
none := None[int]()
assert.Equal(t, zero, none, "Zero should be equal to None")
assert.Equal(t, IsNone(zero), IsNone(none), "Both should be None")
assert.Equal(t, IsSome(zero), IsSome(none), "Both should not be Some")
}
// TestZeroEqualsDefaultInitialization tests that Zero returns the same value as default initialization
func TestZeroEqualsDefaultInitialization(t *testing.T) {
// Default initialization of Option
var defaultInit Option[int]
// Zero function
zero := Zero[int]()
// They should be equal
assert.Equal(t, defaultInit, zero, "Zero should equal default initialization")
assert.Equal(t, IsNone(defaultInit), IsNone(zero), "Both should be None")
assert.Equal(t, IsSome(defaultInit), IsSome(zero), "Both should not be Some")
}

320
v2/ord/monoid_test.go Normal file
View File

@@ -0,0 +1,320 @@
// 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 ord
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test Semigroup laws
func TestSemigroup_Associativity(t *testing.T) {
type Person struct {
LastName string
FirstName string
MiddleName string
}
stringOrd := FromStrictCompare[string]()
byLastName := Contramap(func(p Person) string { return p.LastName })(stringOrd)
byFirstName := Contramap(func(p Person) string { return p.FirstName })(stringOrd)
byMiddleName := Contramap(func(p Person) string { return p.MiddleName })(stringOrd)
sg := Semigroup[Person]()
// Test associativity: (a <> b) <> c == a <> (b <> c)
left := sg.Concat(sg.Concat(byLastName, byFirstName), byMiddleName)
right := sg.Concat(byLastName, sg.Concat(byFirstName, byMiddleName))
p1 := Person{LastName: "Smith", FirstName: "John", MiddleName: "A"}
p2 := Person{LastName: "Smith", FirstName: "John", MiddleName: "B"}
assert.Equal(t, left.Compare(p1, p2), right.Compare(p1, p2), "Associativity should hold")
}
// Test Semigroup with three levels
func TestSemigroup_ThreeLevels(t *testing.T) {
type Employee struct {
Department string
Level int
Name string
}
stringOrd := FromStrictCompare[string]()
intOrd := FromStrictCompare[int]()
byDept := Contramap(func(e Employee) string { return e.Department })(stringOrd)
byLevel := Contramap(func(e Employee) int { return e.Level })(intOrd)
byName := Contramap(func(e Employee) string { return e.Name })(stringOrd)
sg := Semigroup[Employee]()
employeeOrd := sg.Concat(sg.Concat(byDept, byLevel), byName)
e1 := Employee{Department: "IT", Level: 3, Name: "Alice"}
e2 := Employee{Department: "IT", Level: 3, Name: "Bob"}
e3 := Employee{Department: "IT", Level: 2, Name: "Charlie"}
e4 := Employee{Department: "HR", Level: 3, Name: "David"}
// Same dept, same level, different name
assert.Equal(t, -1, employeeOrd.Compare(e1, e2), "Alice < Bob")
// Same dept, different level
assert.Equal(t, 1, employeeOrd.Compare(e1, e3), "Level 3 > Level 2")
// Different dept
assert.Equal(t, -1, employeeOrd.Compare(e4, e1), "HR < IT")
}
// Test Monoid identity laws
func TestMonoid_IdentityLaws(t *testing.T) {
m := Monoid[int]()
intOrd := FromStrictCompare[int]()
emptyOrd := m.Empty()
// Left identity: empty <> x == x
leftIdentity := m.Concat(emptyOrd, intOrd)
assert.Equal(t, -1, leftIdentity.Compare(3, 5), "Left identity: 3 < 5")
assert.Equal(t, 1, leftIdentity.Compare(5, 3), "Left identity: 5 > 3")
// Right identity: x <> empty == x
rightIdentity := m.Concat(intOrd, emptyOrd)
assert.Equal(t, -1, rightIdentity.Compare(3, 5), "Right identity: 3 < 5")
assert.Equal(t, 1, rightIdentity.Compare(5, 3), "Right identity: 5 > 3")
}
// Test Monoid with multiple empty concatenations
func TestMonoid_MultipleEmpty(t *testing.T) {
m := Monoid[int]()
emptyOrd := m.Empty()
// Concatenating multiple empty orderings should still be empty
combined := m.Concat(m.Concat(emptyOrd, emptyOrd), emptyOrd)
assert.Equal(t, 0, combined.Compare(5, 3), "Multiple empties: always equal")
assert.Equal(t, 0, combined.Compare(3, 5), "Multiple empties: always equal")
assert.True(t, combined.Equals(5, 3), "Multiple empties: always equal")
}
// Test MaxSemigroup with edge cases
func TestMaxSemigroup_EdgeCases(t *testing.T) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
tests := []struct {
name string
a int
b int
expected int
}{
{"both positive", 5, 3, 5},
{"both negative", -5, -3, -3},
{"mixed signs", -5, 3, 3},
{"zero and positive", 0, 5, 5},
{"zero and negative", 0, -5, 0},
{"both zero", 0, 0, 0},
{"equal positive", 5, 5, 5},
{"equal negative", -5, -5, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := maxSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MinSemigroup with edge cases
func TestMinSemigroup_EdgeCases(t *testing.T) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
tests := []struct {
name string
a int
b int
expected int
}{
{"both positive", 5, 3, 3},
{"both negative", -5, -3, -5},
{"mixed signs", -5, 3, -5},
{"zero and positive", 0, 5, 0},
{"zero and negative", 0, -5, -5},
{"both zero", 0, 0, 0},
{"equal positive", 5, 5, 5},
{"equal negative", -5, -5, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := minSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MaxSemigroup with strings
func TestMaxSemigroup_Strings(t *testing.T) {
stringOrd := FromStrictCompare[string]()
maxSg := MaxSemigroup(stringOrd)
tests := []struct {
name string
a string
b string
expected string
}{
{"alphabetical", "apple", "banana", "banana"},
{"same string", "apple", "apple", "apple"},
{"empty and non-empty", "", "apple", "apple"},
{"both empty", "", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := maxSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MinSemigroup with strings
func TestMinSemigroup_Strings(t *testing.T) {
stringOrd := FromStrictCompare[string]()
minSg := MinSemigroup(stringOrd)
tests := []struct {
name string
a string
b string
expected string
}{
{"alphabetical", "apple", "banana", "apple"},
{"same string", "apple", "apple", "apple"},
{"empty and non-empty", "", "apple", ""},
{"both empty", "", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := minSg.Concat(tt.a, tt.b)
assert.Equal(t, tt.expected, result)
})
}
}
// Test MaxSemigroup associativity
func TestMaxSemigroup_Associativity(t *testing.T) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
// (a <> b) <> c == a <> (b <> c)
a, b, c := 5, 3, 7
left := maxSg.Concat(maxSg.Concat(a, b), c)
right := maxSg.Concat(a, maxSg.Concat(b, c))
assert.Equal(t, left, right, "MaxSemigroup should be associative")
assert.Equal(t, 7, left, "Should return maximum value")
}
// Test MinSemigroup associativity
func TestMinSemigroup_Associativity(t *testing.T) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
// (a <> b) <> c == a <> (b <> c)
a, b, c := 5, 3, 7
left := minSg.Concat(minSg.Concat(a, b), c)
right := minSg.Concat(a, minSg.Concat(b, c))
assert.Equal(t, left, right, "MinSemigroup should be associative")
assert.Equal(t, 3, left, "Should return minimum value")
}
// Test Semigroup with reversed ordering
func TestSemigroup_WithReverse(t *testing.T) {
type Person struct {
Age int
Name string
}
intOrd := FromStrictCompare[int]()
stringOrd := FromStrictCompare[string]()
// Order by age descending, then by name ascending
byAge := Contramap(func(p Person) int { return p.Age })(Reverse(intOrd))
byName := Contramap(func(p Person) string { return p.Name })(stringOrd)
sg := Semigroup[Person]()
personOrd := sg.Concat(byAge, byName)
p1 := Person{Age: 30, Name: "Alice"}
p2 := Person{Age: 30, Name: "Bob"}
p3 := Person{Age: 25, Name: "Charlie"}
// Same age, different name
assert.Equal(t, -1, personOrd.Compare(p1, p2), "Alice < Bob (same age)")
// Different age (descending)
assert.Equal(t, -1, personOrd.Compare(p1, p3), "30 > 25 (descending)")
}
// Benchmark MaxSemigroup
func BenchmarkMaxSemigroup(b *testing.B) {
intOrd := FromStrictCompare[int]()
maxSg := MaxSemigroup(intOrd)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = maxSg.Concat(i, i+1)
}
}
// Benchmark MinSemigroup
func BenchmarkMinSemigroup(b *testing.B) {
intOrd := FromStrictCompare[int]()
minSg := MinSemigroup(intOrd)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = minSg.Concat(i, i+1)
}
}
// Benchmark Semigroup concatenation
func BenchmarkSemigroup_Concat(b *testing.B) {
type Person struct {
LastName string
FirstName string
}
stringOrd := FromStrictCompare[string]()
byLastName := Contramap(func(p Person) string { return p.LastName })(stringOrd)
byFirstName := Contramap(func(p Person) string { return p.FirstName })(stringOrd)
sg := Semigroup[Person]()
personOrd := sg.Concat(byLastName, byFirstName)
p1 := Person{LastName: "Smith", FirstName: "Alice"}
p2 := Person{LastName: "Smith", FirstName: "Bob"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = personOrd.Compare(p1, p2)
}
}

View File

@@ -156,6 +156,8 @@ func Reverse[T any](o Ord[T]) Ord[T] {
// This allows ordering values of type B by first transforming them to type A
// and then using the ordering for type A.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// Parameters:
// - f: A transformation function from B to A
//
@@ -169,7 +171,7 @@ func Reverse[T any](o Ord[T]) Ord[T] {
// return p.Age
// })(intOrd)
// // Now persons are ordered by age
func Contramap[A, B any](f func(B) A) func(Ord[A]) Ord[B] {
func Contramap[A, B any](f func(B) A) Operator[A, B] {
return func(o Ord[A]) Ord[B] {
return MakeOrd(func(x, y B) int {
return o.Compare(f(x), f(y))
@@ -371,6 +373,8 @@ func Between[A any](o Ord[A]) func(A, A) func(A) bool {
}
}
// compareTime is a helper function that compares two time.Time values.
// Returns -1 if a is before b, 1 if a is after b, and 0 if they are equal.
func compareTime(a, b time.Time) int {
if a.Before(b) {
return -1

59
v2/ord/types.go Normal file
View File

@@ -0,0 +1,59 @@
// 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 ord
type (
// Kleisli represents a function that takes a value of type A and returns an Ord[B].
// This is useful for creating orderings that depend on input values.
//
// Type Parameters:
// - A: The input type
// - B: The type for which ordering is produced
//
// Example:
//
// // Create a Kleisli that produces different orderings based on input
// var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
// if mode == "ascending" {
// return ord.FromStrictCompare[int]()
// }
// return ord.Reverse(ord.FromStrictCompare[int]())
// }
// ascOrd := orderingFactory("ascending")
// descOrd := orderingFactory("descending")
Kleisli[A, B any] = func(A) Ord[B]
// Operator represents a function that transforms an Ord[A] into a value of type B.
// This is commonly used for operations that modify or combine orderings.
//
// Type Parameters:
// - A: The type for which ordering is defined
// - B: The result type of the operation
//
// This is equivalent to Kleisli[Ord[A], B] and is used for operations like
// Contramap, which takes an Ord[A] and produces an Ord[B].
//
// Example:
//
// // Contramap is an Operator that transforms Ord[A] to Ord[B]
// type Person struct { Age int }
// var ageOperator Operator[int, Person] = ord.Contramap(func(p Person) int {
// return p.Age
// })
// intOrd := ord.FromStrictCompare[int]()
// personOrd := ageOperator(intOrd)
Operator[A, B any] = Kleisli[Ord[A], B]
)

203
v2/ord/types_test.go Normal file
View File

@@ -0,0 +1,203 @@
// 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 ord
import (
"testing"
"github.com/stretchr/testify/assert"
)
// Test Kleisli type
func TestKleisli(t *testing.T) {
// Create a Kleisli that produces different orderings based on input
var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
if mode == "ascending" {
return FromStrictCompare[int]()
}
return Reverse(FromStrictCompare[int]())
}
// Test ascending order
ascOrd := orderingFactory("ascending")
assert.Equal(t, -1, ascOrd.Compare(3, 5), "ascending: 3 < 5")
assert.Equal(t, 1, ascOrd.Compare(5, 3), "ascending: 5 > 3")
assert.Equal(t, 0, ascOrd.Compare(5, 5), "ascending: 5 == 5")
// Test descending order
descOrd := orderingFactory("descending")
assert.Equal(t, 1, descOrd.Compare(3, 5), "descending: 3 > 5")
assert.Equal(t, -1, descOrd.Compare(5, 3), "descending: 5 < 3")
assert.Equal(t, 0, descOrd.Compare(5, 5), "descending: 5 == 5")
}
// Test Kleisli with complex types
func TestKleisli_ComplexType(t *testing.T) {
type Person struct {
Name string
Age int
}
// Kleisli that creates orderings based on a field selector
var personOrderingFactory Kleisli[string, Person] = func(field string) Ord[Person] {
stringOrd := FromStrictCompare[string]()
intOrd := FromStrictCompare[int]()
switch field {
case "name":
return Contramap(func(p Person) string { return p.Name })(stringOrd)
case "age":
return Contramap(func(p Person) int { return p.Age })(intOrd)
default:
// Default to name ordering
return Contramap(func(p Person) string { return p.Name })(stringOrd)
}
}
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
// Order by name
nameOrd := personOrderingFactory("name")
assert.Equal(t, -1, nameOrd.Compare(p1, p2), "Alice < Bob by name")
// Order by age
ageOrd := personOrderingFactory("age")
assert.Equal(t, 1, ageOrd.Compare(p1, p2), "30 > 25 by age")
}
// Test Operator type
func TestOperator(t *testing.T) {
type Person struct {
Name string
Age int
}
// Operator that transforms Ord[int] to Ord[Person] by age
var ageOperator Operator[int, Person] = Contramap(func(p Person) int {
return p.Age
})
intOrd := FromStrictCompare[int]()
personOrd := ageOperator(intOrd)
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
p3 := Person{Name: "Charlie", Age: 30}
assert.Equal(t, 1, personOrd.Compare(p1, p2), "30 > 25")
assert.Equal(t, -1, personOrd.Compare(p2, p1), "25 < 30")
assert.Equal(t, 0, personOrd.Compare(p1, p3), "30 == 30")
assert.True(t, personOrd.Equals(p1, p3), "same age")
assert.False(t, personOrd.Equals(p1, p2), "different age")
}
// Test Operator composition
func TestOperator_Composition(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
// Create operators for different transformations
stringOrd := FromStrictCompare[string]()
// Operator to order Person by city
var cityOperator Operator[string, Person] = Contramap(func(p Person) string {
return p.Address.City
})
personOrd := cityOperator(stringOrd)
p1 := Person{Name: "Alice", Address: Address{Street: "Main St", City: "Boston"}}
p2 := Person{Name: "Bob", Address: Address{Street: "Oak Ave", City: "Austin"}}
assert.Equal(t, 1, personOrd.Compare(p1, p2), "Boston > Austin")
assert.Equal(t, -1, personOrd.Compare(p2, p1), "Austin < Boston")
}
// Test Operator with multiple transformations
func TestOperator_MultipleTransformations(t *testing.T) {
type Product struct {
Name string
Price float64
}
floatOrd := FromStrictCompare[float64]()
// Operator to order by price
var priceOperator Operator[float64, Product] = Contramap(func(p Product) float64 {
return p.Price
})
// Operator to reverse the ordering
var reverseOperator Operator[float64, Product] = func(o Ord[float64]) Ord[Product] {
return priceOperator(Reverse(o))
}
// Order by price descending
productOrd := reverseOperator(floatOrd)
prod1 := Product{Name: "Widget", Price: 19.99}
prod2 := Product{Name: "Gadget", Price: 29.99}
assert.Equal(t, 1, productOrd.Compare(prod1, prod2), "19.99 > 29.99 (reversed)")
assert.Equal(t, -1, productOrd.Compare(prod2, prod1), "29.99 < 19.99 (reversed)")
}
// Example test for Kleisli
func ExampleKleisli() {
// Create a Kleisli that produces different orderings based on input
var orderingFactory Kleisli[string, int] = func(mode string) Ord[int] {
if mode == "ascending" {
return FromStrictCompare[int]()
}
return Reverse(FromStrictCompare[int]())
}
ascOrd := orderingFactory("ascending")
descOrd := orderingFactory("descending")
println(ascOrd.Compare(5, 3)) // 1
println(descOrd.Compare(5, 3)) // -1
}
// Example test for Operator
func ExampleOperator() {
type Person struct {
Name string
Age int
}
// Operator that transforms Ord[int] to Ord[Person] by age
var ageOperator Operator[int, Person] = Contramap(func(p Person) int {
return p.Age
})
intOrd := FromStrictCompare[int]()
personOrd := ageOperator(intOrd)
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Bob", Age: 25}
result := personOrd.Compare(p1, p2)
println(result) // 1 (30 > 25)
}

View File

@@ -73,7 +73,7 @@ Map operations transform one or both values:
// Map both values
p4 := pair.MonadBiMap(p,
func(n int) string { return fmt.Sprintf("%d", n) },
func(s string) int { return len(s) },
S.Size,
) // Pair[string, int]{"5", 5}
Curried versions for composition:
@@ -91,7 +91,7 @@ Curried versions for composition:
// Compose multiple transformations
transform := F.Flow2(
pair.MapHead[string](N.Mul(2)),
pair.MapTail[int](func(s string) int { return len(s) }),
pair.MapTail[int](S.Size),
)
result := transform(p) // Pair[int, int]{10, 5}
@@ -147,7 +147,7 @@ Apply functions wrapped in pairs to values in pairs:
intSum := N.SemigroupSum[int]()
// Function in a pair
pf := pair.MakePair(10, func(s string) int { return len(s) })
pf := pair.MakePair(10, S.Size)
// Value in a pair
pv := pair.MakePair(5, "hello")
@@ -244,7 +244,7 @@ Functor - Map over values:
// Functor for tail
functor := pair.FunctorTail[int, string, int]()
mapper := functor.Map(func(s string) int { return len(s) })
mapper := functor.Map(S.Size)
p := pair.MakePair(5, "hello")
result := mapper(p) // Pair[int, int]{5, 5}
@@ -267,7 +267,7 @@ Applicative - Apply wrapped functions:
applicative := pair.ApplicativeTail[string, int, int](intSum)
// Create a pair with a function
pf := applicative.Of(func(s string) int { return len(s) })
pf := applicative.Of(S.Size)
// Apply to a value
pv := pair.MakePair(5, "hello")

View File

@@ -233,7 +233,7 @@ func PointedTail[B, A any](m monoid.Monoid[A]) pointed.Pointed[B, Pair[A, B]] {
// Example:
//
// functor := pair.FunctorTail[string, int, int]()
// mapper := functor.Map(func(s string) int { return len(s) })
// mapper := functor.Map(S.Size)
// p := pair.MakePair(5, "hello")
// p2 := mapper(p) // Pair[int, int]{5, 5}
func FunctorTail[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]] {
@@ -250,7 +250,7 @@ func FunctorTail[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]]
//
// intSum := M.MonoidSum[int]()
// applicative := pair.ApplicativeTail[string, int, int](intSum)
// pf := applicative.Of(func(s string) int { return len(s) })
// pf := applicative.Of(S.Size)
// pv := pair.MakePair(5, "hello")
// result := applicative.Ap(pv)(pf) // Pair[int, int]{5, 5}
func ApplicativeTail[B, A, B1 any](m monoid.Monoid[A]) applicative.Applicative[B, B1, Pair[A, B], Pair[A, B1], Pair[A, func(B) B1]] {
@@ -291,7 +291,7 @@ func Pointed[B, A any](m monoid.Monoid[A]) pointed.Pointed[B, Pair[A, B]] {
// Example:
//
// functor := pair.Functor[string, int, int]()
// mapper := functor.Map(func(s string) int { return len(s) })
// mapper := functor.Map(S.Size)
// p := pair.MakePair(5, "hello")
// p2 := mapper(p) // Pair[int, int]{5, 5}
func Functor[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]] {
@@ -307,7 +307,7 @@ func Functor[B, A, B1 any]() functor.Functor[B, B1, Pair[A, B], Pair[A, B1]] {
//
// intSum := M.MonoidSum[int]()
// applicative := pair.Applicative[string, int, int](intSum)
// pf := applicative.Of(func(s string) int { return len(s) })
// pf := applicative.Of(S.Size)
// pv := pair.MakePair(5, "hello")
// result := applicative.Ap(pv)(pf) // Pair[int, int]{5, 5}
func Applicative[B, A, B1 any](m monoid.Monoid[A]) applicative.Applicative[B, B1, Pair[A, B], Pair[A, B1], Pair[A, func(B) B1]] {

315
v2/pair/monoid.go Normal file
View File

@@ -0,0 +1,315 @@
// Copyright (c) 2024 - 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 pair
import (
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid creates a simple component-wise monoid for [Pair].
//
// This function creates a monoid that combines pairs by independently combining their
// head and tail components using the provided monoids. Both components are combined
// in NORMAL left-to-right order.
//
// IMPORTANT: This is DIFFERENT from [ApplicativeMonoidTail] and [ApplicativeMonoidHead],
// which use applicative functor operations and reverse the order of the non-focused component.
//
// Use this function when you want:
// - Simple, predictable left-to-right combination for both components
// - Behavior that matches intuition for non-commutative operations
// - Direct component-wise combination without applicative functor semantics
//
// Use [ApplicativeMonoidTail] or [ApplicativeMonoidHead] when you need applicative
// functor semantics for lifting monoid operations into the Pair context.
//
// Parameters:
// - l: A monoid for the head (left) values of type L
// - r: A monoid for the tail (right) values of type R
//
// Returns:
// - A Monoid[Pair[L, R]] that combines both components left-to-right
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// intAdd := N.MonoidSum[int]()
// strConcat := S.Monoid
//
// pairMonoid := pair.Monoid(intAdd, strConcat)
//
// p1 := pair.MakePair(5, "hello")
// p2 := pair.MakePair(10, " world")
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[int, string]{15, "hello world"}
// // Both components combine left-to-right: (5+10, "hello"+" world")
//
// empty := pairMonoid.Empty()
// // empty is Pair[int, string]{0, ""}
//
// Comparison with ApplicativeMonoidTail:
//
// strConcat := S.Monoid
//
// // Simple component-wise monoid
// simpleMonoid := pair.Monoid(strConcat, strConcat)
// p1 := pair.MakePair("A", "1")
// p2 := pair.MakePair("B", "2")
// result1 := simpleMonoid.Concat(p1, p2)
// // result1 is Pair[string, string]{"AB", "12"}
// // Both components: left-to-right
//
// // Applicative monoid
// appMonoid := pair.ApplicativeMonoidTail(strConcat, strConcat)
// result2 := appMonoid.Concat(p1, p2)
// // result2 is Pair[string, string]{"BA", "12"}
// // Head: reversed, Tail: normal
//
//go:inline
func Monoid[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
return M.MakeMonoid(
func(pl, pr Pair[L, R]) Pair[L, R] {
return MakePair(l.Concat(Head(pl), Head(pr)), r.Concat(Tail(pl), Tail(pr)))
},
MakePair(l.Empty(), r.Empty()),
)
}
// ApplicativeMonoid creates a monoid for [Pair] using applicative functor operations on the tail.
//
// This is an alias for [ApplicativeMonoidTail], which lifts the right (tail) monoid into the
// Pair applicative functor. The left monoid provides the semigroup for combining head values
// during applicative operations.
//
// IMPORTANT BEHAVIORAL NOTE: The applicative implementation causes the HEAD component to be
// combined in REVERSE order (right-to-left) while the TAIL combines normally (left-to-right).
// This differs from Haskell's standard Applicative instance for pairs, which combines the
// first component left-to-right. This matters for non-commutative operations like string
// concatenation.
//
// Parameters:
// - l: A monoid for the head (left) values of type L
// - r: A monoid for the tail (right) values of type R
//
// Returns:
// - A Monoid[Pair[L, R]] that combines pairs using applicative operations on the tail
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// intAdd := N.MonoidSum[int]()
// strConcat := S.Monoid
//
// pairMonoid := pair.ApplicativeMonoid(intAdd, strConcat)
//
// p1 := pair.MakePair(10, "foo")
// p2 := pair.MakePair(20, "bar")
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[int, string]{30, "foobar"}
// // Note: head combines normally (10+20), tail combines normally ("foo"+"bar")
//
// empty := pairMonoid.Empty()
// // empty is Pair[int, string]{0, ""}
//
//go:inline
func ApplicativeMonoid[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
return ApplicativeMonoidTail(l, r)
}
// ApplicativeMonoidTail creates a monoid for [Pair] by lifting the tail monoid into the applicative functor.
//
// This function constructs a monoid using the applicative structure of Pair, focusing on
// the tail (right) value. The head values are combined using the left monoid's semigroup
// operation during applicative application.
//
// CRITICAL BEHAVIORAL NOTE: The HEAD values are combined in REVERSE order (right-to-left),
// while TAIL values combine in normal order (left-to-right). This is due to how the
// applicative `ap` operation is implemented for Pair.
//
// NOTE: This differs from Haskell's standard Applicative instance for (,) which combines
// the first component left-to-right. The reversal occurs because MonadApTail implements:
//
// MakePair(sg.Concat(second.head, first.head), ...)
//
// Example showing the reversal with non-commutative operations:
//
// strConcat := S.Monoid
// pairMonoid := pair.ApplicativeMonoidTail(strConcat, strConcat)
// p1 := pair.MakePair("hello", "foo")
// p2 := pair.MakePair(" world", "bar")
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[string, string]{" worldhello", "foobar"}
// // ^^^^^^^^^^^^^^ ^^^^^^
// // REVERSED! normal order
//
// In Haskell's Applicative for (,), this would give ("hellohello world", "foobar")
//
// The resulting monoid satisfies the standard monoid laws:
// - Associativity: Concat(Concat(p1, p2), p3) = Concat(p1, Concat(p2, p3))
// - Left identity: Concat(Empty(), p) = p
// - Right identity: Concat(p, Empty()) = p
//
// Parameters:
// - l: A monoid for the head (left) values of type L
// - r: A monoid for the tail (right) values of type R
//
// Returns:
// - A Monoid[Pair[L, R]] that combines pairs component-wise
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// M "github.com/IBM/fp-go/v2/monoid"
// )
//
// intAdd := N.MonoidSum[int]()
// intMul := N.MonoidProduct[int]()
//
// pairMonoid := pair.ApplicativeMonoidTail(intAdd, intMul)
//
// p1 := pair.MakePair(5, 3)
// p2 := pair.MakePair(10, 4)
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[int, int]{15, 12} (5+10, 3*4)
// // Note: Addition is commutative, so order doesn't matter for head
//
// empty := pairMonoid.Empty()
// // empty is Pair[int, int]{0, 1}
//
// Example with different types:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// boolAnd := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
// strConcat := S.Monoid
//
// pairMonoid := pair.ApplicativeMonoidTail(boolAnd, strConcat)
//
// p1 := pair.MakePair(true, "hello")
// p2 := pair.MakePair(true, " world")
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[bool, string]{true, "hello world"}
// // Note: Boolean AND is commutative, so order doesn't matter for head
//
//go:inline
func ApplicativeMonoidTail[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
return M.ApplicativeMonoid(
FromHead[R](l.Empty()),
MonadMapTail[L, R, func(R) R],
F.Bind1of3(MonadApTail[L, R, R])(l),
r)
}
// ApplicativeMonoidHead creates a monoid for [Pair] by lifting the head monoid into the applicative functor.
//
// This function constructs a monoid using the applicative structure of Pair, focusing on
// the head (left) value. The tail values are combined using the right monoid's semigroup
// operation during applicative application.
//
// This is the dual of [ApplicativeMonoidTail], operating on the head instead of the tail.
//
// CRITICAL BEHAVIORAL NOTE: The TAIL values are combined in REVERSE order (right-to-left),
// while HEAD values combine in normal order (left-to-right). This is the opposite behavior
// of ApplicativeMonoidTail. The reversal occurs because MonadApHead implements:
//
// MakePair(..., sg.Concat(second.tail, first.tail))
//
// Example showing the reversal with non-commutative operations:
//
// strConcat := S.Monoid
// pairMonoid := pair.ApplicativeMonoidHead(strConcat, strConcat)
// p1 := pair.MakePair("hello", "foo")
// p2 := pair.MakePair(" world", "bar")
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[string, string]{"hello world", "barfoo"}
// // ^^^^^^^^^^^^ ^^^^^^^^
// // normal order REVERSED!
//
// The resulting monoid satisfies the standard monoid laws:
// - Associativity: Concat(Concat(p1, p2), p3) = Concat(p1, Concat(p2, p3))
// - Left identity: Concat(Empty(), p) = p
// - Right identity: Concat(p, Empty()) = p
//
// Parameters:
// - l: A monoid for the head (left) values of type L
// - r: A monoid for the tail (right) values of type R
//
// Returns:
// - A Monoid[Pair[L, R]] that combines pairs component-wise
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// M "github.com/IBM/fp-go/v2/monoid"
// )
//
// intMul := N.MonoidProduct[int]()
// intAdd := N.MonoidSum[int]()
//
// pairMonoid := pair.ApplicativeMonoidHead(intMul, intAdd)
//
// p1 := pair.MakePair(3, 5)
// p2 := pair.MakePair(4, 10)
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[int, int]{12, 15} (3*4, 5+10)
// // Note: Both operations are commutative, so order doesn't matter
//
// empty := pairMonoid.Empty()
// // empty is Pair[int, int]{1, 0}
//
// Example comparing Head vs Tail with non-commutative operations:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// strConcat := S.Monoid
//
// // Using ApplicativeMonoidHead - tail values REVERSED
// headMonoid := pair.ApplicativeMonoidHead(strConcat, strConcat)
// p1 := pair.MakePair("hello", "foo")
// p2 := pair.MakePair(" world", "bar")
// result := headMonoid.Concat(p1, p2)
// // result is Pair[string, string]{"hello world", "barfoo"}
//
// // Using ApplicativeMonoidTail - head values REVERSED
// tailMonoid := pair.ApplicativeMonoidTail(strConcat, strConcat)
// result2 := tailMonoid.Concat(p1, p2)
// // result2 is Pair[string, string]{" worldhello", "foobar"}
// // DIFFERENT result! Head and tail are swapped in their reversal behavior
//
//go:inline
func ApplicativeMonoidHead[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
return M.ApplicativeMonoid(
FromTail[L](r.Empty()),
MonadMapHead[R, L, func(L) L],
F.Bind1of3(MonadApHead[R, L, L])(r),
l)
}

756
v2/pair/monoid_test.go Normal file
View File

@@ -0,0 +1,756 @@
// Copyright (c) 2024 - 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 pair
import (
"testing"
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestApplicativeMonoidTail tests the ApplicativeMonoidTail implementation
func TestApplicativeMonoidTail(t *testing.T) {
t.Run("integer addition and string concatenation", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p1 := MakePair(5, "hello")
p2 := MakePair(3, " world")
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 8, Head(result))
assert.Equal(t, "hello world", Tail(result))
})
t.Run("integer multiplication and addition", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidTail(intMul, intAdd)
p1 := MakePair(3, 5)
p2 := MakePair(4, 10)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 12, Head(result)) // 3 * 4
assert.Equal(t, 15, Tail(result)) // 5 + 10
})
t.Run("boolean AND and OR", func(t *testing.T) {
boolAnd := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
boolOr := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
pairMonoid := ApplicativeMonoidTail(boolAnd, boolOr)
p1 := MakePair(true, false)
p2 := MakePair(true, true)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, true, Head(result)) // true && true
assert.Equal(t, true, Tail(result)) // false || true
})
t.Run("empty value", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
empty := pairMonoid.Empty()
assert.Equal(t, 0, Head(empty))
assert.Equal(t, "", Tail(empty))
})
t.Run("left identity law", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p := MakePair(5, "test")
result := pairMonoid.Concat(pairMonoid.Empty(), p)
assert.Equal(t, p, result)
})
t.Run("right identity law", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p := MakePair(5, "test")
result := pairMonoid.Concat(p, pairMonoid.Empty())
assert.Equal(t, p, result)
})
t.Run("associativity law", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p1 := MakePair(1, "a")
p2 := MakePair(2, "b")
p3 := MakePair(3, "c")
left := pairMonoid.Concat(pairMonoid.Concat(p1, p2), p3)
right := pairMonoid.Concat(p1, pairMonoid.Concat(p2, p3))
assert.Equal(t, left, right)
assert.Equal(t, 6, Head(left))
assert.Equal(t, "abc", Tail(left))
})
t.Run("multiple concatenations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
pairs := []Pair[int, int]{
MakePair(1, 2),
MakePair(3, 4),
MakePair(5, 6),
}
result := pairMonoid.Empty()
for _, p := range pairs {
result = pairMonoid.Concat(result, p)
}
assert.Equal(t, 9, Head(result)) // 0 + 1 + 3 + 5
assert.Equal(t, 48, Tail(result)) // 1 * 2 * 4 * 6
})
}
// TestApplicativeMonoidHead tests the ApplicativeMonoidHead implementation
func TestApplicativeMonoidHead(t *testing.T) {
t.Run("integer multiplication and addition", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p1 := MakePair(3, 5)
p2 := MakePair(4, 10)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 12, Head(result)) // 3 * 4
assert.Equal(t, 15, Tail(result)) // 5 + 10
})
t.Run("string concatenation and boolean OR", func(t *testing.T) {
strConcat := S.Monoid
boolOr := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
pairMonoid := ApplicativeMonoidHead(strConcat, boolOr)
p1 := MakePair("hello", false)
p2 := MakePair(" world", true)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, "hello world", Head(result))
assert.Equal(t, true, Tail(result))
})
t.Run("empty value", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
empty := pairMonoid.Empty()
assert.Equal(t, 1, Head(empty))
assert.Equal(t, 0, Tail(empty))
})
t.Run("left identity law", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p := MakePair(5, 10)
result := pairMonoid.Concat(pairMonoid.Empty(), p)
assert.Equal(t, p, result)
})
t.Run("right identity law", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p := MakePair(5, 10)
result := pairMonoid.Concat(p, pairMonoid.Empty())
assert.Equal(t, p, result)
})
t.Run("associativity law", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p1 := MakePair(2, 1)
p2 := MakePair(3, 2)
p3 := MakePair(4, 3)
left := pairMonoid.Concat(pairMonoid.Concat(p1, p2), p3)
right := pairMonoid.Concat(p1, pairMonoid.Concat(p2, p3))
assert.Equal(t, left, right)
assert.Equal(t, 24, Head(left)) // 2 * 3 * 4
assert.Equal(t, 6, Tail(left)) // 1 + 2 + 3
})
t.Run("multiple concatenations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidHead(intAdd, intMul)
pairs := []Pair[int, int]{
MakePair(1, 2),
MakePair(3, 4),
MakePair(5, 6),
}
result := pairMonoid.Empty()
for _, p := range pairs {
result = pairMonoid.Concat(result, p)
}
assert.Equal(t, 9, Head(result)) // 0 + 1 + 3 + 5
assert.Equal(t, 48, Tail(result)) // 1 * 2 * 4 * 6
})
}
// TestApplicativeMonoid tests the ApplicativeMonoid alias
func TestApplicativeMonoid(t *testing.T) {
t.Run("is alias for ApplicativeMonoidTail", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
monoid1 := ApplicativeMonoid(intAdd, strConcat)
monoid2 := ApplicativeMonoidTail(intAdd, strConcat)
p1 := MakePair(5, "hello")
p2 := MakePair(3, " world")
result1 := monoid1.Concat(p1, p2)
result2 := monoid2.Concat(p1, p2)
assert.Equal(t, result1, result2)
assert.Equal(t, 8, Head(result1))
assert.Equal(t, "hello world", Tail(result1))
})
t.Run("empty values are identical", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
monoid1 := ApplicativeMonoid(intAdd, strConcat)
monoid2 := ApplicativeMonoidTail(intAdd, strConcat)
assert.Equal(t, monoid1.Empty(), monoid2.Empty())
})
}
// TestMonoidHeadVsTail compares ApplicativeMonoidHead and ApplicativeMonoidTail
func TestMonoidHeadVsTail(t *testing.T) {
t.Run("same result with commutative operations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
headMonoid := ApplicativeMonoidHead(intMul, intAdd)
tailMonoid := ApplicativeMonoidTail(intMul, intAdd)
p1 := MakePair(2, 3)
p2 := MakePair(4, 5)
resultHead := headMonoid.Concat(p1, p2)
resultTail := tailMonoid.Concat(p1, p2)
// Both should give same result since operations are commutative
assert.Equal(t, 8, Head(resultHead)) // 2 * 4
assert.Equal(t, 8, Tail(resultHead)) // 3 + 5
assert.Equal(t, 8, Head(resultTail)) // 2 * 4
assert.Equal(t, 8, Tail(resultTail)) // 3 + 5
})
t.Run("different empty values", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
headMonoid := ApplicativeMonoidHead(intMul, intAdd)
tailMonoid := ApplicativeMonoidTail(intAdd, intMul)
emptyHead := headMonoid.Empty()
emptyTail := tailMonoid.Empty()
assert.Equal(t, 1, Head(emptyHead)) // intMul empty
assert.Equal(t, 0, Tail(emptyHead)) // intAdd empty
assert.Equal(t, 0, Head(emptyTail)) // intAdd empty
assert.Equal(t, 1, Tail(emptyTail)) // intMul empty
})
}
// TestMonoidLaws verifies monoid laws for all implementations
func TestMonoidLaws(t *testing.T) {
testCases := []struct {
name string
monoid M.Monoid[Pair[int, int]]
p1, p2, p3 Pair[int, int]
}{
{
name: "ApplicativeMonoidTail",
monoid: ApplicativeMonoidTail(N.MonoidSum[int](), N.MonoidProduct[int]()),
p1: MakePair(1, 2),
p2: MakePair(3, 4),
p3: MakePair(5, 6),
},
{
name: "ApplicativeMonoidHead",
monoid: ApplicativeMonoidHead(N.MonoidProduct[int](), N.MonoidSum[int]()),
p1: MakePair(2, 1),
p2: MakePair(3, 2),
p3: MakePair(4, 3),
},
{
name: "ApplicativeMonoid",
monoid: ApplicativeMonoid(N.MonoidSum[int](), N.MonoidSum[int]()),
p1: MakePair(1, 2),
p2: MakePair(3, 4),
p3: MakePair(5, 6),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("associativity", func(t *testing.T) {
left := tc.monoid.Concat(tc.monoid.Concat(tc.p1, tc.p2), tc.p3)
right := tc.monoid.Concat(tc.p1, tc.monoid.Concat(tc.p2, tc.p3))
assert.Equal(t, left, right)
})
t.Run("left identity", func(t *testing.T) {
result := tc.monoid.Concat(tc.monoid.Empty(), tc.p1)
assert.Equal(t, tc.p1, result)
})
t.Run("right identity", func(t *testing.T) {
result := tc.monoid.Concat(tc.p1, tc.monoid.Empty())
assert.Equal(t, tc.p1, result)
})
})
}
}
// TestMonoidEdgeCases tests edge cases for monoid operations
func TestMonoidEdgeCases(t *testing.T) {
t.Run("concatenating empty with empty", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
result := pairMonoid.Concat(pairMonoid.Empty(), pairMonoid.Empty())
assert.Equal(t, pairMonoid.Empty(), result)
})
t.Run("chain of operations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
result := pairMonoid.Concat(
pairMonoid.Concat(
pairMonoid.Concat(MakePair(1, 2), MakePair(2, 3)),
MakePair(3, 4),
),
MakePair(4, 5),
)
assert.Equal(t, 10, Head(result)) // 1 + 2 + 3 + 4
assert.Equal(t, 120, Tail(result)) // 2 * 3 * 4 * 5
})
t.Run("zero values", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
p1 := MakePair(0, 0)
p2 := MakePair(5, 10)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 5, Head(result))
assert.Equal(t, 0, Tail(result)) // 0 * 10 = 0
})
t.Run("negative values", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
p1 := MakePair(-5, -2)
p2 := MakePair(3, 4)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, -2, Head(result)) // -5 + 3
assert.Equal(t, -8, Tail(result)) // -2 * 4
})
}
// TestMonoidWithDifferentTypes tests monoids with various type combinations
func TestMonoidWithDifferentTypes(t *testing.T) {
t.Run("string and boolean", func(t *testing.T) {
strConcat := S.Monoid
boolAnd := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
pairMonoid := ApplicativeMonoidTail(strConcat, boolAnd)
p1 := MakePair("hello", true)
p2 := MakePair(" world", true)
result := pairMonoid.Concat(p1, p2)
// Note: The order depends on the applicative implementation
assert.Equal(t, " worldhello", Head(result))
assert.Equal(t, true, Tail(result))
})
t.Run("boolean and string", func(t *testing.T) {
boolOr := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(boolOr, strConcat)
p1 := MakePair(false, "foo")
p2 := MakePair(true, "bar")
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, true, Head(result))
assert.Equal(t, "foobar", Tail(result))
})
t.Run("float64 addition and multiplication", func(t *testing.T) {
floatAdd := N.MonoidSum[float64]()
floatMul := N.MonoidProduct[float64]()
pairMonoid := ApplicativeMonoidTail(floatAdd, floatMul)
p1 := MakePair(1.5, 2.0)
p2 := MakePair(2.5, 3.0)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 4.0, Head(result))
assert.Equal(t, 6.0, Tail(result))
})
}
// TestMonoidCommutativity tests behavior with non-commutative operations
func TestMonoidCommutativity(t *testing.T) {
t.Run("string concatenation is not commutative", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
p1 := MakePair("hello", "foo")
p2 := MakePair(" world", "bar")
result1 := pairMonoid.Concat(p1, p2)
result2 := pairMonoid.Concat(p2, p1)
// The applicative implementation reverses the order for head values
assert.Equal(t, " worldhello", Head(result1))
assert.Equal(t, "foobar", Tail(result1))
assert.Equal(t, "hello world", Head(result2))
assert.Equal(t, "barfoo", Tail(result2))
assert.NotEqual(t, result1, result2)
})
}
// TestSimpleMonoid tests the basic Monoid function (non-applicative)
func TestSimpleMonoid(t *testing.T) {
t.Run("combines both components left-to-right", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := Monoid(strConcat, strConcat)
p1 := MakePair("hello", "foo")
p2 := MakePair(" world", "bar")
result := pairMonoid.Concat(p1, p2)
// Both components combine in normal left-to-right order
assert.Equal(t, "hello world", Head(result))
assert.Equal(t, "foobar", Tail(result))
})
t.Run("empty value", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := Monoid(intAdd, strConcat)
empty := pairMonoid.Empty()
assert.Equal(t, 0, Head(empty))
assert.Equal(t, "", Tail(empty))
})
t.Run("monoid laws", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := Monoid(intAdd, intMul)
p1 := MakePair(1, 2)
p2 := MakePair(3, 4)
p3 := MakePair(5, 6)
// Associativity
left := pairMonoid.Concat(pairMonoid.Concat(p1, p2), p3)
right := pairMonoid.Concat(p1, pairMonoid.Concat(p2, p3))
assert.Equal(t, left, right)
// Left identity
assert.Equal(t, p1, pairMonoid.Concat(pairMonoid.Empty(), p1))
// Right identity
assert.Equal(t, p1, pairMonoid.Concat(p1, pairMonoid.Empty()))
})
}
// TestMonoidComparison compares the simple Monoid with applicative versions
func TestMonoidComparison(t *testing.T) {
t.Run("Monoid vs ApplicativeMonoidTail with strings", func(t *testing.T) {
strConcat := S.Monoid
simpleMonoid := Monoid(strConcat, strConcat)
appMonoid := ApplicativeMonoidTail(strConcat, strConcat)
p1 := MakePair("A", "1")
p2 := MakePair("B", "2")
simpleResult := simpleMonoid.Concat(p1, p2)
appResult := appMonoid.Concat(p1, p2)
// Simple monoid: both components left-to-right
assert.Equal(t, "AB", Head(simpleResult))
assert.Equal(t, "12", Tail(simpleResult))
// Applicative monoid: head reversed, tail normal
assert.Equal(t, "BA", Head(appResult))
assert.Equal(t, "12", Tail(appResult))
// They produce different results!
assert.NotEqual(t, simpleResult, appResult)
})
t.Run("Monoid vs ApplicativeMonoidHead with strings", func(t *testing.T) {
strConcat := S.Monoid
simpleMonoid := Monoid(strConcat, strConcat)
appMonoid := ApplicativeMonoidHead(strConcat, strConcat)
p1 := MakePair("A", "1")
p2 := MakePair("B", "2")
simpleResult := simpleMonoid.Concat(p1, p2)
appResult := appMonoid.Concat(p1, p2)
// Simple monoid: both components left-to-right
assert.Equal(t, "AB", Head(simpleResult))
assert.Equal(t, "12", Tail(simpleResult))
// Applicative monoid: head normal, tail reversed
assert.Equal(t, "AB", Head(appResult))
assert.Equal(t, "21", Tail(appResult))
// They produce different results!
assert.NotEqual(t, simpleResult, appResult)
})
t.Run("all three produce same result with commutative operations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
simpleMonoid := Monoid(intAdd, intMul)
appTailMonoid := ApplicativeMonoidTail(intAdd, intMul)
appHeadMonoid := ApplicativeMonoidHead(intAdd, intMul)
p1 := MakePair(2, 3)
p2 := MakePair(4, 5)
simpleResult := simpleMonoid.Concat(p1, p2)
appTailResult := appTailMonoid.Concat(p1, p2)
appHeadResult := appHeadMonoid.Concat(p1, p2)
// All produce the same result with commutative operations
// Simple: (2+4, 3*5) = (6, 15)
// AppTail: (4+2, 3*5) = (6, 15) - addition is commutative
// AppHead: (2+4, 5*3) = (6, 15) - multiplication is commutative
assert.Equal(t, 6, Head(simpleResult))
assert.Equal(t, 15, Tail(simpleResult))
assert.Equal(t, simpleResult, appTailResult)
assert.Equal(t, simpleResult, appHeadResult)
})
t.Run("Monoid matches Haskell behavior", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := Monoid(strConcat, strConcat)
p1 := MakePair("hello", "foo")
p2 := MakePair(" world", "bar")
result := pairMonoid.Concat(p1, p2)
// This matches what you'd expect from simple tuple combination
// and is closer to intuitive behavior
assert.Equal(t, "hello world", Head(result))
assert.Equal(t, "foobar", Tail(result))
})
}
// TestMonoidHaskellComparison documents how this implementation differs from Haskell's
// standard Applicative instance for pairs (tuples).
func TestMonoidHaskellComparison(t *testing.T) {
t.Run("ApplicativeMonoidTail reverses head order unlike Haskell", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
p1 := MakePair("hello", "foo")
p2 := MakePair(" world", "bar")
result := pairMonoid.Concat(p1, p2)
// Go implementation: head is reversed, tail is normal
assert.Equal(t, " worldhello", Head(result))
assert.Equal(t, "foobar", Tail(result))
// In Haskell's Applicative for (,):
// (u, f) <*> (v, x) = (u <> v, f x)
// pure (<>) <*> ("hello", "foo") <*> (" world", "bar")
// would give: ("hello world", "foobar")
// Note: Haskell combines first component left-to-right, not reversed
})
t.Run("ApplicativeMonoidHead reverses tail order", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidHead(strConcat, strConcat)
p1 := MakePair("hello", "foo")
p2 := MakePair(" world", "bar")
result := pairMonoid.Concat(p1, p2)
// Go implementation: head is normal, tail is reversed
assert.Equal(t, "hello world", Head(result))
assert.Equal(t, "barfoo", Tail(result))
// This is the dual operation, focusing on head instead of tail
})
t.Run("behavior with commutative operations matches Haskell", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
p1 := MakePair(5, 3)
p2 := MakePair(10, 4)
result := pairMonoid.Concat(p1, p2)
// With commutative operations, order doesn't matter
// Both Go and Haskell give the same result
assert.Equal(t, 15, Head(result)) // 5 + 10 = 10 + 5
assert.Equal(t, 12, Tail(result)) // 3 * 4 = 4 * 3
})
}
// TestMonoidOrderingDocumentation provides clear examples of the ordering behavior
// for documentation purposes.
func TestMonoidOrderingDocumentation(t *testing.T) {
t.Run("ApplicativeMonoidTail ordering", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
p1 := MakePair("A", "1")
p2 := MakePair("B", "2")
p3 := MakePair("C", "3")
// Concat p1 and p2
r12 := pairMonoid.Concat(p1, p2)
assert.Equal(t, "BA", Head(r12)) // Head: reversed (p2 + p1)
assert.Equal(t, "12", Tail(r12)) // Tail: normal (p1 + p2)
// Concat all three
r123 := pairMonoid.Concat(r12, p3)
assert.Equal(t, "CBA", Head(r123)) // Head: reversed (p3 + p2 + p1)
assert.Equal(t, "123", Tail(r123)) // Tail: normal (p1 + p2 + p3)
})
t.Run("ApplicativeMonoidHead ordering", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidHead(strConcat, strConcat)
p1 := MakePair("A", "1")
p2 := MakePair("B", "2")
p3 := MakePair("C", "3")
// Concat p1 and p2
r12 := pairMonoid.Concat(p1, p2)
assert.Equal(t, "AB", Head(r12)) // Head: normal (p1 + p2)
assert.Equal(t, "21", Tail(r12)) // Tail: reversed (p2 + p1)
// Concat all three
r123 := pairMonoid.Concat(r12, p3)
assert.Equal(t, "ABC", Head(r123)) // Head: normal (p1 + p2 + p3)
assert.Equal(t, "321", Tail(r123)) // Tail: reversed (p3 + p2 + p1)
})
t.Run("empty values respect ordering", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
empty := pairMonoid.Empty()
p := MakePair("X", "Y")
// Empty is identity regardless of order
r1 := pairMonoid.Concat(empty, p)
r2 := pairMonoid.Concat(p, empty)
assert.Equal(t, p, r1)
assert.Equal(t, p, r2)
})
}

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