mirror of
https://github.com/IBM/fp-go.git
synced 2026-01-17 00:53:55 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc2041d8e | ||
|
|
777fff9a5a | ||
|
|
8acea9043f | ||
|
|
c6445ac021 | ||
|
|
840ffbb51d | ||
|
|
380ba2853c | ||
|
|
c18e5e2107 | ||
|
|
89766bdb26 | ||
|
|
21d116d325 | ||
|
|
7f2e76dd94 | ||
|
|
77965a12ff | ||
|
|
ed77bd7971 |
14
v2/DESIGN.md
14
v2/DESIGN.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -622,3 +622,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)
|
||||
}
|
||||
|
||||
@@ -474,3 +474,293 @@ 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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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:]) })
|
||||
}
|
||||
}
|
||||
|
||||
298
v2/array/generic/array_test.go
Normal file
298
v2/array/generic/array_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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:]) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
74
v2/context/readerio/profunctor.go
Normal file
74
v2/context/readerio/profunctor.go
Normal 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)
|
||||
}
|
||||
97
v2/context/readerio/profunctor_test.go
Normal file
97
v2/context/readerio/profunctor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
75
v2/context/readerioresult/profunctor.go
Normal file
75
v2/context/readerioresult/profunctor.go
Normal 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)
|
||||
}
|
||||
98
v2/context/readerioresult/profunctor_test.go
Normal file
98
v2/context/readerioresult/profunctor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
106
v2/context/readerresult/profunctor.go
Normal file
106
v2/context/readerresult/profunctor.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
92
v2/context/readerresult/profunctor_test.go
Normal file
92
v2/context/readerresult/profunctor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -56,3 +56,77 @@ func AltMonoid[E, A any](zero Lazy[Either[E, A]]) Monoid[E, A] {
|
||||
MonadAlt[E, A],
|
||||
)
|
||||
}
|
||||
|
||||
// takeFirst is a helper function that returns the first Right value, or the second if the first is Left.
|
||||
func takeFirst[E, A any](l, r Either[E, A]) Either[E, A] {
|
||||
if IsRight(l) {
|
||||
return l
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// FirstMonoid creates a Monoid for Either[E, A] that returns the first Right value.
|
||||
// This monoid prefers the left operand when it is Right, otherwise returns the right operand.
|
||||
// The empty value is provided as a lazy computation.
|
||||
//
|
||||
// This is equivalent to AltMonoid but implemented more directly.
|
||||
//
|
||||
// Truth table:
|
||||
//
|
||||
// | x | y | concat(x, y) |
|
||||
// | --------- | --------- | ------------ |
|
||||
// | left(e1) | left(e2) | left(e2) |
|
||||
// | right(a) | left(e) | right(a) |
|
||||
// | left(e) | right(b) | right(b) |
|
||||
// | right(a) | right(b) | right(a) |
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "errors"
|
||||
// zero := func() either.Either[error, int] { return either.Left[int](errors.New("empty")) }
|
||||
// m := either.FirstMonoid[error, int](zero)
|
||||
// m.Concat(either.Right[error](2), either.Right[error](3)) // Right(2) - returns first Right
|
||||
// m.Concat(either.Left[int](errors.New("err")), either.Right[error](3)) // Right(3)
|
||||
// m.Concat(either.Right[error](2), either.Left[int](errors.New("err"))) // Right(2)
|
||||
// m.Empty() // Left(error("empty"))
|
||||
//
|
||||
//go:inline
|
||||
func FirstMonoid[E, A any](zero Lazy[Either[E, A]]) M.Monoid[Either[E, A]] {
|
||||
return M.MakeMonoid(takeFirst[E, A], zero())
|
||||
}
|
||||
|
||||
// takeLast is a helper function that returns the last Right value, or the first if the last is Left.
|
||||
func takeLast[E, A any](l, r Either[E, A]) Either[E, A] {
|
||||
if IsRight(r) {
|
||||
return r
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// LastMonoid creates a Monoid for Either[E, A] that returns the last Right value.
|
||||
// This monoid prefers the right operand when it is Right, otherwise returns the left operand.
|
||||
// The empty value is provided as a lazy computation.
|
||||
//
|
||||
// Truth table:
|
||||
//
|
||||
// | x | y | concat(x, y) |
|
||||
// | --------- | --------- | ------------ |
|
||||
// | left(e1) | left(e2) | left(e1) |
|
||||
// | right(a) | left(e) | right(a) |
|
||||
// | left(e) | right(b) | right(b) |
|
||||
// | right(a) | right(b) | right(b) |
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "errors"
|
||||
// zero := func() either.Either[error, int] { return either.Left[int](errors.New("empty")) }
|
||||
// m := either.LastMonoid[error, int](zero)
|
||||
// m.Concat(either.Right[error](2), either.Right[error](3)) // Right(3) - returns last Right
|
||||
// m.Concat(either.Left[int](errors.New("err")), either.Right[error](3)) // Right(3)
|
||||
// m.Concat(either.Right[error](2), either.Left[int](errors.New("err"))) // Right(2)
|
||||
// m.Empty() // Left(error("empty"))
|
||||
//
|
||||
//go:inline
|
||||
func LastMonoid[E, A any](zero Lazy[Either[E, A]]) M.Monoid[Either[E, A]] {
|
||||
return M.MakeMonoid(takeLast[E, A], zero())
|
||||
}
|
||||
|
||||
402
v2/either/monoid_test.go
Normal file
402
v2/either/monoid_test.go
Normal file
@@ -0,0 +1,402 @@
|
||||
// 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 either
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestFirstMonoid tests the FirstMonoid implementation
|
||||
func TestFirstMonoid(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
t.Run("both Right values - returns first", func(t *testing.T) {
|
||||
result := m.Concat(Right[error](2), Right[error](3))
|
||||
assert.Equal(t, Right[error](2), result)
|
||||
})
|
||||
|
||||
t.Run("left Right, right Left", func(t *testing.T) {
|
||||
result := m.Concat(Right[error](2), Left[int](errors.New("err")))
|
||||
assert.Equal(t, Right[error](2), result)
|
||||
})
|
||||
|
||||
t.Run("left Left, right Right", func(t *testing.T) {
|
||||
result := m.Concat(Left[int](errors.New("err")), Right[error](3))
|
||||
assert.Equal(t, Right[error](3), result)
|
||||
})
|
||||
|
||||
t.Run("both Left", func(t *testing.T) {
|
||||
err1 := errors.New("err1")
|
||||
err2 := errors.New("err2")
|
||||
result := m.Concat(Left[int](err1), Left[int](err2))
|
||||
// Should return the second Left
|
||||
assert.True(t, IsLeft(result))
|
||||
_, leftErr := Unwrap(result)
|
||||
assert.Equal(t, err2, leftErr)
|
||||
})
|
||||
|
||||
t.Run("empty value", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
assert.True(t, IsLeft(empty))
|
||||
_, leftErr := Unwrap(empty)
|
||||
assert.Equal(t, "empty", leftErr.Error())
|
||||
})
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Right[error](5)
|
||||
result := m.Concat(m.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Right[error](5)
|
||||
result := m.Concat(x, m.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
a := Right[error](1)
|
||||
b := Right[error](2)
|
||||
c := Right[error](3)
|
||||
|
||||
left := m.Concat(m.Concat(a, b), c)
|
||||
right := m.Concat(a, m.Concat(b, c))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, Right[error](1), left)
|
||||
})
|
||||
|
||||
t.Run("multiple concatenations", func(t *testing.T) {
|
||||
// Should return the first Right value encountered
|
||||
result := m.Concat(
|
||||
m.Concat(Left[int](errors.New("err1")), Right[error](1)),
|
||||
m.Concat(Right[error](2), Right[error](3)),
|
||||
)
|
||||
assert.Equal(t, Right[error](1), result)
|
||||
})
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
zeroStr := func() Either[error, string] { return Left[string](errors.New("empty")) }
|
||||
strMonoid := FirstMonoid(zeroStr)
|
||||
|
||||
result := strMonoid.Concat(Right[error]("first"), Right[error]("second"))
|
||||
assert.Equal(t, Right[error]("first"), result)
|
||||
|
||||
result = strMonoid.Concat(Left[string](errors.New("err")), Right[error]("second"))
|
||||
assert.Equal(t, Right[error]("second"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastMonoid tests the LastMonoid implementation
|
||||
func TestLastMonoid(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid(zero)
|
||||
|
||||
t.Run("both Right values - returns last", func(t *testing.T) {
|
||||
result := m.Concat(Right[error](2), Right[error](3))
|
||||
assert.Equal(t, Right[error](3), result)
|
||||
})
|
||||
|
||||
t.Run("left Right, right Left", func(t *testing.T) {
|
||||
result := m.Concat(Right[error](2), Left[int](errors.New("err")))
|
||||
assert.Equal(t, Right[error](2), result)
|
||||
})
|
||||
|
||||
t.Run("left Left, right Right", func(t *testing.T) {
|
||||
result := m.Concat(Left[int](errors.New("err")), Right[error](3))
|
||||
assert.Equal(t, Right[error](3), result)
|
||||
})
|
||||
|
||||
t.Run("both Left", func(t *testing.T) {
|
||||
err1 := errors.New("err1")
|
||||
err2 := errors.New("err2")
|
||||
result := m.Concat(Left[int](err1), Left[int](err2))
|
||||
// Should return the first Left
|
||||
assert.True(t, IsLeft(result))
|
||||
_, leftErr := Unwrap(result)
|
||||
assert.Equal(t, err1, leftErr)
|
||||
})
|
||||
|
||||
t.Run("empty value", func(t *testing.T) {
|
||||
empty := m.Empty()
|
||||
assert.True(t, IsLeft(empty))
|
||||
_, leftErr := Unwrap(empty)
|
||||
assert.Equal(t, "empty", leftErr.Error())
|
||||
})
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Right[error](5)
|
||||
result := m.Concat(m.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Right[error](5)
|
||||
result := m.Concat(x, m.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
a := Right[error](1)
|
||||
b := Right[error](2)
|
||||
c := Right[error](3)
|
||||
|
||||
left := m.Concat(m.Concat(a, b), c)
|
||||
right := m.Concat(a, m.Concat(b, c))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, Right[error](3), left)
|
||||
})
|
||||
|
||||
t.Run("multiple concatenations", func(t *testing.T) {
|
||||
// Should return the last Right value encountered
|
||||
result := m.Concat(
|
||||
m.Concat(Right[error](1), Right[error](2)),
|
||||
m.Concat(Right[error](3), Left[int](errors.New("err"))),
|
||||
)
|
||||
assert.Equal(t, Right[error](3), result)
|
||||
})
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
zeroStr := func() Either[error, string] { return Left[string](errors.New("empty")) }
|
||||
strMonoid := LastMonoid(zeroStr)
|
||||
|
||||
result := strMonoid.Concat(Right[error]("first"), Right[error]("second"))
|
||||
assert.Equal(t, Right[error]("second"), result)
|
||||
|
||||
result = strMonoid.Concat(Right[error]("first"), Left[string](errors.New("err")))
|
||||
assert.Equal(t, Right[error]("first"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// 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(zero)
|
||||
altMonoid := AltMonoid(zero)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
left Either[error, int]
|
||||
right Either[error, int]
|
||||
}{
|
||||
{"both Right", Right[error](1), Right[error](2)},
|
||||
{"left Right, right Left", Right[error](1), Left[int](errors.New("err"))},
|
||||
{"left Left, right Right", Left[int](errors.New("err")), Right[error](2)},
|
||||
{"both Left", Left[int](errors.New("err1")), Left[int](errors.New("err2"))},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(tc.left, tc.right)
|
||||
altResult := altMonoid.Concat(tc.left, tc.right)
|
||||
|
||||
// Both should have the same Right/Left status
|
||||
assert.Equal(t, IsRight(firstResult), IsRight(altResult), "FirstMonoid and AltMonoid should have same Right/Left status")
|
||||
|
||||
if IsRight(firstResult) {
|
||||
rightVal1, _ := Unwrap(firstResult)
|
||||
rightVal2, _ := Unwrap(altResult)
|
||||
assert.Equal(t, rightVal1, rightVal2, "FirstMonoid and AltMonoid should have same Right value")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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(zero)
|
||||
lastMonoid := LastMonoid(zero)
|
||||
|
||||
t.Run("both Right - different results", func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(Right[error](1), Right[error](2))
|
||||
lastResult := lastMonoid.Concat(Right[error](1), Right[error](2))
|
||||
|
||||
assert.Equal(t, Right[error](1), firstResult)
|
||||
assert.Equal(t, Right[error](2), lastResult)
|
||||
assert.NotEqual(t, firstResult, lastResult)
|
||||
})
|
||||
|
||||
t.Run("with Left values - different behavior", func(t *testing.T) {
|
||||
err1 := errors.New("err1")
|
||||
err2 := errors.New("err2")
|
||||
|
||||
// Both Left: FirstMonoid returns second, LastMonoid returns first
|
||||
firstResult := firstMonoid.Concat(Left[int](err1), Left[int](err2))
|
||||
lastResult := lastMonoid.Concat(Left[int](err1), Left[int](err2))
|
||||
|
||||
assert.True(t, IsLeft(firstResult))
|
||||
assert.True(t, IsLeft(lastResult))
|
||||
_, leftErr1 := Unwrap(firstResult)
|
||||
_, leftErr2 := Unwrap(lastResult)
|
||||
assert.Equal(t, err2, leftErr1)
|
||||
assert.Equal(t, err1, leftErr2)
|
||||
})
|
||||
|
||||
t.Run("mixed values - same results", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
left Either[error, int]
|
||||
right Either[error, int]
|
||||
expected Either[error, int]
|
||||
}{
|
||||
{"left Right, right Left", Right[error](1), Left[int](errors.New("err")), Right[error](1)},
|
||||
{"left Left, right Right", Left[int](errors.New("err")), Right[error](2), Right[error](2)},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(tc.left, tc.right)
|
||||
lastResult := lastMonoid.Concat(tc.left, tc.right)
|
||||
|
||||
assert.Equal(t, tc.expected, firstResult)
|
||||
assert.Equal(t, tc.expected, lastResult)
|
||||
assert.Equal(t, firstResult, lastResult)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidLaws verifies monoid laws for FirstMonoid and LastMonoid
|
||||
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(zero)
|
||||
|
||||
a := Right[error](1)
|
||||
b := Right[error](2)
|
||||
c := Right[error](3)
|
||||
|
||||
// Associativity: (a • b) • c = a • (b • c)
|
||||
left := m.Concat(m.Concat(a, b), c)
|
||||
right := m.Concat(a, m.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
|
||||
// Left identity: Empty() • a = a
|
||||
leftId := m.Concat(m.Empty(), a)
|
||||
assert.Equal(t, a, leftId)
|
||||
|
||||
// Right identity: a • Empty() = a
|
||||
rightId := m.Concat(a, m.Empty())
|
||||
assert.Equal(t, a, rightId)
|
||||
})
|
||||
|
||||
t.Run("LastMonoid laws", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid(zero)
|
||||
|
||||
a := Right[error](1)
|
||||
b := Right[error](2)
|
||||
c := Right[error](3)
|
||||
|
||||
// Associativity: (a • b) • c = a • (b • c)
|
||||
left := m.Concat(m.Concat(a, b), c)
|
||||
right := m.Concat(a, m.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
|
||||
// Left identity: Empty() • a = a
|
||||
leftId := m.Concat(m.Empty(), a)
|
||||
assert.Equal(t, a, leftId)
|
||||
|
||||
// Right identity: a • Empty() = a
|
||||
rightId := m.Concat(a, m.Empty())
|
||||
assert.Equal(t, a, rightId)
|
||||
})
|
||||
|
||||
t.Run("FirstMonoid laws with Left values", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
a := Left[int](errors.New("err1"))
|
||||
b := Left[int](errors.New("err2"))
|
||||
c := Left[int](errors.New("err3"))
|
||||
|
||||
// Associativity with Left values
|
||||
left := m.Concat(m.Concat(a, b), c)
|
||||
right := m.Concat(a, m.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
|
||||
t.Run("LastMonoid laws with Left values", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid(zero)
|
||||
|
||||
a := Left[int](errors.New("err1"))
|
||||
b := Left[int](errors.New("err2"))
|
||||
c := Left[int](errors.New("err3"))
|
||||
|
||||
// Associativity with Left values
|
||||
left := m.Concat(m.Concat(a, b), c)
|
||||
right := m.Concat(a, m.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidEdgeCases tests edge cases for monoid operations
|
||||
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(zero)
|
||||
|
||||
// Empty with empty
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("LastMonoid with empty concatenations", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid(zero)
|
||||
|
||||
// Empty with empty
|
||||
result := m.Concat(m.Empty(), m.Empty())
|
||||
assert.True(t, IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("FirstMonoid chain of operations", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := FirstMonoid(zero)
|
||||
|
||||
// Chain multiple operations
|
||||
result := m.Concat(
|
||||
m.Concat(
|
||||
m.Concat(Left[int](errors.New("err1")), Left[int](errors.New("err2"))),
|
||||
Right[error](1),
|
||||
),
|
||||
m.Concat(Right[error](2), Right[error](3)),
|
||||
)
|
||||
assert.Equal(t, Right[error](1), result)
|
||||
})
|
||||
|
||||
t.Run("LastMonoid chain of operations", func(t *testing.T) {
|
||||
zero := func() Either[error, int] { return Left[int](errors.New("empty")) }
|
||||
m := LastMonoid(zero)
|
||||
|
||||
// Chain multiple operations
|
||||
result := m.Concat(
|
||||
m.Concat(Right[error](1), Right[error](2)),
|
||||
m.Concat(
|
||||
Right[error](3),
|
||||
m.Concat(Right[error](4), Left[int](errors.New("err"))),
|
||||
),
|
||||
)
|
||||
assert.Equal(t, Right[error](4), result)
|
||||
})
|
||||
}
|
||||
91
v2/either/profunctor.go
Normal file
91
v2/either/profunctor.go
Normal 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)
|
||||
}
|
||||
375
v2/either/profunctor_test.go
Normal file
375
v2/either/profunctor_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
@@ -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
89
v2/file/doc.go
Normal 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
|
||||
@@ -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
367
v2/file/getters_test.go
Normal 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
45
v2/file/types.go
Normal 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]
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
59
v2/idiomatic/context/readerresult/profunctor.go
Normal file
59
v2/idiomatic/context/readerresult/profunctor.go
Normal 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)
|
||||
}
|
||||
187
v2/idiomatic/context/readerresult/profunctor_test.go
Normal file
187
v2/idiomatic/context/readerresult/profunctor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
74
v2/idiomatic/readerioresult/profunctor.go
Normal file
74
v2/idiomatic/readerioresult/profunctor.go
Normal 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)
|
||||
}
|
||||
199
v2/idiomatic/readerioresult/profunctor_test.go
Normal file
199
v2/idiomatic/readerioresult/profunctor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
76
v2/idiomatic/readerresult/profunctor.go
Normal file
76
v2/idiomatic/readerresult/profunctor.go
Normal 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)
|
||||
}
|
||||
238
v2/idiomatic/readerresult/profunctor_test.go
Normal file
238
v2/idiomatic/readerresult/profunctor_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
)()
|
||||
|
||||
@@ -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)))()
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -83,6 +83,8 @@ func Monoid[A any]() func(S.Semigroup[A]) M.Monoid[Option[A]] {
|
||||
// intMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
// optMonoid := AlternativeMonoid(intMonoid)
|
||||
// result := optMonoid.Concat(Some(2), Some(3)) // Some(5)
|
||||
//
|
||||
//go:inline
|
||||
func AlternativeMonoid[A any](m M.Monoid[A]) M.Monoid[Option[A]] {
|
||||
return M.AlternativeMonoid(
|
||||
Of[A],
|
||||
@@ -103,9 +105,81 @@ func AlternativeMonoid[A any](m M.Monoid[A]) M.Monoid[Option[A]] {
|
||||
// optMonoid.Concat(Some(2), Some(3)) // Some(2) - returns first Some
|
||||
// optMonoid.Concat(None[int](), Some(3)) // Some(3)
|
||||
// optMonoid.Empty() // None
|
||||
//
|
||||
//go:inline
|
||||
func AltMonoid[A any]() M.Monoid[Option[A]] {
|
||||
return M.AltMonoid(
|
||||
None[A],
|
||||
MonadAlt[A],
|
||||
)
|
||||
}
|
||||
|
||||
// takeFirst is a helper function that returns the first Some value, or the second if the first is None.
|
||||
func takeFirst[A any](l, r Option[A]) Option[A] {
|
||||
if IsSome(l) {
|
||||
return l
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// FirstMonoid creates a Monoid for Option[A] that returns the first Some value.
|
||||
// This monoid prefers the left operand when it is Some, otherwise returns the right operand.
|
||||
// The empty value is None.
|
||||
//
|
||||
// This is equivalent to AltMonoid but implemented more directly.
|
||||
//
|
||||
// Truth table:
|
||||
//
|
||||
// | x | y | concat(x, y) |
|
||||
// | ------- | ------- | ------------ |
|
||||
// | none | none | none |
|
||||
// | some(a) | none | some(a) |
|
||||
// | none | some(b) | some(b) |
|
||||
// | some(a) | some(b) | some(a) |
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// optMonoid := FirstMonoid[int]()
|
||||
// optMonoid.Concat(Some(2), Some(3)) // Some(2) - returns first Some
|
||||
// optMonoid.Concat(None[int](), Some(3)) // Some(3)
|
||||
// optMonoid.Concat(Some(2), None[int]()) // Some(2)
|
||||
// optMonoid.Empty() // None
|
||||
//
|
||||
//go:inline
|
||||
func FirstMonoid[A any]() M.Monoid[Option[A]] {
|
||||
return M.MakeMonoid(takeFirst[A], None[A]())
|
||||
}
|
||||
|
||||
// takeLast is a helper function that returns the last Some value, or the first if the last is None.
|
||||
func takeLast[A any](l, r Option[A]) Option[A] {
|
||||
if IsSome(r) {
|
||||
return r
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// LastMonoid creates a Monoid for Option[A] that returns the last Some value.
|
||||
// This monoid prefers the right operand when it is Some, otherwise returns the left operand.
|
||||
// The empty value is None.
|
||||
//
|
||||
// Truth table:
|
||||
//
|
||||
// | x | y | concat(x, y) |
|
||||
// | ------- | ------- | ------------ |
|
||||
// | none | none | none |
|
||||
// | some(a) | none | some(a) |
|
||||
// | none | some(b) | some(b) |
|
||||
// | some(a) | some(b) | some(b) |
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// optMonoid := LastMonoid[int]()
|
||||
// optMonoid.Concat(Some(2), Some(3)) // Some(3) - returns last Some
|
||||
// optMonoid.Concat(None[int](), Some(3)) // Some(3)
|
||||
// optMonoid.Concat(Some(2), None[int]()) // Some(2)
|
||||
// optMonoid.Empty() // None
|
||||
//
|
||||
//go:inline
|
||||
func LastMonoid[A any]() M.Monoid[Option[A]] {
|
||||
return M.MakeMonoid(takeLast[A], None[A]())
|
||||
}
|
||||
|
||||
445
v2/option/monoid_test.go
Normal file
445
v2/option/monoid_test.go
Normal file
@@ -0,0 +1,445 @@
|
||||
// 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 option
|
||||
|
||||
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/semigroup"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSemigroupAssociativity tests the associativity law for Semigroup
|
||||
func TestSemigroupAssociativity(t *testing.T) {
|
||||
intSemigroup := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
optSemigroup := Semigroup[int]()(intSemigroup)
|
||||
|
||||
a := Some(1)
|
||||
b := Some(2)
|
||||
c := Some(3)
|
||||
|
||||
// Test that (a • b) • c = a • (b • c)
|
||||
left := optSemigroup.Concat(optSemigroup.Concat(a, b), c)
|
||||
right := optSemigroup.Concat(a, optSemigroup.Concat(b, c))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, Some(6), left)
|
||||
}
|
||||
|
||||
// TestSemigroupWithNone tests Semigroup behavior with None values
|
||||
func TestSemigroupWithNone(t *testing.T) {
|
||||
intSemigroup := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
optSemigroup := Semigroup[int]()(intSemigroup)
|
||||
|
||||
t.Run("None with None", func(t *testing.T) {
|
||||
result := optSemigroup.Concat(None[int](), None[int]())
|
||||
assert.Equal(t, None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("associativity with None", func(t *testing.T) {
|
||||
a := None[int]()
|
||||
b := Some(2)
|
||||
c := Some(3)
|
||||
|
||||
left := optSemigroup.Concat(optSemigroup.Concat(a, b), c)
|
||||
right := optSemigroup.Concat(a, optSemigroup.Concat(b, c))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, Some(5), left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidIdentityLaws tests the identity laws for Monoid
|
||||
func TestMonoidIdentityLaws(t *testing.T) {
|
||||
intSemigroup := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
optMonoid := Monoid[int]()(intSemigroup)
|
||||
|
||||
t.Run("left identity with Some", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(optMonoid.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity with Some", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(x, optMonoid.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("left identity with None", func(t *testing.T) {
|
||||
x := None[int]()
|
||||
result := optMonoid.Concat(optMonoid.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity with None", func(t *testing.T) {
|
||||
x := None[int]()
|
||||
result := optMonoid.Concat(x, optMonoid.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlternativeMonoidIdentityLaws tests identity laws for AlternativeMonoid
|
||||
func TestAlternativeMonoidIdentityLaws(t *testing.T) {
|
||||
intMonoid := N.MonoidSum[int]()
|
||||
optMonoid := AlternativeMonoid(intMonoid)
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(optMonoid.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(x, optMonoid.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("empty is Some(0)", func(t *testing.T) {
|
||||
empty := optMonoid.Empty()
|
||||
assert.Equal(t, Some(0), empty)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAltMonoidIdentityLaws tests identity laws for AltMonoid
|
||||
func TestAltMonoidIdentityLaws(t *testing.T) {
|
||||
optMonoid := AltMonoid[int]()
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(optMonoid.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(x, optMonoid.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
a := Some(1)
|
||||
b := None[int]()
|
||||
c := Some(3)
|
||||
|
||||
left := optMonoid.Concat(optMonoid.Concat(a, b), c)
|
||||
right := optMonoid.Concat(a, optMonoid.Concat(b, c))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, Some(1), left)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstMonoid tests the FirstMonoid implementation
|
||||
func TestFirstMonoid(t *testing.T) {
|
||||
optMonoid := FirstMonoid[int]()
|
||||
|
||||
t.Run("both Some values - returns first", func(t *testing.T) {
|
||||
result := optMonoid.Concat(Some(2), Some(3))
|
||||
assert.Equal(t, Some(2), result)
|
||||
})
|
||||
|
||||
t.Run("left Some, right None", func(t *testing.T) {
|
||||
result := optMonoid.Concat(Some(2), None[int]())
|
||||
assert.Equal(t, Some(2), result)
|
||||
})
|
||||
|
||||
t.Run("left None, right Some", func(t *testing.T) {
|
||||
result := optMonoid.Concat(None[int](), Some(3))
|
||||
assert.Equal(t, Some(3), result)
|
||||
})
|
||||
|
||||
t.Run("both None", func(t *testing.T) {
|
||||
result := optMonoid.Concat(None[int](), None[int]())
|
||||
assert.Equal(t, None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("empty value", func(t *testing.T) {
|
||||
empty := optMonoid.Empty()
|
||||
assert.Equal(t, None[int](), empty)
|
||||
})
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(optMonoid.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(x, optMonoid.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
a := Some(1)
|
||||
b := Some(2)
|
||||
c := Some(3)
|
||||
|
||||
left := optMonoid.Concat(optMonoid.Concat(a, b), c)
|
||||
right := optMonoid.Concat(a, optMonoid.Concat(b, c))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, Some(1), left)
|
||||
})
|
||||
|
||||
t.Run("multiple concatenations", func(t *testing.T) {
|
||||
// Should return the first Some value encountered
|
||||
result := optMonoid.Concat(
|
||||
optMonoid.Concat(None[int](), Some(1)),
|
||||
optMonoid.Concat(Some(2), Some(3)),
|
||||
)
|
||||
assert.Equal(t, Some(1), result)
|
||||
})
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
strMonoid := FirstMonoid[string]()
|
||||
|
||||
result := strMonoid.Concat(Some("first"), Some("second"))
|
||||
assert.Equal(t, Some("first"), result)
|
||||
|
||||
result = strMonoid.Concat(None[string](), Some("second"))
|
||||
assert.Equal(t, Some("second"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLastMonoid tests the LastMonoid implementation
|
||||
func TestLastMonoid(t *testing.T) {
|
||||
optMonoid := LastMonoid[int]()
|
||||
|
||||
t.Run("both Some values - returns last", func(t *testing.T) {
|
||||
result := optMonoid.Concat(Some(2), Some(3))
|
||||
assert.Equal(t, Some(3), result)
|
||||
})
|
||||
|
||||
t.Run("left Some, right None", func(t *testing.T) {
|
||||
result := optMonoid.Concat(Some(2), None[int]())
|
||||
assert.Equal(t, Some(2), result)
|
||||
})
|
||||
|
||||
t.Run("left None, right Some", func(t *testing.T) {
|
||||
result := optMonoid.Concat(None[int](), Some(3))
|
||||
assert.Equal(t, Some(3), result)
|
||||
})
|
||||
|
||||
t.Run("both None", func(t *testing.T) {
|
||||
result := optMonoid.Concat(None[int](), None[int]())
|
||||
assert.Equal(t, None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("empty value", func(t *testing.T) {
|
||||
empty := optMonoid.Empty()
|
||||
assert.Equal(t, None[int](), empty)
|
||||
})
|
||||
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(optMonoid.Empty(), x)
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
x := Some(5)
|
||||
result := optMonoid.Concat(x, optMonoid.Empty())
|
||||
assert.Equal(t, x, result)
|
||||
})
|
||||
|
||||
t.Run("associativity", func(t *testing.T) {
|
||||
a := Some(1)
|
||||
b := Some(2)
|
||||
c := Some(3)
|
||||
|
||||
left := optMonoid.Concat(optMonoid.Concat(a, b), c)
|
||||
right := optMonoid.Concat(a, optMonoid.Concat(b, c))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
assert.Equal(t, Some(3), left)
|
||||
})
|
||||
|
||||
t.Run("multiple concatenations", func(t *testing.T) {
|
||||
// Should return the last Some value encountered
|
||||
result := optMonoid.Concat(
|
||||
optMonoid.Concat(Some(1), Some(2)),
|
||||
optMonoid.Concat(Some(3), None[int]()),
|
||||
)
|
||||
assert.Equal(t, Some(3), result)
|
||||
})
|
||||
|
||||
t.Run("with strings", func(t *testing.T) {
|
||||
strMonoid := LastMonoid[string]()
|
||||
|
||||
result := strMonoid.Concat(Some("first"), Some("second"))
|
||||
assert.Equal(t, Some("second"), result)
|
||||
|
||||
result = strMonoid.Concat(Some("first"), None[string]())
|
||||
assert.Equal(t, Some("first"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstMonoidVsAltMonoid verifies FirstMonoid and AltMonoid have the same behavior
|
||||
func TestFirstMonoidVsAltMonoid(t *testing.T) {
|
||||
firstMonoid := FirstMonoid[int]()
|
||||
altMonoid := AltMonoid[int]()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
left Option[int]
|
||||
right Option[int]
|
||||
}{
|
||||
{"both Some", Some(1), Some(2)},
|
||||
{"left Some, right None", Some(1), None[int]()},
|
||||
{"left None, right Some", None[int](), Some(2)},
|
||||
{"both None", None[int](), None[int]()},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(tc.left, tc.right)
|
||||
altResult := altMonoid.Concat(tc.left, tc.right)
|
||||
assert.Equal(t, firstResult, altResult, "FirstMonoid and AltMonoid should behave the same")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFirstMonoidVsLastMonoid verifies the difference between FirstMonoid and LastMonoid
|
||||
func TestFirstMonoidVsLastMonoid(t *testing.T) {
|
||||
firstMonoid := FirstMonoid[int]()
|
||||
lastMonoid := LastMonoid[int]()
|
||||
|
||||
t.Run("both Some - different results", func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(Some(1), Some(2))
|
||||
lastResult := lastMonoid.Concat(Some(1), Some(2))
|
||||
|
||||
assert.Equal(t, Some(1), firstResult)
|
||||
assert.Equal(t, Some(2), lastResult)
|
||||
assert.NotEqual(t, firstResult, lastResult)
|
||||
})
|
||||
|
||||
t.Run("with None - same results", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
left Option[int]
|
||||
right Option[int]
|
||||
expected Option[int]
|
||||
}{
|
||||
{"left Some, right None", Some(1), None[int](), Some(1)},
|
||||
{"left None, right Some", None[int](), Some(2), Some(2)},
|
||||
{"both None", None[int](), None[int](), None[int]()},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
firstResult := firstMonoid.Concat(tc.left, tc.right)
|
||||
lastResult := lastMonoid.Concat(tc.left, tc.right)
|
||||
|
||||
assert.Equal(t, tc.expected, firstResult)
|
||||
assert.Equal(t, tc.expected, lastResult)
|
||||
assert.Equal(t, firstResult, lastResult)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidComparison compares different monoid implementations
|
||||
func TestMonoidComparison(t *testing.T) {
|
||||
t.Run("Monoid vs AlternativeMonoid with addition", func(t *testing.T) {
|
||||
intSemigroup := S.MakeSemigroup(func(a, b int) int { return a + b })
|
||||
regularMonoid := Monoid[int]()(intSemigroup)
|
||||
|
||||
intMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
|
||||
altMonoid := AlternativeMonoid(intMonoid)
|
||||
|
||||
// Both should combine Some values the same way
|
||||
assert.Equal(t,
|
||||
regularMonoid.Concat(Some(2), Some(3)),
|
||||
altMonoid.Concat(Some(2), Some(3)),
|
||||
)
|
||||
|
||||
// But empty values differ
|
||||
assert.Equal(t, None[int](), regularMonoid.Empty())
|
||||
assert.Equal(t, Some(0), altMonoid.Empty())
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonoidLaws verifies monoid laws for all monoid implementations
|
||||
func TestMonoidLaws(t *testing.T) {
|
||||
t.Run("Monoid with addition", func(t *testing.T) {
|
||||
intSemigroup := N.SemigroupSum[int]()
|
||||
optMonoid := Monoid[int]()(intSemigroup)
|
||||
|
||||
a := Some(1)
|
||||
b := Some(2)
|
||||
c := Some(3)
|
||||
|
||||
// Associativity: (a • b) • c = a • (b • c)
|
||||
left := optMonoid.Concat(optMonoid.Concat(a, b), c)
|
||||
right := optMonoid.Concat(a, optMonoid.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
|
||||
// Left identity: Empty() • a = a
|
||||
leftId := optMonoid.Concat(optMonoid.Empty(), a)
|
||||
assert.Equal(t, a, leftId)
|
||||
|
||||
// Right identity: a • Empty() = a
|
||||
rightId := optMonoid.Concat(a, optMonoid.Empty())
|
||||
assert.Equal(t, a, rightId)
|
||||
})
|
||||
|
||||
t.Run("FirstMonoid laws", func(t *testing.T) {
|
||||
optMonoid := FirstMonoid[int]()
|
||||
|
||||
a := Some(1)
|
||||
b := Some(2)
|
||||
c := Some(3)
|
||||
|
||||
// Associativity
|
||||
left := optMonoid.Concat(optMonoid.Concat(a, b), c)
|
||||
right := optMonoid.Concat(a, optMonoid.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
|
||||
// Left identity
|
||||
leftId := optMonoid.Concat(optMonoid.Empty(), a)
|
||||
assert.Equal(t, a, leftId)
|
||||
|
||||
// Right identity
|
||||
rightId := optMonoid.Concat(a, optMonoid.Empty())
|
||||
assert.Equal(t, a, rightId)
|
||||
})
|
||||
|
||||
t.Run("LastMonoid laws", func(t *testing.T) {
|
||||
optMonoid := LastMonoid[int]()
|
||||
|
||||
a := Some(1)
|
||||
b := Some(2)
|
||||
c := Some(3)
|
||||
|
||||
// Associativity
|
||||
left := optMonoid.Concat(optMonoid.Concat(a, b), c)
|
||||
right := optMonoid.Concat(a, optMonoid.Concat(b, c))
|
||||
assert.Equal(t, left, right)
|
||||
|
||||
// Left identity
|
||||
leftId := optMonoid.Concat(optMonoid.Empty(), a)
|
||||
assert.Equal(t, a, leftId)
|
||||
|
||||
// Right identity
|
||||
rightId := optMonoid.Concat(a, optMonoid.Empty())
|
||||
assert.Equal(t, a, rightId)
|
||||
})
|
||||
}
|
||||
320
v2/ord/monoid_test.go
Normal file
320
v2/ord/monoid_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
59
v2/ord/types.go
Normal 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
203
v2/ord/types_test.go
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
315
v2/pair/monoid.go
Normal 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
756
v2/pair/monoid_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func MonadMapTail[A, B, B1 any](fa Pair[A, B], f func(B) B1) Pair[A, B1] {
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := 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}
|
||||
//
|
||||
//go:inline
|
||||
@@ -202,7 +202,7 @@ func MonadBiMap[A, B, A1, B1 any](fa Pair[A, B], f func(A) A1, g func(B) B1) Pai
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mapper := pair.Map[int](func(s string) int { return len(s) })
|
||||
// mapper := pair.Map[int](S.Size)
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[int, int]{5, 5}
|
||||
//
|
||||
@@ -232,7 +232,7 @@ func MapHead[B, A, A1 any](f func(A) A1) func(Pair[A, B]) Pair[A1, B] {
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// mapper := pair.MapTail[int](func(s string) int { return len(s) })
|
||||
// mapper := pair.MapTail[int](S.Size)
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[int, int]{5, 5}
|
||||
//
|
||||
@@ -248,7 +248,7 @@ func MapTail[A, B, B1 any](f func(B) B1) Operator[A, B, B1] {
|
||||
//
|
||||
// mapper := pair.BiMap(
|
||||
// func(n int) string { return fmt.Sprintf("%d", n) },
|
||||
// func(s string) int { return len(s) },
|
||||
// S.Size,
|
||||
// )
|
||||
// p := pair.MakePair(5, "hello")
|
||||
// p2 := mapper(p) // Pair[string, int]{"5", 5}
|
||||
@@ -400,7 +400,7 @@ func MonadApHead[B, A, A1 any](sg Semigroup[B], faa Pair[func(A) A1, B], fa Pair
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// result := pair.MonadApTail(intSum, pf, pv) // Pair[int, int]{15, 5}
|
||||
//
|
||||
@@ -417,7 +417,7 @@ func MonadApTail[A, B, B1 any](sg Semigroup[A], fbb Pair[A, func(B) B1], fb Pair
|
||||
// import N "github.com/IBM/fp-go/v2/number"
|
||||
//
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// result := pair.MonadAp(intSum, pf, pv) // Pair[int, int]{15, 5}
|
||||
//
|
||||
@@ -454,7 +454,7 @@ func ApHead[B, A, A1 any](sg Semigroup[B], fa Pair[A, B]) func(Pair[func(A) A1,
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// ap := pair.ApTail(intSum, pv)
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// result := ap(pf) // Pair[int, int]{15, 5}
|
||||
func ApTail[A, B, B1 any](sg Semigroup[A], fb Pair[A, B]) Operator[A, func(B) B1, B1] {
|
||||
return func(fbb Pair[A, func(B) B1]) Pair[A, B1] {
|
||||
@@ -472,7 +472,7 @@ func ApTail[A, B, B1 any](sg Semigroup[A], fb Pair[A, B]) Operator[A, func(B) B1
|
||||
// intSum := N.SemigroupSum[int]()
|
||||
// pv := pair.MakePair(5, "hello")
|
||||
// ap := pair.Ap(intSum, pv)
|
||||
// pf := pair.MakePair(10, func(s string) int { return len(s) })
|
||||
// pf := pair.MakePair(10, S.Size)
|
||||
// result := ap(pf) // Pair[int, int]{15, 5}
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -60,6 +60,8 @@ import (
|
||||
// - You need to partially apply environments in a different order
|
||||
// - You're composing functions that expect parameters in reverse order
|
||||
// - You want to curry multi-parameter functions differently
|
||||
//
|
||||
//go:inline
|
||||
func Sequence[R1, R2, A any](ma Reader[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return function.Flip(ma)
|
||||
}
|
||||
|
||||
78
v2/reader/profunctor.go
Normal file
78
v2/reader/profunctor.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 reader
|
||||
|
||||
import "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a Reader.
|
||||
// It applies f to the input (contravariantly) and g to the output (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Port int }
|
||||
// type Env struct { Config Config }
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// extractConfig := func(e Env) Config { return e.Config }
|
||||
// toString := strconv.Itoa
|
||||
// r := reader.Promap(extractConfig, toString)(getPort)
|
||||
// result := r(Env{Config: Config{Port: 8080}}) // "8080"
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, Reader[E, A], B] {
|
||||
return function.Bind13of3(function.Flow3[func(D) E, func(E) A, func(A) B])(f, g)
|
||||
}
|
||||
|
||||
// Local changes the value of the local context during the execution of the action `ma`.
|
||||
// This is similar to Contravariant's contramap and allows you to modify the environment
|
||||
// before passing it to a Reader.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DetailedConfig struct { Host string; Port int }
|
||||
// type SimpleConfig struct { Host string }
|
||||
// getHost := func(c SimpleConfig) string { return c.Host }
|
||||
// simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
// r := reader.Local(simplify)(getHost)
|
||||
// result := r(DetailedConfig{Host: "localhost", Port: 8080}) // "localhost"
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
return Compose[A](f)
|
||||
}
|
||||
|
||||
// Contramap is an alias for Local.
|
||||
// It changes the value of the local context during the execution of a Reader.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// Contramap is semantically identical to Local - both modify the environment before
|
||||
// passing it to a Reader. The name "Contramap" emphasizes the contravariant nature
|
||||
// of the transformation (transforming the input rather than the output).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DetailedConfig struct { Host string; Port int }
|
||||
// type SimpleConfig struct { Host string }
|
||||
// getHost := func(c SimpleConfig) string { return c.Host }
|
||||
// simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
// r := reader.Contramap(simplify)(getHost)
|
||||
// result := r(DetailedConfig{Host: "localhost", Port: 8080}) // "localhost"
|
||||
//
|
||||
// See also: Local
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
return Compose[A](f)
|
||||
}
|
||||
@@ -249,6 +249,34 @@ func MonadChain[R, A, B any](ma Reader[R, A], f Kleisli[R, A, B]) Reader[R, B] {
|
||||
// Chain sequences two Reader computations where the second depends on the result of the first.
|
||||
// This is the Monad operation that enables dependent computations.
|
||||
//
|
||||
// Relationship with Compose:
|
||||
//
|
||||
// Chain and Compose serve different purposes in Reader composition:
|
||||
//
|
||||
// - Chain: Monadic composition - sequences Readers that share the SAME environment type.
|
||||
// The second Reader depends on the VALUE produced by the first Reader, but both
|
||||
// Readers receive the same environment R. This is the monadic bind (>>=) operation.
|
||||
// Signature: Chain[R, A, B](f: A -> Reader[R, B]) -> Reader[R, A] -> Reader[R, B]
|
||||
//
|
||||
// - Compose: Function composition - chains Readers where the OUTPUT of the first
|
||||
// becomes the INPUT environment of the second. The environment types can differ.
|
||||
// This is standard function composition (.) for Readers as functions.
|
||||
// Signature: Compose[C, R, B](ab: Reader[R, B]) -> Reader[B, C] -> Reader[R, C]
|
||||
//
|
||||
// Key Differences:
|
||||
//
|
||||
// 1. Environment handling:
|
||||
// - Chain: Both Readers use the same environment R
|
||||
// - Compose: First Reader's output B becomes second Reader's input environment
|
||||
//
|
||||
// 2. Data flow:
|
||||
// - Chain: R -> A, then A -> Reader[R, B], both using same R
|
||||
// - Compose: R -> B, then B -> C (B is both output and environment)
|
||||
//
|
||||
// 3. Use cases:
|
||||
// - Chain: Dependent computations in the same context (e.g., fetch user, then fetch user's posts)
|
||||
// - Compose: Transforming nested environments (e.g., extract config from app state, then read from config)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { UserId int }
|
||||
@@ -360,6 +388,53 @@ func Flatten[R, A any](mma Reader[R, Reader[R, A]]) Reader[R, A] {
|
||||
// Compose composes two Readers sequentially, where the output environment of the first
|
||||
// becomes the input environment of the second.
|
||||
//
|
||||
// Relationship with Chain:
|
||||
//
|
||||
// Compose and Chain serve different purposes in Reader composition:
|
||||
//
|
||||
// - Compose: Function composition - chains Readers where the OUTPUT of the first
|
||||
// becomes the INPUT environment of the second. The environment types can differ.
|
||||
// This is standard function composition (.) for Readers as functions.
|
||||
// Signature: Compose[C, R, B](ab: Reader[R, B]) -> Reader[B, C] -> Reader[R, C]
|
||||
//
|
||||
// - Chain: Monadic composition - sequences Readers that share the SAME environment type.
|
||||
// The second Reader depends on the VALUE produced by the first Reader, but both
|
||||
// Readers receive the same environment R. This is the monadic bind (>>=) operation.
|
||||
// Signature: Chain[R, A, B](f: A -> Reader[R, B]) -> Reader[R, A] -> Reader[R, B]
|
||||
//
|
||||
// Key Differences:
|
||||
//
|
||||
// 1. Environment handling:
|
||||
// - Compose: First Reader's output B becomes second Reader's input environment
|
||||
// - Chain: Both Readers use the same environment R
|
||||
//
|
||||
// 2. Data flow:
|
||||
// - Compose: R -> B, then B -> C (B is both output and environment)
|
||||
// - Chain: R -> A, then A -> Reader[R, B], both using same R
|
||||
//
|
||||
// 3. Use cases:
|
||||
// - Compose: Transforming nested environments (e.g., extract config from app state, then read from config)
|
||||
// - Chain: Dependent computations in the same context (e.g., fetch user, then fetch user's posts)
|
||||
//
|
||||
// Visual Comparison:
|
||||
//
|
||||
// // Compose: Environment transformation
|
||||
// type AppState struct { Config Config }
|
||||
// type Config struct { Port int }
|
||||
// getConfig := func(s AppState) Config { return s.Config }
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// getPortFromState := reader.Compose(getConfig)(getPort)
|
||||
// // Flow: AppState -> Config -> int (Config is both output and next input)
|
||||
//
|
||||
// // Chain: Same environment, dependent values
|
||||
// type Env struct { UserId int; Users map[int]string }
|
||||
// getUserId := func(e Env) int { return e.UserId }
|
||||
// getUser := func(id int) reader.Reader[Env, string] {
|
||||
// return func(e Env) string { return e.Users[id] }
|
||||
// }
|
||||
// getUserName := reader.Chain(getUser)(getUserId)
|
||||
// // Flow: Env -> int, then int -> Reader[Env, string] (Env used twice)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Port int }
|
||||
@@ -367,10 +442,10 @@ func Flatten[R, A any](mma Reader[R, Reader[R, A]]) Reader[R, A] {
|
||||
// getConfig := func(e Env) Config { return e.Config }
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// getPortFromEnv := reader.Compose(getConfig)(getPort)
|
||||
//
|
||||
//go:inline
|
||||
func Compose[C, R, B any](ab Reader[R, B]) Kleisli[R, Reader[B, C], C] {
|
||||
return func(bc Reader[B, C]) Reader[R, C] {
|
||||
return function.Flow2(ab, bc)
|
||||
}
|
||||
return function.Bind1st(function.Flow2[Reader[R, B], Reader[B, C]], ab)
|
||||
}
|
||||
|
||||
// First applies a Reader to the first element of a tuple, leaving the second element unchanged.
|
||||
@@ -401,42 +476,6 @@ func Second[A, B, C any](pbc Reader[B, C]) Reader[T.Tuple2[A, B], T.Tuple2[A, C]
|
||||
}
|
||||
}
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a Reader.
|
||||
// It applies f to the input (contravariantly) and g to the output (covariantly).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Port int }
|
||||
// type Env struct { Config Config }
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// extractConfig := func(e Env) Config { return e.Config }
|
||||
// toString := strconv.Itoa
|
||||
// r := reader.Promap(extractConfig, toString)(getPort)
|
||||
// result := r(Env{Config: Config{Port: 8080}}) // "8080"
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, Reader[E, A], B] {
|
||||
return func(fea Reader[E, A]) Reader[D, B] {
|
||||
return function.Flow3(f, fea, g)
|
||||
}
|
||||
}
|
||||
|
||||
// Local changes the value of the local context during the execution of the action `ma`.
|
||||
// This is similar to Contravariant's contramap and allows you to modify the environment
|
||||
// before passing it to a Reader.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type DetailedConfig struct { Host string; Port int }
|
||||
// type SimpleConfig struct { Host string }
|
||||
// getHost := func(c SimpleConfig) string { return c.Host }
|
||||
// simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
// r := reader.Local(simplify)(getHost)
|
||||
// result := r(DetailedConfig{Host: "localhost", Port: 8080}) // "localhost"
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R2, R1 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
return Compose[A](f)
|
||||
}
|
||||
|
||||
// Read applies a context to a Reader to obtain its value.
|
||||
// This is the "run" operation that executes a Reader with a specific environment.
|
||||
//
|
||||
@@ -446,8 +485,10 @@ func Local[A, R2, R1 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
// getPort := reader.Asks(func(c Config) int { return c.Port })
|
||||
// run := reader.Read(Config{Port: 8080})
|
||||
// port := run(getPort) // 8080
|
||||
//
|
||||
//go:inline
|
||||
func Read[A, E any](e E) func(Reader[E, A]) A {
|
||||
return I.Ap[A](e)
|
||||
return I.Flap[A](e)
|
||||
}
|
||||
|
||||
// MonadFlap is the monadic version of Flap.
|
||||
@@ -461,6 +502,8 @@ func Read[A, E any](e E) func(Reader[E, A]) A {
|
||||
// }
|
||||
// r := reader.MonadFlap(getMultiplier, 5)
|
||||
// result := r(Config{Multiplier: 3}) // 15
|
||||
//
|
||||
//go:inline
|
||||
func MonadFlap[R, B, A any](fab Reader[R, func(A) B], a A) Reader[R, B] {
|
||||
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
|
||||
}
|
||||
@@ -477,6 +520,8 @@ func MonadFlap[R, B, A any](fab Reader[R, func(A) B], a A) Reader[R, B] {
|
||||
// applyTo5 := reader.Flap[Config](5)
|
||||
// r := applyTo5(getMultiplier)
|
||||
// result := r(Config{Multiplier: 3}) // 15
|
||||
//
|
||||
//go:inline
|
||||
func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
return functor.Flap(Map[R, func(A) B, B], a)
|
||||
}
|
||||
|
||||
76
v2/readereither/profunctor.go
Normal file
76
v2/readereither/profunctor.go
Normal 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 readereither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderEither.
|
||||
// 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 ReaderEither (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// The error type E remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderEither
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The original success type produced by the ReaderEither
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderEither[R, E, A] and returns a ReaderEither[D, E, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, E, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, E, ReaderEither[R, E, A], B] {
|
||||
return reader.Promap(f, either.Map[E](g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderEither.
|
||||
// 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 ReaderEither to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderEither
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderEither[R1, E, A] and returns a ReaderEither[R2, E, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[E, A, R1, R2 any](f func(R2) R1) Kleisli[R2, E, ReaderEither[R1, E, A], A] {
|
||||
return reader.Contramap[Either[E, A]](f)
|
||||
}
|
||||
135
v2/readereither/profunctor_test.go
Normal file
135
v2/readereither/profunctor_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// 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 readereither
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
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) {
|
||||
// ReaderEither that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, E.Of[string]("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderEither that returns an error
|
||||
getError := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Left[int]("error occurred")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, E.Left[string]("error occurred"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderEither that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, E.Of[string](9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) Either[string, int] {
|
||||
return E.Left[int]("config error")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, E.Left[int]("config error"), 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) Either[string, int] {
|
||||
return E.Of[string](c.Value)
|
||||
}
|
||||
|
||||
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[Config1, string](f1, g1)(reader)
|
||||
step2 := Promap[Config2, string](f2, g2)(step1)
|
||||
|
||||
result := step2(Config3{Value: 5})
|
||||
|
||||
// (5 * 2) + 10 = 20
|
||||
assert.Equal(t, E.Of[string](20), result)
|
||||
})
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func BiMap[E, E1, E2, A, B any](f func(E1) E2, g func(A) B) func(ReaderEither[E,
|
||||
|
||||
// Local changes the value of the local context during the execution of the action `ma` (similar to `Contravariant`'s
|
||||
// `contramap`).
|
||||
func Local[E, A, R2, R1 any](f func(R2) R1) func(ReaderEither[R1, E, A]) ReaderEither[R2, E, A] {
|
||||
func Local[E, A, R1, R2 any](f func(R2) R1) func(ReaderEither[R1, E, A]) ReaderEither[R2, E, A] {
|
||||
return reader.Local[Either[E, A]](f)
|
||||
}
|
||||
|
||||
@@ -160,6 +160,66 @@ func Read[E1, A, E any](e E) func(ReaderEither[E, E1, A]) Either[E1, A] {
|
||||
return reader.Read[Either[E1, A]](e)
|
||||
}
|
||||
|
||||
// ReadEither applies a context wrapped in an Either to a ReaderEither to obtain its result.
|
||||
// This function is useful when the context itself may be absent or invalid (represented as Left),
|
||||
// allowing you to conditionally execute a ReaderEither computation based on the availability
|
||||
// of the required context.
|
||||
//
|
||||
// If the context Either is Left, it short-circuits and returns Left without executing the ReaderEither.
|
||||
// If the context Either is Right, it extracts the context value and applies it to the ReaderEither,
|
||||
// returning the resulting Either.
|
||||
//
|
||||
// This is particularly useful in scenarios where:
|
||||
// - Configuration or dependencies may be missing or invalid
|
||||
// - You want to chain context validation with computation execution
|
||||
// - You need to propagate context errors through your computation pipeline
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E1: The error type (Left value) of both the input Either and the ReaderEither result
|
||||
// - A: The success type (Right value) of the ReaderEither result
|
||||
// - E: The context/environment type that the ReaderEither depends on
|
||||
//
|
||||
// Parameters:
|
||||
// - e: An Either[E1, E] representing the context that may or may not be available
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderEither[E, E1, A] and returns Either[E1, A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ apiKey string }
|
||||
// type ConfigError struct{ msg string }
|
||||
//
|
||||
// // A computation that needs config
|
||||
// fetchData := func(cfg Config) either.Either[ConfigError, string] {
|
||||
// if cfg.apiKey == "" {
|
||||
// return either.Left[string](ConfigError{"missing API key"})
|
||||
// }
|
||||
// return either.Right[ConfigError]("data from API")
|
||||
// }
|
||||
//
|
||||
// // Context may be invalid
|
||||
// validConfig := either.Right[ConfigError](Config{apiKey: "secret"})
|
||||
// invalidConfig := either.Left[Config](ConfigError{"config not found"})
|
||||
//
|
||||
// computation := readereither.FromReader[ConfigError](fetchData)
|
||||
//
|
||||
// // With valid config - executes computation
|
||||
// result1 := readereither.ReadEither(validConfig)(computation)
|
||||
// // result1 = Right("data from API")
|
||||
//
|
||||
// // With invalid config - short-circuits without executing
|
||||
// result2 := readereither.ReadEither(invalidConfig)(computation)
|
||||
// // result2 = Left(ConfigError{"config not found"})
|
||||
//
|
||||
//go:inline
|
||||
func ReadEither[E1, A, E any](e Either[E1, E]) func(ReaderEither[E, E1, A]) Either[E1, A] {
|
||||
return function.Flow2(
|
||||
ET.Chain[E1, E],
|
||||
Read[E1, A](e),
|
||||
)
|
||||
}
|
||||
|
||||
func MonadFlap[L, E, A, B any](fab ReaderEither[L, E, func(A) B], a A) ReaderEither[L, E, B] {
|
||||
return functor.MonadFlap(MonadMap[L, E, func(A) B, B], fab, a)
|
||||
}
|
||||
|
||||
@@ -223,3 +223,164 @@ func TestOrElse(t *testing.T) {
|
||||
appResult := wideningRecover(validationErr)(Config{})
|
||||
assert.Equal(t, ET.Right[AppError](100), appResult)
|
||||
}
|
||||
|
||||
func TestReadEither(t *testing.T) {
|
||||
type Config struct {
|
||||
apiKey string
|
||||
host string
|
||||
}
|
||||
|
||||
// Test with Right context - should execute the ReaderEither
|
||||
t.Run("Right context executes computation", func(t *testing.T) {
|
||||
validConfig := ET.Right[string](Config{apiKey: "secret", host: "localhost"})
|
||||
|
||||
computation := func(cfg Config) Either[string, int] {
|
||||
if cfg.apiKey == "secret" {
|
||||
return ET.Right[string](42)
|
||||
}
|
||||
return ET.Left[int]("invalid key")
|
||||
}
|
||||
|
||||
result := ReadEither[string, int](validConfig)(computation)
|
||||
assert.Equal(t, ET.Right[string](42), result)
|
||||
})
|
||||
|
||||
// Test with Right context but computation fails
|
||||
t.Run("Right context with failing computation", func(t *testing.T) {
|
||||
validConfig := ET.Right[string](Config{apiKey: "wrong", host: "localhost"})
|
||||
|
||||
computation := func(cfg Config) Either[string, int] {
|
||||
if cfg.apiKey == "secret" {
|
||||
return ET.Right[string](42)
|
||||
}
|
||||
return ET.Left[int]("invalid key")
|
||||
}
|
||||
|
||||
result := ReadEither[string, int](validConfig)(computation)
|
||||
assert.Equal(t, ET.Left[int]("invalid key"), result)
|
||||
})
|
||||
|
||||
// Test with Left context - should short-circuit without executing
|
||||
t.Run("Left context short-circuits", func(t *testing.T) {
|
||||
invalidConfig := ET.Left[Config]("config not found")
|
||||
|
||||
executed := false
|
||||
computation := func(cfg Config) Either[string, int] {
|
||||
executed = true
|
||||
return ET.Right[string](42)
|
||||
}
|
||||
|
||||
result := ReadEither[string, int](invalidConfig)(computation)
|
||||
assert.Equal(t, ET.Left[int]("config not found"), result)
|
||||
assert.False(t, executed, "computation should not be executed with Left context")
|
||||
})
|
||||
|
||||
// Test with complex ReaderEither computation
|
||||
t.Run("Complex ReaderEither computation", func(t *testing.T) {
|
||||
validConfig := ET.Right[string](Config{apiKey: "secret", host: "api.example.com"})
|
||||
|
||||
// A more complex computation using the config
|
||||
computation := F.Pipe2(
|
||||
Ask[Config, string](),
|
||||
Map[Config, string](func(cfg Config) string {
|
||||
return cfg.host + "/data"
|
||||
}),
|
||||
Chain[Config, string, string, int](func(url string) ReaderEither[Config, string, int] {
|
||||
return func(cfg Config) Either[string, int] {
|
||||
if cfg.apiKey != "" {
|
||||
return ET.Right[string](len(url))
|
||||
}
|
||||
return ET.Left[int]("no API key")
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result := ReadEither[string, int](validConfig)(computation)
|
||||
assert.Equal(t, ET.Right[string](20), result) // len("api.example.com/data") = 20
|
||||
})
|
||||
|
||||
// Test error type consistency
|
||||
t.Run("Error type consistency", func(t *testing.T) {
|
||||
type AppError struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
configError := AppError{code: 404, message: "config not found"}
|
||||
invalidConfig := ET.Left[Config](configError)
|
||||
|
||||
computation := func(cfg Config) Either[AppError, string] {
|
||||
return ET.Right[AppError]("success")
|
||||
}
|
||||
|
||||
result := ReadEither[AppError, string](invalidConfig)(computation)
|
||||
assert.Equal(t, ET.Left[string](configError), result)
|
||||
})
|
||||
|
||||
// Test with chained operations
|
||||
t.Run("Chained operations with ReadEither", func(t *testing.T) {
|
||||
config1 := ET.Right[string](Config{apiKey: "key1", host: "host1"})
|
||||
config2 := ET.Right[string](Config{apiKey: "key2", host: "host2"})
|
||||
|
||||
computation := func(cfg Config) Either[string, string] {
|
||||
return ET.Right[string](cfg.host)
|
||||
}
|
||||
|
||||
// Apply first config
|
||||
result1 := ReadEither[string, string](config1)(computation)
|
||||
assert.Equal(t, ET.Right[string]("host1"), result1)
|
||||
|
||||
// Apply second config
|
||||
result2 := ReadEither[string, string](config2)(computation)
|
||||
assert.Equal(t, ET.Right[string]("host2"), result2)
|
||||
})
|
||||
|
||||
// Test with FromReader
|
||||
t.Run("ReadEither with FromReader", func(t *testing.T) {
|
||||
validConfig := ET.Right[string](Config{apiKey: "secret", host: "localhost"})
|
||||
|
||||
// Create a ReaderEither from a Reader
|
||||
readerComputation := func(cfg Config) int {
|
||||
return len(cfg.apiKey)
|
||||
}
|
||||
|
||||
computation := FromReader[string](readerComputation)
|
||||
|
||||
result := ReadEither[string, int](validConfig)(computation)
|
||||
assert.Equal(t, ET.Right[string](6), result) // len("secret") = 6
|
||||
})
|
||||
|
||||
// Test with Of (pure value)
|
||||
t.Run("ReadEither with pure value", func(t *testing.T) {
|
||||
validConfig := ET.Right[string](Config{apiKey: "secret", host: "localhost"})
|
||||
computation := Of[Config, string](100)
|
||||
|
||||
result := ReadEither[string, int](validConfig)(computation)
|
||||
assert.Equal(t, ET.Right[string](100), result)
|
||||
})
|
||||
|
||||
// Test with Left computation
|
||||
t.Run("ReadEither with Left computation", func(t *testing.T) {
|
||||
validConfig := ET.Right[string](Config{apiKey: "secret", host: "localhost"})
|
||||
computation := Left[Config, int]("computation error")
|
||||
|
||||
result := ReadEither[string, int](validConfig)(computation)
|
||||
assert.Equal(t, ET.Left[int]("computation error"), result)
|
||||
})
|
||||
|
||||
// Test composition with Read
|
||||
t.Run("ReadEither vs Read comparison", func(t *testing.T) {
|
||||
config := Config{apiKey: "secret", host: "localhost"}
|
||||
computation := func(cfg Config) Either[string, int] {
|
||||
return ET.Right[string](len(cfg.apiKey))
|
||||
}
|
||||
|
||||
// Using Read directly
|
||||
resultRead := Read[string, int](config)(computation)
|
||||
|
||||
// Using ReadEither with Right
|
||||
resultReadEither := ReadEither[string, int](ET.Right[string](config))(computation)
|
||||
|
||||
assert.Equal(t, resultRead, resultReadEither)
|
||||
})
|
||||
}
|
||||
|
||||
236
v2/readerio/profunctor.go
Normal file
236
v2/readerio/profunctor.go
Normal file
@@ -0,0 +1,236 @@
|
||||
// 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 (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIO.
|
||||
// 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 ReaderIO (via f)
|
||||
// - Transform the result value after the IO effect completes (via g)
|
||||
//
|
||||
// The transformation happens in two stages:
|
||||
// 1. The input environment D is transformed to E using f (contravariant)
|
||||
// 2. The ReaderIO[E, A] is executed with the transformed environment
|
||||
// 3. The result value A is transformed to B using g (covariant) within the IO context
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The original environment type expected by the ReaderIO
|
||||
// - A: The original result type produced by the ReaderIO
|
||||
// - D: The new input environment type
|
||||
// - B: The new output result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to E (contravariant)
|
||||
// - g: Function to transform the output value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIO[E, A] and returns a ReaderIO[D, B]
|
||||
//
|
||||
// Example - Adapting environment and transforming result:
|
||||
//
|
||||
// type DetailedConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// Debug bool
|
||||
// }
|
||||
//
|
||||
// type SimpleConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // ReaderIO that reads port from SimpleConfig
|
||||
// getPort := readerio.Asks(func(c SimpleConfig) io.IO[int] {
|
||||
// return io.Of(c.Port)
|
||||
// })
|
||||
//
|
||||
// // Adapt DetailedConfig to SimpleConfig and convert int to string
|
||||
// simplify := func(d DetailedConfig) SimpleConfig {
|
||||
// return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
// }
|
||||
// toString := strconv.Itoa
|
||||
//
|
||||
// adapted := readerio.Promap(simplify, toString)(getPort)
|
||||
// result := adapted(DetailedConfig{Host: "localhost", Port: 8080, Debug: true})()
|
||||
// // result = "8080"
|
||||
//
|
||||
// Example - Logging with environment transformation:
|
||||
//
|
||||
// type AppEnv struct {
|
||||
// Logger *log.Logger
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// type LoggerEnv struct {
|
||||
// Logger *log.Logger
|
||||
// }
|
||||
//
|
||||
// logMessage := func(msg string) readerio.ReaderIO[LoggerEnv, func()] {
|
||||
// return readerio.Asks(func(env LoggerEnv) io.IO[func()] {
|
||||
// return io.Of(func() { env.Logger.Println(msg) })
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// extractLogger := func(app AppEnv) LoggerEnv {
|
||||
// return LoggerEnv{Logger: app.Logger}
|
||||
// }
|
||||
// ignore := func(func()) string { return "logged" }
|
||||
//
|
||||
// logAndReturn := readerio.Promap(extractLogger, ignore)(logMessage("Hello"))
|
||||
// // Now works with AppEnv and returns string instead of func()
|
||||
//
|
||||
//go:inline
|
||||
func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, ReaderIO[E, A], B] {
|
||||
return reader.Promap(f, io.Map(g))
|
||||
}
|
||||
|
||||
// Local changes the value of the local environment during the execution of a ReaderIO.
|
||||
// This allows you to modify or adapt the environment before passing it to a ReaderIO computation.
|
||||
//
|
||||
// Local is particularly useful for:
|
||||
// - Extracting a subset of a larger environment
|
||||
// - Transforming environment types
|
||||
// - Providing different views of the same environment to different computations
|
||||
//
|
||||
// The transformation is contravariant - it transforms the input environment before
|
||||
// the ReaderIO computation sees it, but doesn't affect the output value.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type produced by the ReaderIO
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIO
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIO[R1, A] and returns a ReaderIO[R2, A]
|
||||
//
|
||||
// Example - Extracting a subset of environment:
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// Database DatabaseConfig
|
||||
// Server ServerConfig
|
||||
// Logger *log.Logger
|
||||
// }
|
||||
//
|
||||
// type DatabaseConfig struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // ReaderIO that only needs DatabaseConfig
|
||||
// connectDB := readerio.Asks(func(cfg DatabaseConfig) io.IO[string] {
|
||||
// return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
|
||||
// })
|
||||
//
|
||||
// // Extract database config from full app config
|
||||
// extractDB := func(app AppConfig) DatabaseConfig {
|
||||
// return app.Database
|
||||
// }
|
||||
//
|
||||
// // Adapt to work with full AppConfig
|
||||
// connectWithAppConfig := readerio.Local(extractDB)(connectDB)
|
||||
// result := connectWithAppConfig(AppConfig{
|
||||
// Database: DatabaseConfig{Host: "localhost", Port: 5432},
|
||||
// })()
|
||||
// // result = "Connected to localhost:5432"
|
||||
//
|
||||
// Example - Providing different views:
|
||||
//
|
||||
// type FullEnv struct {
|
||||
// UserID int
|
||||
// Role string
|
||||
// }
|
||||
//
|
||||
// type UserEnv struct {
|
||||
// UserID int
|
||||
// }
|
||||
//
|
||||
// getUserData := readerio.Asks(func(env UserEnv) io.IO[string] {
|
||||
// return io.Of(fmt.Sprintf("User: %d", env.UserID))
|
||||
// })
|
||||
//
|
||||
// toUserEnv := func(full FullEnv) UserEnv {
|
||||
// return UserEnv{UserID: full.UserID}
|
||||
// }
|
||||
//
|
||||
// adapted := readerio.Local(toUserEnv)(getUserData)
|
||||
// result := adapted(FullEnv{UserID: 42, Role: "admin"})()
|
||||
// // result = "User: 42"
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
|
||||
return reader.Local[IO[A]](f)
|
||||
}
|
||||
|
||||
// Contramap is an alias for Local.
|
||||
// It changes the value of the local environment during the execution of a ReaderIO.
|
||||
// This is the contravariant functor operation that transforms the input environment.
|
||||
//
|
||||
// Contramap is semantically identical to Local - both modify the environment before
|
||||
// passing it to a ReaderIO. The name "Contramap" emphasizes the contravariant nature
|
||||
// of the transformation (transforming the input rather than the output).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type produced by the ReaderIO
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIO
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIO[R1, A] and returns a ReaderIO[R2, A]
|
||||
//
|
||||
// Example - Environment adaptation:
|
||||
//
|
||||
// type DetailedEnv struct {
|
||||
// Config Config
|
||||
// Logger *log.Logger
|
||||
// Metrics Metrics
|
||||
// }
|
||||
//
|
||||
// type SimpleEnv struct {
|
||||
// Config Config
|
||||
// }
|
||||
//
|
||||
// readConfig := readerio.Asks(func(env SimpleEnv) io.IO[string] {
|
||||
// return io.Of(env.Config.Value)
|
||||
// })
|
||||
//
|
||||
// simplify := func(detailed DetailedEnv) SimpleEnv {
|
||||
// return SimpleEnv{Config: detailed.Config}
|
||||
// }
|
||||
//
|
||||
// adapted := readerio.Contramap(simplify)(readConfig)
|
||||
// result := adapted(DetailedEnv{Config: Config{Value: "test"}})()
|
||||
// // result = "test"
|
||||
//
|
||||
// See also: Local
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
|
||||
return reader.Contramap[IO[A]](f)
|
||||
}
|
||||
614
v2/readerio/profunctor_test.go
Normal file
614
v2/readerio/profunctor_test.go
Normal file
@@ -0,0 +1,614 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test environment types
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type SimpleConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Database DatabaseConfig
|
||||
Server ServerConfig
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
type UserEnv struct {
|
||||
UserID int
|
||||
}
|
||||
|
||||
type FullEnv struct {
|
||||
UserID int
|
||||
Role string
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both input and output", func(t *testing.T) {
|
||||
// ReaderIO that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IO[int] {
|
||||
return io.Of(c.Port)
|
||||
}
|
||||
|
||||
// Adapt DetailedConfig to SimpleConfig and convert int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080, Debug: true})()
|
||||
|
||||
assert.Equal(t, "8080", result)
|
||||
})
|
||||
|
||||
t.Run("identity transformations", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 2)
|
||||
}
|
||||
|
||||
// Identity functions should not change behavior
|
||||
identity := reader.Ask[int]()
|
||||
adapted := Promap(identity, identity)(getValue)
|
||||
result := adapted(5)()
|
||||
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
|
||||
t.Run("compose multiple transformations", func(t *testing.T) {
|
||||
getPort := func(c SimpleConfig) IO[int] {
|
||||
return io.Of(c.Port)
|
||||
}
|
||||
|
||||
// First transformation
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
double := N.Mul(2)
|
||||
|
||||
step1 := Promap(simplify, double)(getPort)
|
||||
|
||||
// Second transformation
|
||||
addDebug := func(d DetailedConfig) DetailedConfig {
|
||||
d.Debug = true
|
||||
return d
|
||||
}
|
||||
toString := S.Format[int]("Port: %d")
|
||||
|
||||
step2 := Promap(addDebug, toString)(step1)
|
||||
result := step2(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, "Port: 16160", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithIO tests Promap with actual IO effects
|
||||
func TestPromapWithIO(t *testing.T) {
|
||||
t.Run("transform IO result", func(t *testing.T) {
|
||||
counter := 0
|
||||
getAndIncrement := func(n int) IO[int] {
|
||||
return func() int {
|
||||
counter++
|
||||
return n + counter
|
||||
}
|
||||
}
|
||||
|
||||
double := reader.Ask[int]()
|
||||
toString := S.Format[int]("Result: %d")
|
||||
|
||||
adapted := Promap(double, toString)(getAndIncrement)
|
||||
result := adapted(10)()
|
||||
|
||||
assert.Equal(t, "Result: 11", result)
|
||||
assert.Equal(t, 1, counter)
|
||||
})
|
||||
|
||||
t.Run("environment transformation with side effects", func(t *testing.T) {
|
||||
var log []string
|
||||
|
||||
logAndReturn := func(msg string) IO[string] {
|
||||
return func() string {
|
||||
log = append(log, msg)
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
addPrefix := S.Prepend("Input: ")
|
||||
addSuffix := S.Append(" [processed]")
|
||||
|
||||
adapted := Promap(addPrefix, addSuffix)(logAndReturn)
|
||||
result := adapted("test")()
|
||||
|
||||
assert.Equal(t, "Input: test [processed]", result)
|
||||
assert.Equal(t, []string{"Input: test"}, log)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapEnvironmentExtraction tests extracting subsets of environments
|
||||
func TestPromapEnvironmentExtraction(t *testing.T) {
|
||||
t.Run("extract database config", func(t *testing.T) {
|
||||
connectDB := func(cfg DatabaseConfig) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
|
||||
extractDB := func(app AppConfig) DatabaseConfig {
|
||||
return app.Database
|
||||
}
|
||||
identity := reader.Ask[string]()
|
||||
|
||||
adapted := Promap(extractDB, identity)(connectDB)
|
||||
result := adapted(AppConfig{
|
||||
Database: DatabaseConfig{Host: "localhost", Port: 5432},
|
||||
Server: ServerConfig{Port: 8080, Timeout: 30},
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Connected to localhost:5432", result)
|
||||
})
|
||||
|
||||
t.Run("extract and transform", func(t *testing.T) {
|
||||
getServerPort := func(cfg ServerConfig) IO[int] {
|
||||
return io.Of(cfg.Port)
|
||||
}
|
||||
|
||||
extractServer := func(app AppConfig) ServerConfig {
|
||||
return app.Server
|
||||
}
|
||||
formatPort := func(port int) string {
|
||||
return fmt.Sprintf("Server listening on port %d", port)
|
||||
}
|
||||
|
||||
adapted := Promap(extractServer, formatPort)(getServerPort)
|
||||
result := adapted(AppConfig{
|
||||
Server: ServerConfig{Port: 8080, Timeout: 30},
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Server listening on port 8080", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalBasic tests basic Local functionality
|
||||
func TestLocalBasic(t *testing.T) {
|
||||
t.Run("extract subset of environment", func(t *testing.T) {
|
||||
connectDB := func(cfg DatabaseConfig) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
|
||||
}
|
||||
|
||||
extractDB := func(app AppConfig) DatabaseConfig {
|
||||
return app.Database
|
||||
}
|
||||
|
||||
adapted := Local[string](extractDB)(connectDB)
|
||||
result := adapted(AppConfig{
|
||||
Database: DatabaseConfig{Host: "localhost", Port: 5432},
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Connected to localhost:5432", result)
|
||||
})
|
||||
|
||||
t.Run("transform environment type", func(t *testing.T) {
|
||||
getUserData := func(env UserEnv) IO[string] {
|
||||
return io.Of(fmt.Sprintf("User: %d", env.UserID))
|
||||
}
|
||||
|
||||
toUserEnv := func(full FullEnv) UserEnv {
|
||||
return UserEnv{UserID: full.UserID}
|
||||
}
|
||||
|
||||
adapted := Local[string](toUserEnv)(getUserData)
|
||||
result := adapted(FullEnv{UserID: 42, Role: "admin"})()
|
||||
|
||||
assert.Equal(t, "User: 42", result)
|
||||
})
|
||||
|
||||
t.Run("identity transformation", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 2)
|
||||
}
|
||||
|
||||
identity := reader.Ask[int]()
|
||||
adapted := Local[int](identity)(getValue)
|
||||
result := adapted(5)()
|
||||
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalComposition tests composing Local transformations
|
||||
func TestLocalComposition(t *testing.T) {
|
||||
t.Run("compose two Local transformations", func(t *testing.T) {
|
||||
getPort := func(cfg DatabaseConfig) IO[int] {
|
||||
return io.Of(cfg.Port)
|
||||
}
|
||||
|
||||
extractDB := func(app AppConfig) DatabaseConfig {
|
||||
return app.Database
|
||||
}
|
||||
|
||||
// First Local
|
||||
step1 := Local[int](extractDB)(getPort)
|
||||
|
||||
// Second Local - add default values
|
||||
addDefaults := func(app AppConfig) AppConfig {
|
||||
if app.Database.Host == "" {
|
||||
app.Database.Host = "localhost"
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
step2 := Local[int](addDefaults)(step1)
|
||||
result := step2(AppConfig{
|
||||
Database: DatabaseConfig{Host: "", Port: 5432},
|
||||
})()
|
||||
|
||||
assert.Equal(t, 5432, result)
|
||||
})
|
||||
|
||||
t.Run("chain multiple environment transformations", func(t *testing.T) {
|
||||
getHost := func(cfg SimpleConfig) IO[string] {
|
||||
return io.Of(cfg.Host)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig -> SimpleConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Local[string](simplify)(getHost)
|
||||
result := adapted(DetailedConfig{Host: "example.com", Port: 8080, Debug: true})()
|
||||
|
||||
assert.Equal(t, "example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocalWithIO tests Local with IO effects
|
||||
func TestLocalWithIO(t *testing.T) {
|
||||
t.Run("environment transformation with side effects", func(t *testing.T) {
|
||||
var accessLog []int
|
||||
|
||||
logAccess := func(id int) IO[string] {
|
||||
return func() string {
|
||||
accessLog = append(accessLog, id)
|
||||
return fmt.Sprintf("Accessed: %d", id)
|
||||
}
|
||||
}
|
||||
|
||||
extractUserID := func(env FullEnv) int {
|
||||
return env.UserID
|
||||
}
|
||||
|
||||
adapted := Local[string](extractUserID)(logAccess)
|
||||
result := adapted(FullEnv{UserID: 123, Role: "user"})()
|
||||
|
||||
assert.Equal(t, "Accessed: 123", result)
|
||||
assert.Equal(t, []int{123}, accessLog)
|
||||
})
|
||||
|
||||
t.Run("multiple executions with different environments", func(t *testing.T) {
|
||||
counter := 0
|
||||
increment := func(n int) IO[int] {
|
||||
return func() int {
|
||||
counter++
|
||||
return n + counter
|
||||
}
|
||||
}
|
||||
|
||||
double := N.Mul(2)
|
||||
adapted := Local[int](double)(increment)
|
||||
|
||||
result1 := adapted(5)() // 10 + 1 = 11
|
||||
result2 := adapted(10)() // 20 + 2 = 22
|
||||
|
||||
assert.Equal(t, 11, result1)
|
||||
assert.Equal(t, 22, result2)
|
||||
assert.Equal(t, 2, counter)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
readConfig := func(env SimpleConfig) IO[string] {
|
||||
return io.Of(fmt.Sprintf("%s:%d", env.Host, env.Port))
|
||||
}
|
||||
|
||||
simplify := func(detailed DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: detailed.Host, Port: detailed.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string](simplify)(readConfig)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080, Debug: true})()
|
||||
|
||||
assert.Equal(t, "localhost:8080", result)
|
||||
})
|
||||
|
||||
t.Run("extract field from larger structure", func(t *testing.T) {
|
||||
getPort := func(port int) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Port: %d", port))
|
||||
}
|
||||
|
||||
extractPort := func(cfg SimpleConfig) int {
|
||||
return cfg.Port
|
||||
}
|
||||
|
||||
adapted := Contramap[string](extractPort)(getPort)
|
||||
result := adapted(SimpleConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, "Port: 9000", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapVsLocal verifies Contramap and Local are equivalent
|
||||
func TestContramapVsLocal(t *testing.T) {
|
||||
t.Run("same behavior as Local", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 3)
|
||||
}
|
||||
|
||||
double := N.Mul(2)
|
||||
|
||||
localResult := Local[int](double)(getValue)(5)()
|
||||
contramapResult := Contramap[int](double)(getValue)(5)()
|
||||
|
||||
assert.Equal(t, localResult, contramapResult)
|
||||
assert.Equal(t, 30, localResult) // (5 * 2) * 3 = 30
|
||||
})
|
||||
|
||||
t.Run("environment extraction equivalence", func(t *testing.T) {
|
||||
getHost := func(cfg SimpleConfig) IO[string] {
|
||||
return io.Of(cfg.Host)
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
|
||||
env := DetailedConfig{Host: "example.com", Port: 8080, Debug: false}
|
||||
|
||||
localResult := Local[string](simplify)(getHost)(env)()
|
||||
contramapResult := Contramap[string](simplify)(getHost)(env)()
|
||||
|
||||
assert.Equal(t, localResult, contramapResult)
|
||||
assert.Equal(t, "example.com", localResult)
|
||||
})
|
||||
}
|
||||
|
||||
// TestProfunctorLaws tests profunctor laws
|
||||
func TestProfunctorLaws(t *testing.T) {
|
||||
t.Run("identity law", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n + 10)
|
||||
}
|
||||
|
||||
identity := reader.Ask[int]()
|
||||
|
||||
// Promap(id, id) should be equivalent to id
|
||||
adapted := Promap(identity, identity)(getValue)
|
||||
original := getValue(5)()
|
||||
transformed := adapted(5)()
|
||||
|
||||
assert.Equal(t, original, transformed)
|
||||
assert.Equal(t, 15, transformed)
|
||||
})
|
||||
|
||||
t.Run("composition law", func(t *testing.T) {
|
||||
getValue := func(n int) IO[int] {
|
||||
return io.Of(n * 2)
|
||||
}
|
||||
|
||||
f1 := N.Add(1)
|
||||
f2 := N.Mul(3)
|
||||
g1 := N.Sub(5)
|
||||
g2 := N.Mul(2)
|
||||
|
||||
// Promap(f1, g2) . Promap(f2, g1) should equal Promap(f2 . f1, g2 . g1)
|
||||
// Note: composition order is reversed for contravariant part
|
||||
step1 := Promap(f2, g1)(getValue)
|
||||
composed1 := Promap(f1, g2)(step1)
|
||||
|
||||
composed2 := Promap(
|
||||
func(x int) int { return f2(f1(x)) },
|
||||
func(x int) int { return g2(g1(x)) },
|
||||
)(getValue)
|
||||
|
||||
result1 := composed1(10)()
|
||||
result2 := composed2(10)()
|
||||
|
||||
assert.Equal(t, result1, result2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEdgeCases tests edge cases and special scenarios
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Run("empty struct environment", func(t *testing.T) {
|
||||
type Empty struct{}
|
||||
|
||||
getValue := func(e Empty) IO[int] {
|
||||
return io.Of(42)
|
||||
}
|
||||
|
||||
identity := reader.Ask[Empty]()
|
||||
adapted := Local[int](identity)(getValue)
|
||||
result := adapted(Empty{})()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("function type handling", func(t *testing.T) {
|
||||
getFunc := func(n int) IO[func(int) int] {
|
||||
return io.Of(N.Mul(2))
|
||||
}
|
||||
|
||||
double := N.Mul(2)
|
||||
applyFunc := reader.Read[int](5)
|
||||
|
||||
adapted := Promap(double, applyFunc)(getFunc)
|
||||
result := adapted(3)() // (3 * 2) = 6, then func(5) = 10
|
||||
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
|
||||
t.Run("complex nested transformations", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
getValue := func(l3 Level3) IO[int] {
|
||||
return io.Of(l3.Value)
|
||||
}
|
||||
|
||||
extract := func(l1 Level1) Level3 {
|
||||
return l1.L2.L3
|
||||
}
|
||||
multiply := N.Mul(10)
|
||||
|
||||
adapted := Promap(extract, multiply)(getValue)
|
||||
result := adapted(Level1{L2: Level2{L3: Level3{Value: 7}}})()
|
||||
|
||||
assert.Equal(t, 70, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRealWorldScenarios tests practical use cases
|
||||
func TestRealWorldScenarios(t *testing.T) {
|
||||
t.Run("database connection with config extraction", func(t *testing.T) {
|
||||
type DBConfig struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
type AppSettings struct {
|
||||
DB DBConfig
|
||||
APIKey string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
connect := func(cfg DBConfig) IO[string] {
|
||||
return io.Of("Connected: " + cfg.ConnectionString)
|
||||
}
|
||||
|
||||
extractDB := func(settings AppSettings) DBConfig {
|
||||
return settings.DB
|
||||
}
|
||||
|
||||
adapted := Local[string](extractDB)(connect)
|
||||
result := adapted(AppSettings{
|
||||
DB: DBConfig{ConnectionString: "postgres://localhost"},
|
||||
APIKey: "secret",
|
||||
Timeout: 30,
|
||||
})()
|
||||
|
||||
assert.Equal(t, "Connected: postgres://localhost", result)
|
||||
})
|
||||
|
||||
t.Run("logging with environment transformation", func(t *testing.T) {
|
||||
type LogContext struct {
|
||||
RequestID string
|
||||
UserID int
|
||||
}
|
||||
|
||||
type RequestContext struct {
|
||||
RequestID string
|
||||
UserID int
|
||||
Path string
|
||||
Method string
|
||||
}
|
||||
|
||||
var logs []string
|
||||
logMessage := func(ctx LogContext) IO[func()] {
|
||||
return func() func() {
|
||||
return func() {
|
||||
logs = append(logs, fmt.Sprintf("[%s] User %d", ctx.RequestID, ctx.UserID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractLogContext := func(req RequestContext) LogContext {
|
||||
return LogContext{RequestID: req.RequestID, UserID: req.UserID}
|
||||
}
|
||||
|
||||
adapted := Local[func()](extractLogContext)(logMessage)
|
||||
result := adapted(RequestContext{
|
||||
RequestID: "req-123",
|
||||
UserID: 42,
|
||||
Path: "/api/users",
|
||||
Method: "GET",
|
||||
})()
|
||||
|
||||
result()
|
||||
assert.Equal(t, []string{"[req-123] User 42"}, logs)
|
||||
})
|
||||
|
||||
t.Run("API response transformation", func(t *testing.T) {
|
||||
type APIResponse struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
type EnrichedResponse struct {
|
||||
Response APIResponse
|
||||
Timestamp int64
|
||||
RequestID string
|
||||
}
|
||||
|
||||
formatResponse := func(resp APIResponse) IO[string] {
|
||||
return io.Of(fmt.Sprintf("Status: %d, Body: %s", resp.StatusCode, resp.Body))
|
||||
}
|
||||
|
||||
extractResponse := func(enriched EnrichedResponse) APIResponse {
|
||||
return enriched.Response
|
||||
}
|
||||
addMetadata := func(s string) string {
|
||||
return "[API] " + s
|
||||
}
|
||||
|
||||
adapted := Promap(extractResponse, addMetadata)(formatResponse)
|
||||
result := adapted(EnrichedResponse{
|
||||
Response: APIResponse{StatusCode: 200, Body: "OK"},
|
||||
Timestamp: 1234567890,
|
||||
RequestID: "req-456",
|
||||
})()
|
||||
|
||||
assert.Equal(t, "[API] Status: 200, Body: OK", result)
|
||||
})
|
||||
}
|
||||
@@ -1112,6 +1112,63 @@ func Read[A, R any](r R) func(ReaderIO[R, A]) IO[A] {
|
||||
return reader.Read[IO[A]](r)
|
||||
}
|
||||
|
||||
// ReadIO executes a ReaderIO computation by providing an environment wrapped in an IO effect.
|
||||
// This is useful when the environment itself needs to be computed or retrieved through side effects.
|
||||
//
|
||||
// The function takes an IO[R] (an effectful computation that produces an environment) and returns
|
||||
// a function that can execute a ReaderIO[R, A] to produce an IO[A].
|
||||
//
|
||||
// This is particularly useful in scenarios where:
|
||||
// - The environment needs to be loaded from a file, database, or network
|
||||
// - The environment requires initialization with side effects
|
||||
// - You want to compose environment retrieval with the computation that uses it
|
||||
//
|
||||
// The execution flow is:
|
||||
// 1. Execute the IO[R] to get the environment R
|
||||
// 2. Pass the environment to the ReaderIO[R, 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
|
||||
// - R: The environment type required by the ReaderIO
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IO effect that produces the environment of type R
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIO[R, A] and returns an IO[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// DatabaseURL string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Load config from file (side effect)
|
||||
// loadConfig := io.Of(Config{DatabaseURL: "localhost:5432", Port: 8080})
|
||||
//
|
||||
// // A computation that uses the config
|
||||
// getConnectionString := readerio.Asks(func(c Config) io.IO[string] {
|
||||
// return io.Of(c.DatabaseURL)
|
||||
// })
|
||||
//
|
||||
// // Compose them together
|
||||
// result := readerio.ReadIO[string](loadConfig)(getConnectionString)
|
||||
// connectionString := result() // Executes both effects and returns "localhost:5432"
|
||||
//
|
||||
// Comparison with Read:
|
||||
// - [Read]: Takes a pure value R and executes the ReaderIO immediately
|
||||
// - [ReadIO]: Takes an IO[R] and chains the effects together
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[A, R any](r IO[R]) func(ReaderIO[R, A]) IO[A] {
|
||||
return function.Flow2(
|
||||
io.Chain[R, A],
|
||||
Read[A](r),
|
||||
)
|
||||
}
|
||||
|
||||
// Delay creates an operation that passes in the value after some delay
|
||||
//
|
||||
//go:inline
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
G "github.com/IBM/fp-go/v2/io"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -697,6 +698,150 @@ func TestRead(t *testing.T) {
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
t.Run("basic usage with IO environment", func(t *testing.T) {
|
||||
// Create a ReaderIO that uses the config
|
||||
rio := Of[ReaderTestConfig](42)
|
||||
|
||||
// Create an IO that produces the config
|
||||
configIO := G.Of(ReaderTestConfig{Value: 21, Name: "test"})
|
||||
|
||||
// Use ReadIO to execute the ReaderIO with the IO environment
|
||||
result := ReadIO[int](configIO)(rio)()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("chains IO effects correctly", func(t *testing.T) {
|
||||
// Track execution order
|
||||
executionOrder := []string{}
|
||||
|
||||
// Create an IO that produces the config with a side effect
|
||||
configIO := func() ReaderTestConfig {
|
||||
executionOrder = append(executionOrder, "load config")
|
||||
return ReaderTestConfig{Value: 10, Name: "test"}
|
||||
}
|
||||
|
||||
// Create a ReaderIO that uses the config with a side effect
|
||||
rio := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
executionOrder = append(executionOrder, "use config")
|
||||
return c.Value * 3
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the composed computation
|
||||
result := ReadIO[int](configIO)(rio)()
|
||||
|
||||
assert.Equal(t, 30, result)
|
||||
assert.Equal(t, []string{"load config", "use config"}, executionOrder)
|
||||
})
|
||||
|
||||
t.Run("works with complex environment loading", func(t *testing.T) {
|
||||
// Simulate loading config from a file or database
|
||||
loadConfigFromDB := func() ReaderTestConfig {
|
||||
// Simulate side effect
|
||||
return ReaderTestConfig{Value: 100, Name: "production"}
|
||||
}
|
||||
|
||||
// A computation that depends on the loaded config
|
||||
getConnectionString := func(c ReaderTestConfig) G.IO[string] {
|
||||
return G.Of(c.Name + ":" + S.Format[int]("%d")(c.Value))
|
||||
}
|
||||
|
||||
result := ReadIO[string](loadConfigFromDB)(getConnectionString)()
|
||||
|
||||
assert.Equal(t, "production:100", result)
|
||||
})
|
||||
|
||||
t.Run("composes with other ReaderIO operations", func(t *testing.T) {
|
||||
configIO := G.Of(ReaderTestConfig{Value: 5, Name: "test"})
|
||||
|
||||
// Build a pipeline using ReaderIO operations
|
||||
pipeline := F.Pipe2(
|
||||
Ask[ReaderTestConfig](),
|
||||
Map[ReaderTestConfig](func(c ReaderTestConfig) int { return c.Value }),
|
||||
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
|
||||
return Of[ReaderTestConfig](n * 4)
|
||||
}),
|
||||
)
|
||||
|
||||
result := ReadIO[int](configIO)(pipeline)()
|
||||
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("handles environment with multiple fields", func(t *testing.T) {
|
||||
configIO := G.Of(ReaderTestConfig{Value: 42, Name: "answer"})
|
||||
|
||||
// Access both fields from the environment
|
||||
rio := func(c ReaderTestConfig) G.IO[string] {
|
||||
return G.Of(c.Name + "=" + S.Format[int]("%d")(c.Value))
|
||||
}
|
||||
|
||||
result := ReadIO[string](configIO)(rio)()
|
||||
|
||||
assert.Equal(t, "answer=42", result)
|
||||
})
|
||||
|
||||
t.Run("lazy evaluation - IO not executed until called", func(t *testing.T) {
|
||||
executed := false
|
||||
|
||||
configIO := func() ReaderTestConfig {
|
||||
executed = true
|
||||
return ReaderTestConfig{Value: 1, Name: "test"}
|
||||
}
|
||||
|
||||
rio := Of[ReaderTestConfig](42)
|
||||
|
||||
// Create the composed IO but don't execute it yet
|
||||
composedIO := ReadIO[int](configIO)(rio)
|
||||
|
||||
// Config IO should not be executed yet
|
||||
assert.False(t, executed)
|
||||
|
||||
// Now execute it
|
||||
result := composedIO()
|
||||
|
||||
// Now it should be executed
|
||||
assert.True(t, executed)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("works with ChainIOK", func(t *testing.T) {
|
||||
configIO := G.Of(ReaderTestConfig{Value: 10, Name: "test"})
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
Of[ReaderTestConfig](5),
|
||||
ChainIOK[ReaderTestConfig](func(n int) G.IO[int] {
|
||||
return G.Of(n * 2)
|
||||
}),
|
||||
)
|
||||
|
||||
result := ReadIO[int](configIO)(pipeline)()
|
||||
|
||||
assert.Equal(t, 10, result)
|
||||
})
|
||||
|
||||
t.Run("comparison with Read - different input types", func(t *testing.T) {
|
||||
rio := func(c ReaderTestConfig) G.IO[int] {
|
||||
return G.Of(c.Value + 10)
|
||||
}
|
||||
|
||||
config := ReaderTestConfig{Value: 5, Name: "test"}
|
||||
|
||||
// Using Read with a pure value
|
||||
resultRead := Read[int](config)(rio)()
|
||||
|
||||
// Using ReadIO with an IO value
|
||||
resultReadIO := ReadIO[int](G.Of(config))(rio)()
|
||||
|
||||
// Both should produce the same result
|
||||
assert.Equal(t, 15, resultRead)
|
||||
assert.Equal(t, 15, resultReadIO)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapWithLogging(t *testing.T) {
|
||||
// Simulate logging scenario
|
||||
logged := []int{}
|
||||
|
||||
@@ -16,6 +16,82 @@
|
||||
// Package readerioeither provides a functional programming abstraction that combines
|
||||
// three powerful concepts: Reader, IO, and Either monads.
|
||||
//
|
||||
// # Type Parameter Ordering Convention
|
||||
//
|
||||
// This package follows a consistent convention for ordering type parameters in function signatures.
|
||||
// The general rule is: R -> E -> T (context, error, type), where:
|
||||
// - R: The Reader context/environment type
|
||||
// - E: The Either error type
|
||||
// - T: The value type(s) (A, B, etc.)
|
||||
//
|
||||
// However, when some type parameters can be automatically inferred by the Go compiler from
|
||||
// function arguments, the convention is modified to minimize explicit type annotations:
|
||||
//
|
||||
// Rule: Undetectable types come first, followed by detectable types, while preserving
|
||||
// the relative order within each group (R -> E -> T).
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// 1. All types detectable from first argument:
|
||||
// MonadMap[R, E, A, B](fa ReaderIOEither[R, E, A], f func(A) B)
|
||||
// - R, E, A are detectable from fa
|
||||
// - B is detectable from f
|
||||
// - Order: R, E, A, B (standard order, all detectable)
|
||||
//
|
||||
// 2. Some types undetectable:
|
||||
// FromReader[E, R, A](ma Reader[R, A]) ReaderIOEither[R, E, A]
|
||||
// - R, A are detectable from ma
|
||||
// - E is undetectable (not in any argument)
|
||||
// - Order: E, R, A (E first as undetectable, then R, A in standard order)
|
||||
//
|
||||
// 3. Multiple undetectable types:
|
||||
// Local[E, A, R1, R2](f func(R2) R1) func(ReaderIOEither[R1, E, A]) ReaderIOEither[R2, E, A]
|
||||
// - E, A are undetectable
|
||||
// - R1, R2 are detectable from f
|
||||
//
|
||||
// 4. Functions returning Kleisli arrows:
|
||||
// ChainReaderOptionK[R, A, B, E](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, B]
|
||||
// - Canonical order would be R, E, A, B
|
||||
// - E is detectable from onNone parameter
|
||||
// - R, A, B are not detectable (they're in the Kleisli argument type)
|
||||
// - Order: R, A, B, E (undetectable R, A, B first, then detectable E)
|
||||
//
|
||||
// This convention allows for more ergonomic function calls:
|
||||
//
|
||||
// // Without convention - need to specify all types:
|
||||
// result := FromReader[context.Context, error, User](readerFunc)
|
||||
//
|
||||
// // With convention - only specify undetectable type:
|
||||
// result := FromReader[error](readerFunc) // R and A inferred from readerFunc
|
||||
//
|
||||
// The reasoning behind this approach is to reduce the number of explicit type parameters
|
||||
// that developers need to specify when calling functions, improving code readability and
|
||||
// reducing verbosity while maintaining type safety.
|
||||
//
|
||||
// Additional examples demonstrating the convention:
|
||||
//
|
||||
// 5. FromReaderOption[R, A, E](onNone func() E) Kleisli[R, E, ReaderOption[R, A], A]
|
||||
// - Canonical order would be R, E, A
|
||||
// - E is detectable from onNone parameter
|
||||
// - R, A are not detectable (they're in the return type's Kleisli)
|
||||
// - Order: R, A, E (undetectable R, A first, then detectable E)
|
||||
//
|
||||
// 6. MapLeft[R, A, E1, E2](f func(E1) E2) func(ReaderIOEither[R, E1, A]) ReaderIOEither[R, E2, A]
|
||||
// - Canonical order would be R, E1, E2, A
|
||||
// - E1, E2 are detectable from f parameter
|
||||
// - R, A are not detectable (they're in the return type)
|
||||
// - Order: R, A, E1, E2 (undetectable R, A first, then detectable E1, E2)
|
||||
//
|
||||
// Additional special cases:
|
||||
//
|
||||
// - Ap[B, R, E, A]: B is undetectable (in function return type), so B comes first
|
||||
// - OrLeft[A, E1, R, E2]: A is undetectable, comes first before detectable E1, R, E2
|
||||
// - ReadIO[E, A, R]: E and A are undetectable, R is detectable from IO[R]
|
||||
// - ChainFirstLeft[A, R, EA, EB, B]: A is undetectable, comes first before detectable R, EA, EB, B
|
||||
// - TapLeft[A, R, EB, EA, B]: Similar to ChainFirstLeft, A is undetectable and comes first
|
||||
//
|
||||
// All functions in this package follow this convention consistently.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
onNegative := func(n int) string { return "negative number" }
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onNegative)
|
||||
double := Map[context.Context, string](func(n int) int { return n * 2 })
|
||||
double := Map[context.Context, string](N.Mul(2))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
76
v2/readerioeither/profunctor.go
Normal file
76
v2/readerioeither/profunctor.go
Normal 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 readerioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOEither.
|
||||
// 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 ReaderIOEither (via f)
|
||||
// - Transform the success value after the IO effect completes (via g)
|
||||
//
|
||||
// The error type E remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderIOEither
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The original success type produced by the ReaderIOEither
|
||||
// - D: The new input environment type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOEither[R, E, A] and returns a ReaderIOEither[D, E, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, E, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, E, ReaderIOEither[R, E, A], B] {
|
||||
return reader.Promap(f, ioeither.Map[E](g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderIOEither.
|
||||
// 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 ReaderIOEither to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type (unchanged)
|
||||
// - A: The success type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderIOEither
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[E, A, R1, R2 any](f func(R2) R1) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
|
||||
return reader.Contramap[IOEither[E, A]](f)
|
||||
}
|
||||
133
v2/readerioeither/profunctor_test.go
Normal file
133
v2/readerioeither/profunctor_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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 readerioeither
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"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) {
|
||||
// ReaderIOEither that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Transform DetailedConfig to SimpleConfig and int to string
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, E.Of[string]("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderIOEither that returns an error
|
||||
getError := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Left[int]("error occurred")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, E.Left[string]("error occurred"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderIOEither that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Of[string](c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, E.Of[string](9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) IOEither[string, int] {
|
||||
return IOE.Left[int]("config error")
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[string, int](simplify)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, E.Left[int]("config error"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithIO tests Promap with actual IO effects
|
||||
func TestPromapWithIO(t *testing.T) {
|
||||
t.Run("transform IO result", func(t *testing.T) {
|
||||
counter := 0
|
||||
|
||||
// ReaderIOEither with side effect
|
||||
getPortWithEffect := func(c SimpleConfig) IOEither[string, int] {
|
||||
return func() E.Either[string, int] {
|
||||
counter++
|
||||
return E.Of[string](c.Port)
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap[SimpleConfig, string](simplify, toString)(getPortWithEffect)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, E.Of[string]("8080"), result)
|
||||
assert.Equal(t, 1, counter) // Side effect occurred
|
||||
})
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func FromReaderOption[R, A, E any](onNone func() E) Kleisli[R, E, ReaderOption[R, A], A] {
|
||||
func FromReaderOption[R, A, E any](onNone Lazy[E]) Kleisli[R, E, ReaderOption[R, A], A] {
|
||||
return function.Bind2nd(function.Flow2[ReaderOption[R, A], IOE.Kleisli[E, Option[A], A]], IOE.FromOption[A](onNone))
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ func TapReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E, A
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainReaderOptionK[R, A, B, E any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
|
||||
func ChainReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
|
||||
fro := FromReaderOption[R, B](onNone)
|
||||
return func(f readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
|
||||
return fromreader.ChainReaderK(
|
||||
@@ -360,7 +360,7 @@ func ChainReaderOptionK[R, A, B, E any](onNone func() E) func(readeroption.Kleis
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstReaderOptionK[R, A, B, E any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
|
||||
func ChainFirstReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
|
||||
fro := FromReaderOption[R, B](onNone)
|
||||
return func(f readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
@@ -372,7 +372,7 @@ func ChainFirstReaderOptionK[R, A, B, E any](onNone func() E) func(readeroption.
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapReaderOptionK[R, A, B, E any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
|
||||
func TapReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
|
||||
return ChainFirstReaderOptionK[R, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ func TapIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
|
||||
// If the Option is None, the provided error function is called to produce the error value.
|
||||
//
|
||||
//go:inline
|
||||
func ChainOptionK[R, A, B, E any](onNone func() E) func(func(A) Option[B]) Operator[R, E, A, B] {
|
||||
func ChainOptionK[R, A, B, E any](onNone Lazy[E]) func(func(A) Option[B]) Operator[R, E, A, B] {
|
||||
return fromeither.ChainOptionK(
|
||||
MonadChain[R, E, A, B],
|
||||
FromEither[R, E, B],
|
||||
@@ -651,7 +651,7 @@ func Asks[E, R, A any](r Reader[R, A]) ReaderIOEither[R, E, A] {
|
||||
// If the Option is None, the provided function is called to produce the error.
|
||||
//
|
||||
//go:inline
|
||||
func FromOption[R, A, E any](onNone func() E) func(Option[A]) ReaderIOEither[R, E, A] {
|
||||
func FromOption[R, A, E any](onNone Lazy[E]) func(Option[A]) ReaderIOEither[R, E, A] {
|
||||
return fromeither.FromOption(FromEither[R, E, A], onNone)
|
||||
}
|
||||
|
||||
@@ -821,6 +821,108 @@ func Read[E, A, R any](r R) func(ReaderIOEither[R, E, A]) IOEither[E, A] {
|
||||
return reader.Read[IOEither[E, A]](r)
|
||||
}
|
||||
|
||||
// ReadIOEither executes a ReaderIOEither computation by providing it with an environment
|
||||
// obtained from an IOEither computation. This is useful when the environment itself needs
|
||||
// to be computed with side effects and error handling.
|
||||
//
|
||||
// The function first executes the IOEither[E, R] to get the environment R (or fail with error E),
|
||||
// then uses that environment to run the ReaderIOEither[R, E, A] computation.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The success value type of the ReaderIOEither computation
|
||||
// - R: The environment/context type required by the ReaderIOEither
|
||||
// - E: The error type
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IOEither[E, R] that produces the environment (or an error)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOEither[R, E, A] and returns IOEither[E, A]
|
||||
//
|
||||
// Behavior:
|
||||
// - If the IOEither[E, R] fails (Left), the error is propagated without running the ReaderIOEither
|
||||
// - If the IOEither[E, R] succeeds (Right), the resulting environment is used to execute the ReaderIOEither
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Load configuration from a file (may fail)
|
||||
// loadConfig := func() IOEither[error, Config] {
|
||||
// return Lazy[E]ither[error, Config] {
|
||||
// // Read config file with error handling
|
||||
// return either.Right(Config{BaseURL: "https://api.example.com"})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // A computation that needs the config
|
||||
// fetchUser := func(id int) ReaderIOEither[Config, error, User] {
|
||||
// return func(cfg Config) IOEither[error, User] {
|
||||
// // Use cfg.BaseURL to fetch user
|
||||
// return ioeither.Right[error](User{ID: id})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute the computation with dynamically loaded config
|
||||
// result := ReadIOEither[User](loadConfig())(fetchUser(123))()
|
||||
//
|
||||
//go:inline
|
||||
func ReadIOEither[A, R, E any](r IOEither[E, R]) func(ReaderIOEither[R, E, A]) IOEither[E, A] {
|
||||
return function.Flow2(
|
||||
IOE.Chain[E, R, A],
|
||||
Read[E, A](r),
|
||||
)
|
||||
}
|
||||
|
||||
// ReadIO executes a ReaderIOEither computation by providing it with an environment
|
||||
// obtained from an IO computation. This is useful when the environment needs to be
|
||||
// computed with side effects but cannot fail.
|
||||
//
|
||||
// The function first executes the IO[R] to get the environment R,
|
||||
// then uses that environment to run the ReaderIOEither[R, E, A] computation.
|
||||
//
|
||||
// Type parameters:
|
||||
// - E: The error type of the ReaderIOEither computation
|
||||
// - A: The success value type of the ReaderIOEither computation
|
||||
// - R: The environment/context type required by the ReaderIOEither
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IO[R] that produces the environment
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOEither[R, E, A] and returns IOEither[E, A]
|
||||
//
|
||||
// Behavior:
|
||||
// - The IO[R] is always executed successfully to obtain the environment
|
||||
// - The resulting environment is then used to execute the ReaderIOEither
|
||||
// - Only the ReaderIOEither computation can fail with error type E
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Get current timestamp (cannot fail)
|
||||
// getCurrentTime := func() IO[time.Time] {
|
||||
// return func() time.Time {
|
||||
// return time.Now()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // A computation that needs the timestamp
|
||||
// logWithTimestamp := func(msg string) ReaderIOEither[time.Time, error, string] {
|
||||
// return func(t time.Time) IOEither[error, string] {
|
||||
// logged := fmt.Sprintf("[%s] %s", t.Format(time.RFC3339), msg)
|
||||
// return ioeither.Right[error](logged)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute the computation with current time
|
||||
// result := ReadIO[error, string](getCurrentTime())(logWithTimestamp("Hello"))()
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[E, A, R any](r IO[R]) func(ReaderIOEither[R, E, A]) IOEither[E, A] {
|
||||
return function.Flow2(
|
||||
io.Chain[R, Either[E, A]],
|
||||
Read[E, A](r),
|
||||
)
|
||||
}
|
||||
|
||||
// MonadChainLeft chains a computation on the left (error) side of a ReaderIOEither.
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
|
||||
@@ -957,7 +1059,7 @@ func MonadTapLeft[A, R, EA, EB, B any](ma ReaderIOEither[R, EA, A], f Kleisli[R,
|
||||
// - An Operator that performs the side effect but always returns the original error if input was Left
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstLeft[A, R, EB, EA, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A] {
|
||||
func ChainFirstLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A] {
|
||||
return eithert.ChainFirstLeft(
|
||||
readerio.Chain[R, Either[EA, A], Either[EA, A]],
|
||||
readerio.Map[R, Either[EB, B], Either[EA, A]],
|
||||
@@ -974,7 +1076,7 @@ func ChainFirstLeftIOK[A, R, EA, B any](f io.Kleisli[EA, B]) Operator[R, EA, A,
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapLeft[A, R, EB, EA, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A] {
|
||||
func TapLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) Operator[R, EA, A, A] {
|
||||
return ChainFirstLeft[A](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -308,3 +308,226 @@ func TestTapLeft(t *testing.T) {
|
||||
assert.Equal(t, E.Left[int]("error"), result)
|
||||
assert.True(t, sideEffectRan)
|
||||
}
|
||||
|
||||
func TestReadIOEither(t *testing.T) {
|
||||
type Config struct {
|
||||
baseURL string
|
||||
timeout int
|
||||
}
|
||||
|
||||
// Test with Right IOEither - should execute ReaderIOEither with the environment
|
||||
t.Run("Right IOEither provides environment", func(t *testing.T) {
|
||||
// IOEither that successfully produces a config
|
||||
configIO := IOE.Right[error](Config{baseURL: "https://api.example.com", timeout: 30})
|
||||
|
||||
// ReaderIOEither that uses the config
|
||||
computation := func(cfg Config) IOEither[error, string] {
|
||||
return IOE.Right[error](cfg.baseURL + "/users")
|
||||
}
|
||||
|
||||
// Execute using ReadIOEither
|
||||
result := ReadIOEither[string](configIO)(computation)()
|
||||
assert.Equal(t, E.Right[error]("https://api.example.com/users"), result)
|
||||
})
|
||||
|
||||
// Test with Left IOEither - should propagate error without executing ReaderIOEither
|
||||
t.Run("Left IOEither propagates error", func(t *testing.T) {
|
||||
configError := errors.New("failed to load config")
|
||||
configIO := IOE.Left[Config](configError)
|
||||
|
||||
executed := false
|
||||
computation := func(cfg Config) IOEither[error, string] {
|
||||
executed = true
|
||||
return IOE.Right[error]("should not execute")
|
||||
}
|
||||
|
||||
result := ReadIOEither[string](configIO)(computation)()
|
||||
assert.Equal(t, E.Left[string](configError), result)
|
||||
assert.False(t, executed, "ReaderIOEither should not execute when IOEither is Left")
|
||||
})
|
||||
|
||||
// Test with Right IOEither but ReaderIOEither fails
|
||||
t.Run("Right IOEither but ReaderIOEither fails", func(t *testing.T) {
|
||||
configIO := IOE.Right[error](Config{baseURL: "https://api.example.com", timeout: 30})
|
||||
|
||||
computationError := errors.New("computation failed")
|
||||
computation := func(cfg Config) IOEither[error, string] {
|
||||
// Use the config but fail
|
||||
if cfg.timeout < 60 {
|
||||
return IOE.Left[string](computationError)
|
||||
}
|
||||
return IOE.Right[error]("success")
|
||||
}
|
||||
|
||||
result := ReadIOEither[string](configIO)(computation)()
|
||||
assert.Equal(t, E.Left[string](computationError), result)
|
||||
})
|
||||
|
||||
// Test chaining with ReadIOEither
|
||||
t.Run("Chaining with ReadIOEither", func(t *testing.T) {
|
||||
// First get the config
|
||||
configIO := IOE.Right[error](Config{baseURL: "https://api.example.com", timeout: 30})
|
||||
|
||||
// Chain multiple operations
|
||||
result := F.Pipe2(
|
||||
Of[Config, error](10),
|
||||
Map[Config, error](func(x int) int { return x * 2 }),
|
||||
ReadIOEither[int](configIO),
|
||||
)()
|
||||
|
||||
assert.Equal(t, E.Right[error](20), result)
|
||||
})
|
||||
|
||||
// Test with complex error type
|
||||
t.Run("Complex error type", func(t *testing.T) {
|
||||
type AppError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
configIO := IOE.Left[Config](AppError{Code: 500, Message: "Internal error"})
|
||||
|
||||
computation := func(cfg Config) IOEither[AppError, string] {
|
||||
return IOE.Right[AppError]("success")
|
||||
}
|
||||
|
||||
result := ReadIOEither[string](configIO)(computation)()
|
||||
assert.Equal(t, E.Left[string](AppError{Code: 500, Message: "Internal error"}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
type Config struct {
|
||||
baseURL string
|
||||
version string
|
||||
}
|
||||
|
||||
// Test basic execution - IO provides environment
|
||||
t.Run("IO provides environment successfully", func(t *testing.T) {
|
||||
// IO that produces a config (cannot fail)
|
||||
configIO := func() Config {
|
||||
return Config{baseURL: "https://api.example.com", version: "v1"}
|
||||
}
|
||||
|
||||
// ReaderIOEither that uses the config
|
||||
computation := func(cfg Config) IOEither[error, string] {
|
||||
return IOE.Right[error](cfg.baseURL + "/" + cfg.version)
|
||||
}
|
||||
|
||||
result := ReadIO[error, string](configIO)(computation)()
|
||||
assert.Equal(t, E.Right[error]("https://api.example.com/v1"), result)
|
||||
})
|
||||
|
||||
// Test when ReaderIOEither fails
|
||||
t.Run("ReaderIOEither fails after IO succeeds", func(t *testing.T) {
|
||||
configIO := func() Config {
|
||||
return Config{baseURL: "https://api.example.com", version: "v1"}
|
||||
}
|
||||
|
||||
computationError := errors.New("validation failed")
|
||||
computation := func(cfg Config) IOEither[error, string] {
|
||||
// Validate config
|
||||
if cfg.version != "v2" {
|
||||
return IOE.Left[string](computationError)
|
||||
}
|
||||
return IOE.Right[error]("success")
|
||||
}
|
||||
|
||||
result := ReadIO[error, string](configIO)(computation)()
|
||||
assert.Equal(t, E.Left[string](computationError), result)
|
||||
})
|
||||
|
||||
// Test with side effects in IO
|
||||
t.Run("IO with side effects", func(t *testing.T) {
|
||||
counter := 0
|
||||
configIO := func() Config {
|
||||
counter++
|
||||
return Config{baseURL: fmt.Sprintf("https://api%d.example.com", counter), version: "v1"}
|
||||
}
|
||||
|
||||
computation := func(cfg Config) IOEither[error, string] {
|
||||
return IOE.Right[error](cfg.baseURL)
|
||||
}
|
||||
|
||||
result := ReadIO[error, string](configIO)(computation)()
|
||||
assert.Equal(t, E.Right[error]("https://api1.example.com"), result)
|
||||
assert.Equal(t, 1, counter, "IO should execute exactly once")
|
||||
})
|
||||
|
||||
// Test chaining with ReadIO
|
||||
t.Run("Chaining with ReadIO", func(t *testing.T) {
|
||||
configIO := func() Config {
|
||||
return Config{baseURL: "https://api.example.com", version: "v1"}
|
||||
}
|
||||
|
||||
result := F.Pipe2(
|
||||
Of[Config, error](42),
|
||||
Map[Config, error](func(x int) string { return fmt.Sprintf("value-%d", x) }),
|
||||
ReadIO[error, string](configIO),
|
||||
)()
|
||||
|
||||
assert.Equal(t, E.Right[error]("value-42"), result)
|
||||
})
|
||||
|
||||
// Test with different error types
|
||||
t.Run("Different error types", func(t *testing.T) {
|
||||
configIO := func() int {
|
||||
return 100
|
||||
}
|
||||
|
||||
computation := func(cfg int) IOEither[string, int] {
|
||||
if cfg < 200 {
|
||||
return IOE.Left[int]("value too low")
|
||||
}
|
||||
return IOE.Right[string](cfg)
|
||||
}
|
||||
|
||||
result := ReadIO[string, int](configIO)(computation)()
|
||||
assert.Equal(t, E.Left[int]("value too low"), result)
|
||||
})
|
||||
|
||||
// Test ReadIO vs ReadIOEither - ReadIO cannot fail during environment loading
|
||||
t.Run("ReadIO always provides environment", func(t *testing.T) {
|
||||
// This demonstrates that ReadIO's IO always succeeds
|
||||
configIO := func() Config {
|
||||
// Even if we wanted to fail here, we can't - IO cannot fail
|
||||
return Config{baseURL: "fallback", version: "v0"}
|
||||
}
|
||||
|
||||
executed := false
|
||||
computation := func(cfg Config) IOEither[error, string] {
|
||||
executed = true
|
||||
return IOE.Right[error](cfg.baseURL)
|
||||
}
|
||||
|
||||
result := ReadIO[error, string](configIO)(computation)()
|
||||
assert.Equal(t, E.Right[error]("fallback"), result)
|
||||
assert.True(t, executed, "ReaderIOEither should always execute with ReadIO")
|
||||
})
|
||||
|
||||
// Test with complex computation
|
||||
t.Run("Complex computation with environment", func(t *testing.T) {
|
||||
type Env struct {
|
||||
multiplier int
|
||||
offset int
|
||||
}
|
||||
|
||||
envIO := func() Env {
|
||||
return Env{multiplier: 3, offset: 10}
|
||||
}
|
||||
|
||||
computation := func(env Env) IOEither[error, int] {
|
||||
return func() Either[error, int] {
|
||||
// Simulate some computation using the environment
|
||||
result := env.multiplier*5 + env.offset
|
||||
if result > 20 {
|
||||
return E.Right[error](result)
|
||||
}
|
||||
return E.Left[int](errors.New("result too small"))
|
||||
}
|
||||
}
|
||||
|
||||
result := ReadIO[error, int](envIO)(computation)()
|
||||
assert.Equal(t, E.Right[error](25), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -109,4 +110,6 @@ type (
|
||||
|
||||
// Predicate represents a function that tests a value of type A and returns a boolean.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
onNegative := func(n int) error { return fmt.Errorf("negative number") }
|
||||
|
||||
filter := FilterOrElse[context.Context](isPositive, onNegative)
|
||||
double := Map[context.Context](func(n int) int { return n * 2 })
|
||||
double := Map[context.Context](N.Mul(2))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
73
v2/readerioresult/profunctor.go
Normal file
73
v2/readerioresult/profunctor.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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 (
|
||||
RIOE "github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
// 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:
|
||||
// - R: 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 R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderIOResult[R, A] and returns a ReaderIOResult[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderIOResult[R, A], B] {
|
||||
return RIOE.Promap[R, error](f, 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 RIOE.Contramap[error, A](f)
|
||||
}
|
||||
98
v2/readerioresult/profunctor_test.go
Normal file
98
v2/readerioresult/profunctor_test.go
Normal 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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"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) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.Equal(t, R.Of("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderIOResult that returns an error
|
||||
getError := func(c SimpleConfig) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
return R.Left[int](fmt.Errorf("error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// 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) IOResult[int] {
|
||||
return func() R.Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})()
|
||||
|
||||
assert.Equal(t, R.Of(9000), result)
|
||||
})
|
||||
}
|
||||
@@ -824,3 +824,141 @@ func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
|
||||
func After[R, A any](timestamp time.Time) Operator[R, A, A] {
|
||||
return function.Bind2nd(function.Flow2[ReaderIOResult[R, A]], io.After[Result[A]](timestamp))
|
||||
}
|
||||
|
||||
// ReadIOEither executes a ReaderIOResult computation by providing an environment
|
||||
// obtained from an IOResult. This function bridges the gap between IOResult-based
|
||||
// environment acquisition and ReaderIOResult-based computations.
|
||||
//
|
||||
// The function first executes the IOResult[R] to obtain the environment (or an error),
|
||||
// then uses that environment to run the ReaderIOResult[R, A] computation.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The success value type of the ReaderIOResult computation
|
||||
// - R: The environment/context type required by the ReaderIOResult
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IOResult[R] that produces the environment (or an error)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult[R, A] and returns IOResult[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { BaseURL string }
|
||||
//
|
||||
// // Get config from environment with potential error
|
||||
// getConfig := func() IOResult[Config] {
|
||||
// return func() Result[Config] {
|
||||
// // Load config, may fail
|
||||
// return result.Of(Config{BaseURL: "https://api.example.com"})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // A computation that needs config
|
||||
// fetchUser := func(id int) ReaderIOResult[Config, User] {
|
||||
// return func(cfg Config) IOResult[User] {
|
||||
// return func() Result[User] {
|
||||
// // Use cfg.BaseURL to fetch user
|
||||
// return result.Of(User{ID: id})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute the computation with the config
|
||||
// result := ReadIOEither[User](getConfig())(fetchUser(123))()
|
||||
//
|
||||
//go:inline
|
||||
func ReadIOEither[A, R any](r IOResult[R]) func(ReaderIOResult[R, A]) IOResult[A] {
|
||||
return RIOE.ReadIOEither[A](r)
|
||||
}
|
||||
|
||||
// ReadIOResult executes a ReaderIOResult computation by providing an environment
|
||||
// obtained from an IOResult. This is an alias for ReadIOEither with more explicit naming.
|
||||
//
|
||||
// The function first executes the IOResult[R] to obtain the environment (or an error),
|
||||
// then uses that environment to run the ReaderIOResult[R, A] computation.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The success value type of the ReaderIOResult computation
|
||||
// - R: The environment/context type required by the ReaderIOResult
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IOResult[R] that produces the environment (or an error)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult[R, A] and returns IOResult[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct { ConnectionString string }
|
||||
//
|
||||
// // Get database connection with potential error
|
||||
// getDB := func() IOResult[Database] {
|
||||
// return func() Result[Database] {
|
||||
// return result.Of(Database{ConnectionString: "localhost:5432"})
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Query that needs database
|
||||
// queryUsers := ReaderIOResult[Database, []User] {
|
||||
// return func(db Database) IOResult[[]User] {
|
||||
// return func() Result[[]User] {
|
||||
// // Execute query using db
|
||||
// return result.Of([]User{})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute query with database
|
||||
// users := ReadIOResult[[]User](getDB())(queryUsers)()
|
||||
//
|
||||
//go:inline
|
||||
func ReadIOResult[A, R any](r IOResult[R]) func(ReaderIOResult[R, A]) IOResult[A] {
|
||||
return RIOE.ReadIOEither[A](r)
|
||||
}
|
||||
|
||||
// ReadIO executes a ReaderIOResult computation by providing an environment
|
||||
// obtained from an IO computation. Unlike ReadIOEither/ReadIOResult, the environment
|
||||
// acquisition cannot fail (it's a pure IO, not IOResult).
|
||||
//
|
||||
// The function first executes the IO[R] to obtain the environment,
|
||||
// then uses that environment to run the ReaderIOResult[R, A] computation.
|
||||
//
|
||||
// Type parameters:
|
||||
// - A: The success value type of the ReaderIOResult computation
|
||||
// - R: The environment/context type required by the ReaderIOResult
|
||||
//
|
||||
// Parameters:
|
||||
// - r: An IO[R] that produces the environment (cannot fail)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a ReaderIOResult[R, A] and returns IOResult[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Logger struct { Level string }
|
||||
//
|
||||
// // Get logger (always succeeds)
|
||||
// getLogger := func() IO[Logger] {
|
||||
// return func() Logger {
|
||||
// return Logger{Level: "INFO"}
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Log operation that may fail
|
||||
// logMessage := func(msg string) ReaderIOResult[Logger, string] {
|
||||
// return func(logger Logger) IOResult[string] {
|
||||
// return func() Result[string] {
|
||||
// // Log with logger, may fail
|
||||
// return result.Of(fmt.Sprintf("[%s] %s", logger.Level, msg))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Execute logging with logger
|
||||
// logged := ReadIO[string](getLogger())(logMessage("Hello"))()
|
||||
//
|
||||
//go:inline
|
||||
func ReadIO[A, R any](r IO[R]) func(ReaderIOResult[R, A]) IOResult[A] {
|
||||
return RIOE.ReadIO[error, A](r)
|
||||
}
|
||||
|
||||
@@ -77,3 +77,249 @@ func TestTapReaderIOK(t *testing.T) {
|
||||
|
||||
x(10)()
|
||||
}
|
||||
|
||||
func TestReadIOEither(t *testing.T) {
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
t.Run("success case - environment and computation both succeed", func(t *testing.T) {
|
||||
// Create an IOResult that successfully produces a config
|
||||
getConfig := func() IOResult[Config] {
|
||||
return func() Result[Config] {
|
||||
return result.Of(Config{BaseURL: "https://api.example.com"})
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult that uses the config
|
||||
computation := func(cfg Config) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(cfg.BaseURL + "/users")
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIOEither
|
||||
ioResult := ReadIOEither[string](getConfig())(computation)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, "https://api.example.com/users", result.GetOrElse(func(error) string { return "" })(res))
|
||||
})
|
||||
|
||||
t.Run("failure case - environment acquisition fails", func(t *testing.T) {
|
||||
expectedErr := fmt.Errorf("config load failed")
|
||||
|
||||
// Create an IOResult that fails to produce a config
|
||||
getConfig := func() IOResult[Config] {
|
||||
return func() Result[Config] {
|
||||
return result.Left[Config](expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult (won't be executed)
|
||||
computation := func(cfg Config) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of("should not be called")
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIOEither
|
||||
ioResult := ReadIOEither[string](getConfig())(computation)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
leftVal := result.Fold(F.Identity[error], func(string) error { return nil })(res)
|
||||
assert.Equal(t, expectedErr, leftVal)
|
||||
})
|
||||
|
||||
t.Run("failure case - computation fails", func(t *testing.T) {
|
||||
expectedErr := fmt.Errorf("computation failed")
|
||||
|
||||
// Create an IOResult that successfully produces a config
|
||||
getConfig := func() IOResult[Config] {
|
||||
return func() Result[Config] {
|
||||
return result.Of(Config{BaseURL: "https://api.example.com"})
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult that fails
|
||||
computation := func(cfg Config) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIOEither
|
||||
ioResult := ReadIOEither[string](getConfig())(computation)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
leftVal := result.Fold(F.Identity[error], func(string) error { return nil })(res)
|
||||
assert.Equal(t, expectedErr, leftVal)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadIOResult(t *testing.T) {
|
||||
type Database struct {
|
||||
ConnectionString string
|
||||
}
|
||||
|
||||
t.Run("success case - database and query both succeed", func(t *testing.T) {
|
||||
// Create an IOResult that successfully produces a database
|
||||
getDB := func() IOResult[Database] {
|
||||
return func() Result[Database] {
|
||||
return result.Of(Database{ConnectionString: "localhost:5432"})
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult that uses the database
|
||||
queryUsers := func(db Database) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
// Simulate query returning user count
|
||||
return result.Of(42)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIOResult
|
||||
ioResult := ReadIOResult[int](getDB())(queryUsers)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, 42, result.GetOrElse(func(error) int { return 0 })(res))
|
||||
})
|
||||
|
||||
t.Run("failure case - database connection fails", func(t *testing.T) {
|
||||
expectedErr := fmt.Errorf("connection failed")
|
||||
|
||||
// Create an IOResult that fails to produce a database
|
||||
getDB := func() IOResult[Database] {
|
||||
return func() Result[Database] {
|
||||
return result.Left[Database](expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult (won't be executed)
|
||||
queryUsers := func(db Database) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Of(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIOResult
|
||||
ioResult := ReadIOResult[int](getDB())(queryUsers)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
leftVal := result.Fold(F.Identity[error], func(int) error { return nil })(res)
|
||||
assert.Equal(t, expectedErr, leftVal)
|
||||
})
|
||||
|
||||
t.Run("failure case - query fails", func(t *testing.T) {
|
||||
expectedErr := fmt.Errorf("query failed")
|
||||
|
||||
// Create an IOResult that successfully produces a database
|
||||
getDB := func() IOResult[Database] {
|
||||
return func() Result[Database] {
|
||||
return result.Of(Database{ConnectionString: "localhost:5432"})
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult that fails
|
||||
queryUsers := func(db Database) IOResult[int] {
|
||||
return func() Result[int] {
|
||||
return result.Left[int](expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIOResult
|
||||
ioResult := ReadIOResult[int](getDB())(queryUsers)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
leftVal := result.Fold(F.Identity[error], func(int) error { return nil })(res)
|
||||
assert.Equal(t, expectedErr, leftVal)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadIO(t *testing.T) {
|
||||
type Logger struct {
|
||||
Level string
|
||||
}
|
||||
|
||||
t.Run("success case - logger and operation both succeed", func(t *testing.T) {
|
||||
// Create an IO that produces a logger (always succeeds)
|
||||
getLogger := func() IO[Logger] {
|
||||
return func() Logger {
|
||||
return Logger{Level: "INFO"}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult that uses the logger
|
||||
logMessage := func(logger Logger) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("[%s] Message logged", logger.Level))
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIO
|
||||
ioResult := ReadIO[string](getLogger())(logMessage)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, "[INFO] Message logged", result.GetOrElse(func(error) string { return "" })(res))
|
||||
})
|
||||
|
||||
t.Run("failure case - operation fails", func(t *testing.T) {
|
||||
expectedErr := fmt.Errorf("logging failed")
|
||||
|
||||
// Create an IO that produces a logger (always succeeds)
|
||||
getLogger := func() IO[Logger] {
|
||||
return func() Logger {
|
||||
return Logger{Level: "ERROR"}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult that fails
|
||||
logMessage := func(logger Logger) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Left[string](expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIO
|
||||
ioResult := ReadIO[string](getLogger())(logMessage)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsLeft(res))
|
||||
leftVal := result.Fold(F.Identity[error], func(string) error { return nil })(res)
|
||||
assert.Equal(t, expectedErr, leftVal)
|
||||
})
|
||||
|
||||
t.Run("success case - complex computation with context", func(t *testing.T) {
|
||||
type AppContext struct {
|
||||
UserID int
|
||||
Username string
|
||||
}
|
||||
|
||||
// Create an IO that produces an app context
|
||||
getContext := func() IO[AppContext] {
|
||||
return func() AppContext {
|
||||
return AppContext{UserID: 123, Username: "alice"}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a ReaderIOResult that uses the context
|
||||
processUser := func(ctx AppContext) IOResult[string] {
|
||||
return func() Result[string] {
|
||||
return result.Of(fmt.Sprintf("Processing user %s (ID: %d)", ctx.Username, ctx.UserID))
|
||||
}
|
||||
}
|
||||
|
||||
// Execute using ReadIO
|
||||
ioResult := ReadIO[string](getContext())(processUser)
|
||||
res := ioResult()
|
||||
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, "Processing user alice (ID: 123)", result.GetOrElse(func(error) string { return "" })(res))
|
||||
})
|
||||
}
|
||||
|
||||
74
v2/readeroption/profunctor.go
Normal file
74
v2/readeroption/profunctor.go
Normal 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 readeroption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of a ReaderOption.
|
||||
// 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 ReaderOption (via f)
|
||||
// - Transform the Some value after the computation completes (via g)
|
||||
//
|
||||
// The None case remains unchanged through the transformation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The original environment type expected by the ReaderOption
|
||||
// - A: The original value type produced by the ReaderOption
|
||||
// - D: The new input environment type
|
||||
// - B: The new output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the input environment from D to R (contravariant)
|
||||
// - g: Function to transform the output Some value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderOption[R, A] and returns a ReaderOption[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderOption[R, A], B] {
|
||||
return reader.Promap(f, option.Map(g))
|
||||
}
|
||||
|
||||
// Contramap changes the value of the local environment during the execution of a ReaderOption.
|
||||
// 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 ReaderOption to work with a different environment type
|
||||
// by providing a function that converts the new environment to the expected one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The value type (unchanged)
|
||||
// - R2: The new input environment type
|
||||
// - R1: The original environment type expected by the ReaderOption
|
||||
//
|
||||
// Parameters:
|
||||
// - f: Function to transform the environment from R2 to R1
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderOption[R1, A] and returns a ReaderOption[R2, A]
|
||||
//
|
||||
//go:inline
|
||||
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderOption[R1, A], A] {
|
||||
return reader.Contramap[Option[A]](f)
|
||||
}
|
||||
106
v2/readeroption/profunctor_test.go
Normal file
106
v2/readeroption/profunctor_test.go
Normal 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 readeroption
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"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 with Some", func(t *testing.T) {
|
||||
// ReaderOption that reads port from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Option[int] {
|
||||
return O.Of(c.Port)
|
||||
}
|
||||
|
||||
// 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 := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, O.Of("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles None case", func(t *testing.T) {
|
||||
// ReaderOption that returns None
|
||||
getNone := func(c SimpleConfig) Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getNone)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, O.None[string](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestContramapBasic tests basic Contramap functionality
|
||||
func TestContramapBasic(t *testing.T) {
|
||||
t.Run("environment adaptation", func(t *testing.T) {
|
||||
// ReaderOption that reads from SimpleConfig
|
||||
getPort := func(c SimpleConfig) Option[int] {
|
||||
return O.Of(c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, O.Of(9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves None", func(t *testing.T) {
|
||||
getNone := func(c SimpleConfig) Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getNone)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
@@ -320,7 +320,7 @@ func Flatten[E, A any](mma ReaderOption[E, ReaderOption[E, A]]) ReaderOption[E,
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderOption[R1, A]) ReaderOption[R2, A] {
|
||||
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderOption[R1, A]) ReaderOption[R2, A] {
|
||||
return reader.Local[Option[A]](f)
|
||||
}
|
||||
|
||||
@@ -337,6 +337,26 @@ func Read[A, E any](e E) func(ReaderOption[E, A]) Option[A] {
|
||||
return reader.Read[Option[A]](e)
|
||||
}
|
||||
|
||||
// ReadOption executes a ReaderOption with an optional environment.
|
||||
// If the environment is None, the result is None.
|
||||
// If the environment is Some(e), the ReaderOption is executed with e.
|
||||
//
|
||||
// This is useful when the environment itself might not be available.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ro := readeroption.Of[Config](42)
|
||||
// result1 := readeroption.ReadOption[int](option.Some(myConfig))(ro) // Returns option.Some(42)
|
||||
// result2 := readeroption.ReadOption[int](option.None[Config]())(ro) // Returns option.None[int]()
|
||||
//
|
||||
//go:inline
|
||||
func ReadOption[A, E any](e Option[E]) func(ReaderOption[E, A]) Option[A] {
|
||||
return function.Flow2(
|
||||
O.Chain[E],
|
||||
Read[A](e),
|
||||
)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to a function wrapped in a ReaderOption.
|
||||
// This is the reverse of MonadAp.
|
||||
//
|
||||
|
||||
@@ -26,214 +26,534 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MyContext string
|
||||
|
||||
const defaultContext MyContext = "default"
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](1),
|
||||
Map[MyContext](utils.Double),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(2), g(defaultContext))
|
||||
|
||||
// Test context type
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](utils.Double),
|
||||
Ap[int](Of[MyContext](1)),
|
||||
)
|
||||
assert.Equal(t, O.Of(2), g(defaultContext))
|
||||
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](Of[MyContext]("a")),
|
||||
Flatten[MyContext, string],
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of("a"), g(defaultContext))
|
||||
}
|
||||
|
||||
func TestFromOption(t *testing.T) {
|
||||
// Test with Some
|
||||
opt1 := O.Of(42)
|
||||
ro1 := FromOption[MyContext](opt1)
|
||||
assert.Equal(t, O.Of(42), ro1(defaultContext))
|
||||
|
||||
// Test with None
|
||||
opt2 := O.None[int]()
|
||||
ro2 := FromOption[MyContext](opt2)
|
||||
assert.Equal(t, O.None[int](), ro2(defaultContext))
|
||||
}
|
||||
|
||||
func TestSome(t *testing.T) {
|
||||
ro := Some[MyContext](42)
|
||||
assert.Equal(t, O.Of(42), ro(defaultContext))
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
reader := func(ctx MyContext) int {
|
||||
return 42
|
||||
}
|
||||
ro := FromReader(reader)
|
||||
assert.Equal(t, O.Of(42), ro(defaultContext))
|
||||
var defaultConfig = Config{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Timeout: 30,
|
||||
}
|
||||
|
||||
// TestOf tests the Of function which wraps a value in a ReaderOption
|
||||
func TestOf(t *testing.T) {
|
||||
ro := Of[MyContext](42)
|
||||
assert.Equal(t, O.Of(42), ro(defaultContext))
|
||||
ro := Of[Config](42)
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
// TestSome tests the Some function which is an alias for Of
|
||||
func TestSome(t *testing.T) {
|
||||
ro := Some[Config](42)
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
// TestNone tests the None function which creates a ReaderOption representing no value
|
||||
func TestNone(t *testing.T) {
|
||||
ro := None[MyContext, int]()
|
||||
assert.Equal(t, O.None[int](), ro(defaultContext))
|
||||
ro := None[Config, int]()
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
double := func(x int) ReaderOption[MyContext, int] {
|
||||
return Of[MyContext](x * 2)
|
||||
}
|
||||
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](21),
|
||||
Chain(double),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(42), g(defaultContext))
|
||||
|
||||
// Test with None
|
||||
g2 := F.Pipe1(
|
||||
None[MyContext, int](),
|
||||
Chain(double),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), g2(defaultContext))
|
||||
}
|
||||
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate[MyContext](func(x int) bool {
|
||||
return x > 0
|
||||
// TestFromOption tests lifting an Option into a ReaderOption
|
||||
func TestFromOption(t *testing.T) {
|
||||
t.Run("Some value", func(t *testing.T) {
|
||||
opt := O.Some(42)
|
||||
ro := FromOption[Config](opt)
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
// Test with positive number
|
||||
g1 := F.Pipe1(
|
||||
Of[MyContext](42),
|
||||
Chain(isPositive),
|
||||
)
|
||||
assert.Equal(t, O.Of(42), g1(defaultContext))
|
||||
|
||||
// Test with negative number
|
||||
g2 := F.Pipe1(
|
||||
Of[MyContext](-5),
|
||||
Chain(isPositive),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), g2(defaultContext))
|
||||
t.Run("None value", func(t *testing.T) {
|
||||
opt := O.None[int]()
|
||||
ro := FromOption[Config](opt)
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromReader tests lifting a Reader into a ReaderOption
|
||||
func TestFromReader(t *testing.T) {
|
||||
r := reader.Of[Config](42)
|
||||
ro := FromReader(r)
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
// TestSomeReader tests lifting a Reader into a ReaderOption (alias for FromReader)
|
||||
func TestSomeReader(t *testing.T) {
|
||||
r := reader.Of[Config](42)
|
||||
ro := SomeReader(r)
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
// TestMonadMap tests applying a function to the value inside a ReaderOption
|
||||
func TestMonadMap(t *testing.T) {
|
||||
t.Run("Map over Some", func(t *testing.T) {
|
||||
ro := Of[Config](21)
|
||||
mapped := MonadMap(ro, utils.Double)
|
||||
result := mapped(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("Map over None", func(t *testing.T) {
|
||||
ro := None[Config, int]()
|
||||
mapped := MonadMap(ro, utils.Double)
|
||||
result := mapped(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMap tests the curried version of MonadMap
|
||||
func TestMap(t *testing.T) {
|
||||
t.Run("Map over Some", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](21),
|
||||
Map[Config](utils.Double),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Map over None", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
None[Config, int](),
|
||||
Map[Config](utils.Double),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadChain tests sequencing two ReaderOption computations
|
||||
func TestMonadChain(t *testing.T) {
|
||||
t.Run("Chain with Some", func(t *testing.T) {
|
||||
ro := Of[Config](21)
|
||||
chained := MonadChain(ro, func(x int) ReaderOption[Config, int] {
|
||||
return Of[Config](x * 2)
|
||||
})
|
||||
result := chained(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("Chain with None", func(t *testing.T) {
|
||||
ro := None[Config, int]()
|
||||
chained := MonadChain(ro, func(x int) ReaderOption[Config, int] {
|
||||
return Of[Config](x * 2)
|
||||
})
|
||||
result := chained(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("Chain returning None", func(t *testing.T) {
|
||||
ro := Of[Config](21)
|
||||
chained := MonadChain(ro, func(x int) ReaderOption[Config, int] {
|
||||
return None[Config, int]()
|
||||
})
|
||||
result := chained(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestChain tests the curried version of MonadChain
|
||||
func TestChain(t *testing.T) {
|
||||
t.Run("Chain with Some", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](21),
|
||||
Chain(func(x int) ReaderOption[Config, int] {
|
||||
return Of[Config](x * 2)
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Chain with None", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
None[Config, int](),
|
||||
Chain(func(x int) ReaderOption[Config, int] {
|
||||
return Of[Config](x * 2)
|
||||
}),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAp tests applying a function wrapped in a ReaderOption
|
||||
func TestMonadAp(t *testing.T) {
|
||||
t.Run("Ap with both Some", func(t *testing.T) {
|
||||
fab := Of[Config](utils.Double)
|
||||
fa := Of[Config](21)
|
||||
result := MonadAp(fab, fa)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Ap with None function", func(t *testing.T) {
|
||||
fab := None[Config, func(int) int]()
|
||||
fa := Of[Config](21)
|
||||
result := MonadAp(fab, fa)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Ap with None value", func(t *testing.T) {
|
||||
fab := Of[Config](utils.Double)
|
||||
fa := None[Config, int]()
|
||||
result := MonadAp(fab, fa)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAp tests the curried version of MonadAp
|
||||
func TestAp(t *testing.T) {
|
||||
t.Run("Ap with both Some", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](utils.Double),
|
||||
Ap[int](Of[Config](21)),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromPredicate tests creating a Kleisli arrow that filters based on a predicate
|
||||
func TestFromPredicate(t *testing.T) {
|
||||
isPositive := FromPredicate[Config](func(x int) bool { return x > 0 })
|
||||
|
||||
t.Run("Predicate satisfied", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](42),
|
||||
Chain(isPositive),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Predicate not satisfied", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](-5),
|
||||
Chain(isPositive),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFold tests extracting the value from a ReaderOption with handlers
|
||||
func TestFold(t *testing.T) {
|
||||
onNone := reader.Of[MyContext]("none")
|
||||
onSome := func(x int) Reader[MyContext, string] {
|
||||
return reader.Of[MyContext](fmt.Sprintf("%d", x))
|
||||
}
|
||||
t.Run("Fold with Some", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
result := Fold(
|
||||
reader.Of[Config]("none"),
|
||||
func(x int) reader.Reader[Config, string] {
|
||||
return reader.Of[Config](fmt.Sprintf("%d", x))
|
||||
},
|
||||
)(ro)
|
||||
assert.Equal(t, "42", result(defaultConfig))
|
||||
})
|
||||
|
||||
// Test with Some
|
||||
g1 := Fold(onNone, onSome)(Of[MyContext](42))
|
||||
assert.Equal(t, "42", g1(defaultContext))
|
||||
|
||||
// Test with None
|
||||
g2 := Fold(onNone, onSome)(None[MyContext, int]())
|
||||
assert.Equal(t, "none", g2(defaultContext))
|
||||
t.Run("Fold with None", func(t *testing.T) {
|
||||
ro := None[Config, int]()
|
||||
result := Fold(
|
||||
reader.Of[Config]("none"),
|
||||
func(x int) reader.Reader[Config, string] {
|
||||
return reader.Of[Config](fmt.Sprintf("%d", x))
|
||||
},
|
||||
)(ro)
|
||||
assert.Equal(t, "none", result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadFold tests the non-curried version of Fold
|
||||
func TestMonadFold(t *testing.T) {
|
||||
t.Run("MonadFold with Some", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
result := MonadFold(
|
||||
ro,
|
||||
reader.Of[Config]("none"),
|
||||
func(x int) reader.Reader[Config, string] {
|
||||
return reader.Of[Config](fmt.Sprintf("%d", x))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, "42", result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("MonadFold with None", func(t *testing.T) {
|
||||
ro := None[Config, int]()
|
||||
result := MonadFold(
|
||||
ro,
|
||||
reader.Of[Config]("none"),
|
||||
func(x int) reader.Reader[Config, string] {
|
||||
return reader.Of[Config](fmt.Sprintf("%d", x))
|
||||
},
|
||||
)
|
||||
assert.Equal(t, "none", result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetOrElse tests getting the value or a default
|
||||
func TestGetOrElse(t *testing.T) {
|
||||
defaultValue := reader.Of[MyContext](0)
|
||||
t.Run("GetOrElse with Some", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
result := GetOrElse(reader.Of[Config](0))(ro)
|
||||
assert.Equal(t, 42, result(defaultConfig))
|
||||
})
|
||||
|
||||
// Test with Some
|
||||
g1 := GetOrElse(defaultValue)(Of[MyContext](42))
|
||||
assert.Equal(t, 42, g1(defaultContext))
|
||||
|
||||
// Test with None
|
||||
g2 := GetOrElse(defaultValue)(None[MyContext, int]())
|
||||
assert.Equal(t, 0, g2(defaultContext))
|
||||
t.Run("GetOrElse with None", func(t *testing.T) {
|
||||
ro := None[Config, int]()
|
||||
result := GetOrElse(reader.Of[Config](99))(ro)
|
||||
assert.Equal(t, 99, result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsk tests retrieving the current environment
|
||||
func TestAsk(t *testing.T) {
|
||||
ro := Ask[MyContext]()
|
||||
result := ro(defaultContext)
|
||||
assert.Equal(t, O.Of(defaultContext), result)
|
||||
ro := Ask[Config]()
|
||||
result := ro(defaultConfig)
|
||||
assert.Equal(t, O.Some(defaultConfig), result)
|
||||
}
|
||||
|
||||
// TestAsks tests applying a function to the environment
|
||||
func TestAsks(t *testing.T) {
|
||||
reader := func(ctx MyContext) string {
|
||||
return string(ctx)
|
||||
}
|
||||
ro := Asks(reader)
|
||||
result := ro(defaultContext)
|
||||
assert.Equal(t, O.Of("default"), result)
|
||||
getPort := Asks(func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
result := getPort(defaultConfig)
|
||||
assert.Equal(t, O.Some(8080), result)
|
||||
}
|
||||
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
// TestMonadChainOptionK tests chaining with a function that returns an Option
|
||||
func TestMonadChainOptionK(t *testing.T) {
|
||||
parsePositive := func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Of(x)
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
// Test with positive number
|
||||
g1 := F.Pipe1(
|
||||
Of[MyContext](42),
|
||||
ChainOptionK[MyContext](parsePositive),
|
||||
)
|
||||
assert.Equal(t, O.Of(42), g1(defaultContext))
|
||||
|
||||
// Test with negative number
|
||||
g2 := F.Pipe1(
|
||||
Of[MyContext](-5),
|
||||
ChainOptionK[MyContext](parsePositive),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), g2(defaultContext))
|
||||
}
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
type GlobalContext struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
// A computation that needs a string context
|
||||
ro := Asks(func(s string) string {
|
||||
return "Hello, " + s
|
||||
t.Run("ChainOptionK with valid value", func(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
result := MonadChainOptionK(ro, parsePositive)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
// Transform GlobalContext to string
|
||||
transformed := Local[string](func(g GlobalContext) string {
|
||||
return g.Value
|
||||
})(ro)
|
||||
t.Run("ChainOptionK with invalid value", func(t *testing.T) {
|
||||
ro := Of[Config](-5)
|
||||
result := MonadChainOptionK(ro, parsePositive)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
|
||||
result := transformed(GlobalContext{Value: "World"})
|
||||
assert.Equal(t, O.Of("Hello, World"), result)
|
||||
t.Run("ChainOptionK with None", func(t *testing.T) {
|
||||
ro := None[Config, int]()
|
||||
result := MonadChainOptionK(ro, parsePositive)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
ro := Of[MyContext](42)
|
||||
result := Read[int](defaultContext)(ro)
|
||||
assert.Equal(t, O.Of(42), result)
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
addFunc := func(x int) int {
|
||||
return x + 10
|
||||
// TestChainOptionK tests the curried version of MonadChainOptionK
|
||||
func TestChainOptionK(t *testing.T) {
|
||||
parsePositive := func(x int) O.Option[int] {
|
||||
if x > 0 {
|
||||
return O.Some(x)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
|
||||
g := F.Pipe1(
|
||||
Of[MyContext](addFunc),
|
||||
Flap[MyContext, int](32),
|
||||
t.Run("ChainOptionK with valid value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](42),
|
||||
ChainOptionK[Config](parsePositive),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("ChainOptionK with invalid value", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](-5),
|
||||
ChainOptionK[Config](parsePositive),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFlatten tests removing one level of nesting
|
||||
func TestFlatten(t *testing.T) {
|
||||
t.Run("Flatten nested Some", func(t *testing.T) {
|
||||
nested := Of[Config](Of[Config](42))
|
||||
flattened := Flatten(nested)
|
||||
result := flattened(defaultConfig)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("Flatten outer None", func(t *testing.T) {
|
||||
nested := None[Config, ReaderOption[Config, int]]()
|
||||
flattened := Flatten(nested)
|
||||
result := flattened(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("Flatten inner None", func(t *testing.T) {
|
||||
nested := Of[Config](None[Config, int]())
|
||||
flattened := Flatten(nested)
|
||||
result := flattened(defaultConfig)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLocal tests transforming the environment before passing it to a computation
|
||||
func TestLocal(t *testing.T) {
|
||||
type GlobalConfig struct {
|
||||
DB Config
|
||||
}
|
||||
|
||||
getPort := Asks(func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
|
||||
globalConfig := GlobalConfig{
|
||||
DB: defaultConfig,
|
||||
}
|
||||
|
||||
result := Local[int](func(g GlobalConfig) Config {
|
||||
return g.DB
|
||||
})(getPort)
|
||||
|
||||
assert.Equal(t, O.Some(8080), result(globalConfig))
|
||||
}
|
||||
|
||||
// TestRead tests executing a ReaderOption with an environment
|
||||
func TestRead(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
result := Read[int](defaultConfig)(ro)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
}
|
||||
|
||||
// TestReadOption tests executing a ReaderOption with an optional environment
|
||||
func TestReadOption(t *testing.T) {
|
||||
ro := Of[Config](42)
|
||||
|
||||
t.Run("ReadOption with Some environment", func(t *testing.T) {
|
||||
result := ReadOption[int](O.Some(defaultConfig))(ro)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("ReadOption with None environment", func(t *testing.T) {
|
||||
result := ReadOption[int](O.None[Config]())(ro)
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadFlap tests applying a value to a function wrapped in a ReaderOption
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
t.Run("Flap with Some function", func(t *testing.T) {
|
||||
fab := Of[Config](utils.Double)
|
||||
result := MonadFlap(fab, 21)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Flap with None function", func(t *testing.T) {
|
||||
fab := None[Config, func(int) int]()
|
||||
result := MonadFlap(fab, 21)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFlap tests the curried version of MonadFlap
|
||||
func TestFlap(t *testing.T) {
|
||||
t.Run("Flap with Some function", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](utils.Double),
|
||||
Flap[Config, int](21),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestMonadAlt tests providing an alternative ReaderOption
|
||||
func TestMonadAlt(t *testing.T) {
|
||||
t.Run("Alt with first Some", func(t *testing.T) {
|
||||
primary := Of[Config](42)
|
||||
fallback := Of[Config](99)
|
||||
result := MonadAlt(primary, fallback)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Alt with first None", func(t *testing.T) {
|
||||
primary := None[Config, int]()
|
||||
fallback := Of[Config](99)
|
||||
result := MonadAlt(primary, fallback)
|
||||
assert.Equal(t, O.Some(99), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Alt with both None", func(t *testing.T) {
|
||||
primary := None[Config, int]()
|
||||
fallback := None[Config, int]()
|
||||
result := MonadAlt(primary, fallback)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAlt tests the curried version of MonadAlt
|
||||
func TestAlt(t *testing.T) {
|
||||
t.Run("Alt with first Some", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](42),
|
||||
Alt(Of[Config](99)),
|
||||
)
|
||||
assert.Equal(t, O.Some(42), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Alt with first None", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
None[Config, int](),
|
||||
Alt(Of[Config](99)),
|
||||
)
|
||||
assert.Equal(t, O.Some(99), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
// TestComplexChaining tests a complex chain of operations
|
||||
func TestComplexChaining(t *testing.T) {
|
||||
// Simulate a complex workflow with environment access
|
||||
result := F.Pipe3(
|
||||
Ask[Config](),
|
||||
Map[Config](func(cfg Config) int { return cfg.Port }),
|
||||
Chain(func(port int) ReaderOption[Config, int] {
|
||||
if port > 0 {
|
||||
return Of[Config](port * 2)
|
||||
}
|
||||
return None[Config, int]()
|
||||
}),
|
||||
Map[Config](func(x int) string { return fmt.Sprintf("%d", x) }),
|
||||
)
|
||||
|
||||
assert.Equal(t, O.Of(42), g(defaultContext))
|
||||
assert.Equal(t, O.Some("16160"), result(defaultConfig))
|
||||
}
|
||||
|
||||
// TestEnvironmentDependentComputation tests computations that depend on environment
|
||||
func TestEnvironmentDependentComputation(t *testing.T) {
|
||||
// A computation that uses the environment to make decisions
|
||||
validateTimeout := func(value int) ReaderOption[Config, int] {
|
||||
return func(cfg Config) O.Option[int] {
|
||||
if value <= cfg.Timeout {
|
||||
return O.Some(value)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("Value within timeout", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](20),
|
||||
Chain(validateTimeout),
|
||||
)
|
||||
assert.Equal(t, O.Some(20), result(defaultConfig))
|
||||
})
|
||||
|
||||
t.Run("Value exceeds timeout", func(t *testing.T) {
|
||||
result := F.Pipe1(
|
||||
Of[Config](50),
|
||||
Chain(validateTimeout),
|
||||
)
|
||||
assert.Equal(t, O.None[int](), result(defaultConfig))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MyContext string
|
||||
|
||||
const defaultContext MyContext = "default"
|
||||
|
||||
func TestSequenceT1(t *testing.T) {
|
||||
|
||||
t1 := Of[MyContext]("s1")
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestFilterOrElse_WithMap(t *testing.T) {
|
||||
onNegative := func(n int) error { return fmt.Errorf("negative number") }
|
||||
|
||||
filter := FilterOrElse[Config](isPositive, onNegative)
|
||||
double := Map[Config](func(n int) int { return n * 2 })
|
||||
double := Map[Config](N.Mul(2))
|
||||
|
||||
cfg := Config{MaxValue: 100}
|
||||
|
||||
|
||||
73
v2/readerresult/profunctor.go
Normal file
73
v2/readerresult/profunctor.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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 (
|
||||
RE "github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
// 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:
|
||||
// - R: 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 R (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a ReaderResult[R, A] and returns a ReaderResult[D, B]
|
||||
//
|
||||
//go:inline
|
||||
func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderResult[R, A], B] {
|
||||
return RE.Promap[R, error](f, g)
|
||||
}
|
||||
|
||||
// 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 RE.Contramap[error, A](f)
|
||||
}
|
||||
107
v2/readerresult/profunctor_test.go
Normal file
107
v2/readerresult/profunctor_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"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) Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
|
||||
// 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 := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, R.Of("8080"), result)
|
||||
})
|
||||
|
||||
t.Run("handles error case", func(t *testing.T) {
|
||||
// ReaderResult that returns an error
|
||||
getError := func(c SimpleConfig) Result[int] {
|
||||
return R.Left[int](fmt.Errorf("error occurred"))
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
toString := strconv.Itoa
|
||||
|
||||
adapted := Promap(simplify, toString)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.True(t, R.IsLeft(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) Result[int] {
|
||||
return R.Of(c.Port)
|
||||
}
|
||||
|
||||
// Adapt to work with DetailedConfig
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getPort)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.Equal(t, R.Of(9000), result)
|
||||
})
|
||||
|
||||
t.Run("preserves error", func(t *testing.T) {
|
||||
getError := func(c SimpleConfig) Result[int] {
|
||||
return R.Left[int](fmt.Errorf("config error"))
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Port: d.Port}
|
||||
}
|
||||
|
||||
adapted := Contramap[int](simplify)(getError)
|
||||
result := adapted(DetailedConfig{Host: "localhost", Port: 9000})
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
})
|
||||
}
|
||||
@@ -719,7 +719,7 @@ func BiMap[R, A, B any](f Endomorphism[error], g func(A) B) Operator[R, A, B] {
|
||||
// // adapted now accepts DB instead of Config
|
||||
//
|
||||
//go:inline
|
||||
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 reader.Local[Result[A]](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -369,5 +369,3 @@ func TestIntegration_ReduceWithIndexToMap(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -52,3 +52,61 @@ func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
|
||||
func AltMonoid[A any](zero Lazy[Result[A]]) Monoid[A] {
|
||||
return either.AltMonoid(zero)
|
||||
}
|
||||
|
||||
// FirstMonoid creates a Monoid for Result[A] that returns the first Ok (Right) value.
|
||||
// This monoid prefers the left operand when it is Ok, otherwise returns the right operand.
|
||||
// The empty value is provided as a lazy computation.
|
||||
//
|
||||
// This is equivalent to AltMonoid but implemented more directly.
|
||||
//
|
||||
// Truth table:
|
||||
//
|
||||
// | x | y | concat(x, y) |
|
||||
// | --------- | --------- | ------------ |
|
||||
// | err(e1) | err(e2) | err(e2) |
|
||||
// | ok(a) | err(e) | ok(a) |
|
||||
// | err(e) | ok(b) | ok(b) |
|
||||
// | ok(a) | ok(b) | ok(a) |
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "errors"
|
||||
// zero := func() result.Result[int] { return result.Error[int](errors.New("empty")) }
|
||||
// m := result.FirstMonoid[int](zero)
|
||||
// m.Concat(result.Of(2), result.Of(3)) // Ok(2) - returns first Ok
|
||||
// m.Concat(result.Error[int](errors.New("err")), result.Of(3)) // Ok(3)
|
||||
// m.Concat(result.Of(2), result.Error[int](errors.New("err"))) // Ok(2)
|
||||
// m.Empty() // Error(error("empty"))
|
||||
//
|
||||
//go:inline
|
||||
func FirstMonoid[A any](zero Lazy[Result[A]]) M.Monoid[Result[A]] {
|
||||
return either.FirstMonoid(zero)
|
||||
}
|
||||
|
||||
// LastMonoid creates a Monoid for Result[A] that returns the last Ok (Right) value.
|
||||
// This monoid prefers the right operand when it is Ok, otherwise returns the left operand.
|
||||
// The empty value is provided as a lazy computation.
|
||||
//
|
||||
// Truth table:
|
||||
//
|
||||
// | x | y | concat(x, y) |
|
||||
// | --------- | --------- | ------------ |
|
||||
// | err(e1) | err(e2) | err(e1) |
|
||||
// | ok(a) | err(e) | ok(a) |
|
||||
// | err(e) | ok(b) | ok(b) |
|
||||
// | ok(a) | ok(b) | ok(b) |
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import "errors"
|
||||
// zero := func() result.Result[int] { return result.Error[int](errors.New("empty")) }
|
||||
// m := result.LastMonoid[int](zero)
|
||||
// m.Concat(result.Of(2), result.Of(3)) // Ok(3) - returns last Ok
|
||||
// m.Concat(result.Error[int](errors.New("err")), result.Of(3)) // Ok(3)
|
||||
// m.Concat(result.Of(2), result.Error[int](errors.New("err"))) // Ok(2)
|
||||
// m.Empty() // Error(error("empty"))
|
||||
//
|
||||
//go:inline
|
||||
func LastMonoid[A any](zero Lazy[Result[A]]) M.Monoid[Result[A]] {
|
||||
return either.LastMonoid(zero)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user